003_JS基础_面向对象基础
3.1 对象
引入:在js中表示一个人的信息(name, gender, age)通过var申明三个变量,但是这样使用基本数据类型的变量,他们是互相独立的,没有联系;
此时就需要使用对象,对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性;
对象的分类
- 內建对象:有ES标准中定义的对象,在任何的ES的实现中都可以使用,例如:Math, String, Boolean, Function, Object……;
- 宿主对象:由js的运行环境提供的对象,目前来讲是浏览器提供的对象,例如:BOM, DOM;
- 自定义对象:由开发人员自己创建;
对象的基本操作
- 创建对象:使用new关键字调用的函数,是构造函数Constructor,构造函数是专门用来创建对象的函数;使用typeof检查,返回object;语法 var obj = new Object();
- 向对象中添加属性,语法:对象.属性名 = 属性值;
- 读取对象的属性,语法:对象.属性名;
- 修改对象的某个属性值,语法:对象.属性名 = 新的属性值;
- 删除对象的某个属性,语法:delete 对象.属性名;
//创建一个对象
var obj = new Object();
//为该对象添加属性
obj.name = "frank";
obj.gender = "male";
obj.age = 20;
/*
* 读取对象的属性:
* 格式: 对象.属性名
* 注意,如果读取对象中没有的属性,不会报错,会返回undefined;
*/
console.log(obj.name);
console.log(obj.hello);
/*
* 修改对象的属性值:
*/
console.log(obj.name); //修改前
obj.name = "小明";
console.log(obj.name); //修改后
/*
* 删除对象的属性
* 格式: delete 对象.属性名
*/
delete obj.name;
属性名和属性值
属性名:属性名不强制要求标识符的规范,可以自己随便取,但是我们尽量按照标识符的命名要求,如果实在作死,取了奇怪的命名,例如纯数字,则不能使用“.”来对我们的对象进行基本操作,应该使用这种模式: 对象[“属性名”] = 属性值;
/*
* 奇怪的属性名处理情况,不能使用.操作符
* 有时使用[]来操作对象更加的灵活,[]中可以传一个变量,此时变量值就是会读取的对应的属性名;
*/
var obj2 = new Object();
obj["123"] = 100;
obj["name"] = "小明";
//定义一个变量,值为变量名:
var x = "name";
console.log(obj["123"]); //输出:100
console.log(obj[x]); //输出:小明
属性值:可以是任意的数据类型,甚至也可以是一个对象;
in 运算符,可以检查一个对象中是否含有指定的属性;有则返回true,否则返回false;
var obj = new Object();
var obj2 = new Object();
obj2.course = "语文";
obj.name = "小明";
obj.score = obj2; //属性值还可以是对象
//使用in运算符检查
console.log("age" in obj); //false
console.log("name" in obj); //true
引用数据类型
JS中的变量都是存储到栈内存中的,基本数据类型的值都是在栈内存中存储,值与值之间是独立存在的,修改一个变量不会对其他变量产生影响;对象(即引用数据类型)是存储到堆内存 中,每创建一个新的对象,在堆内存中就会开辟一个新的内存空间,而变量保存的是对象的内存地址(对象的引用),如果两个变量保存的是一个对象的引用,当通过一个对象修改属性值时,另一个也要受到影响;
对象字面量
/*
* 使用字面量来创建对象
* 使用这种方式可以在创建时直接指定属性
* 对象的属性名,引号可以加可以不加;若名字特殊,一定要加引号;
*/
var obj = {
name: "Frank",
age: 20,
"gender": "男",
test:{
name: "小明",
age: 15
}
};
console.log(obj.test.age); //输出15
对象的方法
函数也可以成为对象的属性,此时这个函数是这个对象的方法,调用函数则称为调用对象的方法
var obj = new Object();
obj.name = "frank";
obj.age = 18;
//将一个匿名函数赋值给一个对象的属性
obj.showInformaton = function(){
console.log("姓名:" + obj.name + ", 年龄:" + obj.age);
}
obj.showInformaton();
/*
* 另一种写法
*/
var obj2 = {
name: "Alice",
age: 20,
showName: function(){
console.log(obj2.name);
}
};
obj2.showName();
枚举对象中的属性
如何将一个对象中的所有属性列出来?
/*
* 使用for……in语句
*/
var obj = {
name: "frank",
gender: "男",
age: 21,
hometown: "四川"
};
//将对象中的属性名读取出来
for(var n in obj){
console.log("属性名:" + n);
}
//将对象中的各个属性值读取出来(注意不能使用“.”运算符)
for(var n in obj){
console.log(obj[n]);
}
//综合一下
for(var n in obj){
console.log(n + ":" + obj[n]);
}
3.2 函数
函数也是对象,函数中可以封装一些功能,在需要时可以执行这些功能;即函数可以保存一些代码,在需要时调用;使用typeof检查一个函数对象时,将返回function;
定义函数
- 方法一:
- 创建一个函数对象,可以将要封装的代码以字符串的形式传递给构造函数;
- 函数也是一种对象,它拥有普通对象的所有功能;
//创建一个函数对象
var fun = new Function("alert('Hello')");
//调用函数:函数对象+();
fun();
//可以给该函数对象添加一个属性
fun.name = "function";
- 方法二:
- 这种方式较为常用,使用函数声明来创建一个函数;
/*
使用函数声明来创建一个函数,格式如下
function 函数名([形参1,形参2, ……]){
函数体;
}
*/
//创建一个函数:
function hello() {
alert("创建函数的第二种方法");
}
//调用函数:函数名+();
hello();
- 方法三:
- 使用函数表达式来创建一个函数;
- 这种方式的实质是声明了一个匿名函数,并将其复制给了一个变量;
/*
使用函数表达式来创建一个函数
var 函数名 = function([形参1,形参2, ……]){
语句;
}
*/
//创建函数:
var fun3 = function() {
alert("这是匿名函数中封装的代码");
};
//调用函数:变量+ ();
fun3();
函数的参数
在调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数内部对应的形参(形式参数),在调用函数时不会检查实参的类型,所以要注意,是否有可能接收到非法参数;在调用函数时,解析器也不会检查实参的数量,多余的实参不会被赋值,若少了,没有对应的实参,该形参则是undefined;
函数的返回值
使用return后的值会作为函数的执行结果返回;在函数中,return后的语句不会被执行;
/*
* 求三个值的和
*/
function sum(a, b, c){
return a + b + c;
}
var result = sum(1, 2, 3);
console.log(result);
函数的返回值可以是任意数据类型,可以是对象,也可以是一个函数。
/*
* 函数的返回值可以是任意类型
* 可以是对象
*/
function fun(){
var obj = {name: "frank"};
return obj;
}
console.log(fun().name);
var a = fun();
console.log(a);
console.log(a.name);
/*
* 函数的返回值也可以是函数
*/
function fun2(){
function fun3(){
alert("Fun3");
}
//注意区别 fun3(),是返回函数的返回值
//fun是返回函数对象本身
//return fun3();
return fun3;
}
var b = fun2(); //将fun2的返回值(一个函数对象)赋值给b
console.log(b);
b(); //调用fun3函数
//也可以直接这样调用
fun2()();
立即执行函数
函数定义完之后,立即被调用,这种函数就是立即执行函数;特点:这种函数往往只能执行一次;
/*
* 立即执行函数:
* 将一个匿名函数使用()包括起来;
* 函数调用: 函数对象后加();
*/
(function(){
alert("这是一个匿名函数!");
})();
/*
* 一个带参数的立即执行函数
*/
(function(a, b){
console.log(a + "+" + b + "=" + (a+b));
})(1, 2);
作用域
作用域是指一个变量的作用范围,在JS中一共有两种作用域;
1. 全局作用域
直接编写在script标签中的js代码,都在全局作用域;全局作用域在页面打开时创建,在页面关闭时销毁;全局作用域中的变量都是全局变量;
在全局作用域中,有一个全局对象window,它代表着浏览器窗口,由浏览器创建,我们可以直接使用;
在全局作用域中,创建的变量都会作为window对象的属性保存;创建的函数都会作为window对象的方法;
var a = 10;
console.log(a);
console.log(window.a);
function fun(){
console.log("fun");
}
fun();
window.fun();
变量声明提前
使用var关键字声明变量时,会在所有的代码执行之前被声明(但是不会赋值),如果不使用var关键字,则变量不会被声明提前;
函数声明提前
使用函数声明形式创建的函数function 函数名(){ },它会在所有的代码执行前就被创建,所以函数可以在函数声明前的位置处被调用; 使用函数表达式创建的函数不会被声明提前:var a = function(){};
2. 函数作用域(局部作用域)
调用函数时创建函数作用域,函数执行结束后,函数作用域销毁;每调用一次函数就会创建一个新的函数作用域,他们之间相互独立;
在函数作用域中,可以访问到全局作用域的变量; 在全局作用域中,不能访问到函数作用域的变量;
就近原则:当在函数作用域中操作变量时,先在自身作用域中寻找,若有则使用,没有则在上一级作用域中去寻找,直到找到全局作用域中,若全局作用域中也没有,则会报错:ReferenceError;
在函数中,若要访问全局作用域中的变量,使用window.变量名;
在函数作用域中也有声明提前这个特性; 在函数中不使用var声明的变量,则是一个全局变量;
形参就相当于在函数作用域中声明了变量;
this
解析器在调用函数时,每次都会向函数中传递一个隐含的参数,这个隐含的参数就是this,this指向的是一个对象,这个对象我们称之为函数执行的上下文对象(当前对象);
根据函数的调用方式不同,this会指向不同的对象;
- 以函数的方式调用时,this永远都是window;
- 以方法的形式调用,this就是调用方法的那个对象;
function fun(){
console.log(this.name);
}
var obj = {
name: "frank",
showName: fun
}
var obj2 = {
name: "小猪佩奇",
showName: fun
}
//以方法的形式调用
obj.showName();
obj2.showName();
var name = "全局";
//以函数形式调用
fun();
使用工厂方法创建对象
function createPerson(name, age, gender){
//创建一个新对象
var obj = new Object();
向对象中添加属性
obj.name = name;
obj.age = age;
obj.gender = gender;
obj.showName = function(){
console.log(this.name);
}
//返回该对象
return obj;
}
var obj1 = createPerson("frank", 18, "男");
var obj2 = createPerson("kitty", 20, "女")
obj1.showName();
obj2.showName();
使用工厂方法创建的对象使用的构造函数都是Object,这种方式不常用;
构造函数
创建一个构造函数,专门用来创建Person对象,构造函数其实就是一个普通的函数,创建方式和普通函数一样,但是习惯上我们对构造函数的函数名会采取首字母大写
- 构造函数和普通函数的调用方式不同,普通函数直接调用,构造函数需要使用new关键字来调用;
- 构造函数的执行流程:
- 会立即创建一个新的对象
- 将新建对象设置为函数的this,在构造函数中可以使用this来引用新建的对象
- 逐行执行函数的代码
- 将新建的对象作为返回值返回
- 使用同一个构造函数创建的对象称为一类对象,也将构造函数称为一个类,我们将通过一个构造函数创建的对象,称之为该类的实例;
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.showName = function() {
console.log(this.name);
};
};
function Dog(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("I can say 'hello'");
};
}
//将对象实例化
var p1 = new Person("Frank", 18, "男");
var p2 = new Person("Alice", 20, "女");
var d1 = new Dog("Tom", 3);
p1.showName();
d1.sayHello();
/*
* 使用instanceof可以检查一个对象是否是一个类的实例
*/
console.log(p1 instanceof Person);
目前我们使用的方法都是在构造函数内部创建的,例如方法showName(),也就是说,构造函数每执行一次,就会创建一个新的showName()方法,就是说每一个实例的showName()方法都是唯一的,执行10000次就会创建10000个方法,而这10000个都是一模一样的,这是完全没有必要,我们完全可以使所有的对象使用同一个方法;
原型对象
我们所创建的每一个函数,解析器都会在函数中创建一个prototype,这个属性对应着一个对象, 这个对象就是我们称为的原型对象;
- 如果函数作为普通函数调用,prototype无作用;
- 如果函数以构造函数的形式调用时,它所创建的对象都会有一个隐含的属性,指向该构造函数的原型对象;我们可以通过__proto__(两根下划线)来访问该属性;
原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们就可以通过这个特点,将对象中所共有的内容,统一设置到原型对象中。
当我们访问对象的属性或者方法时,先在对象本身中寻找,若没有则去原则对象中寻找,以后我们在创建构造函数时,可以将这些对象的公共的属性和方法,统一添加到构造函数的原型对象中,这样不用为每一个对象都添加,也不会影响到全局作用域,便可以使每一个对象都具有这些属性和方法了。
function MyClass() {
}
//向MyClass的原型中添加属性
MyClass.prototype.a = 123;
//向MyClass的原型中添加方法
MyClass.prototype.sayHello = function() {
console.log("Hello");
}
var mc = new MyClass();
mc.a = "mc的属性";
var mc2 = new MyClass();
console.log(MyClass.prototype);
console.log(mc.__proto__ == MyClass.prototype); //true
console.log(mc2.__proto__ == MyClass.prototype); //true
//访问对象的属性或方法(先对象本身,在原型对象)
console.log(mc.a); //"mc的属性"
console.log(mc2.a); //123
mc.sayHello(); //“Hello”
//使用in运算符检查
//先检查对象本身,在检查原型对象中,若其中一个含有则返回true
console.log("a" in mc); //true
//可以使用对象的hasOwnProperty()来检查对象自身中是否含有某个属性
console.log(mc.hasOwnProperty("age")); //false
console.log(mc.hasOwnProperty("a")); //true
console.log(mc2.hasOwnProperty("a"));// false
原型对象也是对象,所以它也有原型:当我们在使用一个对象的属性或者方法时,会先在自身中寻找,自身中如果有则使用,如果没有则去原型对象中寻找。如果原型对象中有,则使用,如果在没有就去原型的原型中寻找;直到找到Object对象的原型,Object对象的原型没有原型(null),如果在Object中依然没有找到,则会返回undefined;
//寻找hasOwnProperty()方法
console.log(mc.__proto__.__proto__.hasOwnProperty("hasOwnProperty");//true
toString()
我们直接在页面中打印一个对象时,实际上是输出的toString()方法的返回值;
如果我们希望在输出对象时不输出[Object Object],可以为对象添加一个toString()方法;
//给对象std添加一个toString()方法:
std.toString = function(){
return "给对象添加了一个toString()方法";
};
//修改Person原型的toString():
Student.prototype.toString = function(){
return "姓名:" + this.name + ", 性别:" + this.gender + ", 年龄:" + this.age;
}
垃圾回收
- 当一个对象,没有任何的变量或者属性对它进行引用,此时我们将永远无法操作该对象,此时这种对象就是一个垃圾;这种对象过多会占用大量的内存空间,导致程序运行变慢;
- 在JS中有自动垃圾回收机制,会自动将这些垃圾对象从内存中销毁;
- 我们需要做的是将不再使用的对象设置为null即可,如果不设置obj为null,则该对象还存在变量对其进行引用,该对象不会被自动销毁;
函数的方法
call()和apply(),这两个方法都是函数对象的方法,需要通过函数对象来调用,当对函数调用call()和apply()两个方法时,都会调用函数执行;
- 在调用call()和apply()方法时,可以将一个对象指定为第一个参数,此时这个对象会成为函数执行时的this;
- call()方法可以将实参放在第一个参数(某个对象)之后依次传递;
- apply()需要将实参封装到一个数组中统一传递;
function test(a, b){
console.log(this);
console.log("a = " + a);
console.log("b = " + b);
}
var obj3 = new Object();
test();
//apply()方法需要把实参封装到数组中
test.call(obj3, 1, 2);
//apply()方法需要把实参封装到数组中
var arr = [4, 5];
test.apply(obj3, arr);
总结一下this
- 以函数形式调用,this永远都是window;
- 以方法的形式调用,this是调用方法的对象;
- 以构造函数的形式调用时,this是新创建的那个对象;
- 使用call()或者apply()调用时,this是第一个参数中指定的对象;
arguments
在调用函数时,浏览器每次都将传入两个参数:
- 函数的上下文对象this
- 封装实参的对象 arguments
- arguments是一个类数组对象,它可以通过索引来操作数据,也可以获取长度;
- 在调用函数时,我们所传的实参都会在arguments中保存;
- arguments.length可以获取实参的个数;
- 注意:我们即使不定义形参,我们也可以通过arguments来使用实参,arguments[0] 表示第一个实参;
- arguments中有一个属性callee, 这个属性对应的是一个函数对象,就是当前正在指向的函数的对象;
function fun(){
console.log(arguments); //[object Arguments]
console.log(arguments.length); //3
console.log(arguments[1]); //3
console.log(arguments.callee);
console.log(arguments.callee == fun); //true
}
fun("Hello", "a", 4);
3.3 数组
数组(Array),也是一个对象,用来存储一些值,数组是使用数字来作为索引来操作元素。索引(index):以0开始的正整数。
数组的基本操作
- 创建一个数组对象: var arr = new Array();
- 创建一个指定长度的数组: var arr = new Array(n);
- 向数组中添加元素:数组[索引] = 值;
- 获取数组的长度:数组.length;
- 修改数组的长度:数组.length = n; 修改比原来的长,多出来的将空出来 修改比原来短,则多出的会被删除;
数组字面量
使用数组字面量来创建一个数组:
//构造函数创建一个数组
var arr = new Array();
//使用字面量来创建空数组
var arr2 = [];
//使用字面量创建数组时,在创建时就指定数组中的元素;
var arr3 = [1, 2, 3, 4];
//使用构造函数创建对象也可以在创建时赋初值
var arr4 = new Array(1, 3, 4);
//注意这种情况
var arr5 = [10]; //数组中有一个元素,10
var arr6 = new Array(10); //数组长度为10
//数组中的元素的值可以是任意数据类型
var obj = {name: "frank"};
var arr7 = ["Hello", 1, false, obj];
var arr8 = [{name: "frank"},{name: "Alice"},{name: "Tom"}];
//数组中存放函数:
var arr9 = [function(){console.log("这是存放在数组中的第一个函数");},
function(a,b){console.log("和为:" + (a+b));}];
console.log(arr9);
//调用函数
arr9[1](1,2);
//二维数组
var arr10 = [[1,2],[3,4,5]];
console.log(arr10[1][0]);
遍历数组
- 方法一:使用for循环
//打印数组stdArr中的每一个元素:
for(var i = 0; i < stdArr.length; i++) {
console.log(stdArr[i].toString());
}
- 方法二:使用forEach()方法
- forEach()方法需要一个函数作为参数
- 像这种函数,又我们创建但是不需要我们调用的,我们称之为回调函数
- 数组中有几个元素,函数就会执行几次,每次执行时,浏览器会将遍历到的元素以实参的形式传递进来;我们可以定义形参来获取这些内容;
- 浏览器会在回调函数中传递三个参数:
- 第一个:当前正在遍历的元素的值;
- 第二个:当前正在遍历的元素的索引;
- 第三个:当前正在遍历的数组;
var arr = ["Hello", "World", "guojingan", 5];
arr.forEach(function(value, index, obj){
console.log(value);
});
数组的一些常见方法
- push()
- 在数组末尾添加一个或多个元素,返回值是添加元素之后的数组的长度;
- pop()
- 删除数组的最后一个元素,返回值是被删除的元素;
- shift()
- 删除数组的第一个元素,返回值是被删除的元素;
- unshift()
- 向数组开头添加一个或多个元素,并返回添加后数组的长度;
- slice()
- 可以从数组中截取指定元素;
- 不会改变原本的数组,而是将截取的结果封装到一个数组中并返回;
- 参数:
- 第一个:截取开始的索引位置,包括该索引;
- 第二个:截取结束的索引位置,不包括该索引;(第二个参数选填,不填则视为截取第一个参数到数组结束);
- 注意:索引可以传递一个负值, 最后一个元素索引为-1;
- splice()
- 可以删除数组中指定的元素;
- 会影响原数组,该方法的返回值是被删除的元素;
- 参数:
- 第一个: 表示开始位置的索引;
- 第二个: 表示要删除的个数;
- 第三个及以后:可以将一些元素插入到开始索引位置的前面;
- concat()
- 连接数组,不会改变原数组,返回值是连接后的数组;
- join()
- 该方法可以将一个数组转换成一个字符串,返回值是转换后的字符串
- 在join()默认使用逗号连接,我们可以指定一个字符串作为参数,这个会成为新的连接符;
- reverse()
- 反转数组;
- 该方法会直接修改原数组;
- sort()
- 用来对数组进行排序,该方法会改变原数组;
- 默认按照Unicode编码进行排序;
- 按照Unicode编码来排序,在对纯数字排序时,有可能得到一个错误的结果;
- 我们可以自己来指定排序规则,我们可以在sort()中添加一个回调函数,来指定排序规则,回调函数中需要两个形参,浏览器会分别使用数组中的元素作为实参去调用回调函数;
- 浏览器会根据回调函数的返回值来决定元素的位置:如果返回一个大于0的值,则交换位置;如果返回一个小于或等于0的值,则元素位置不变;
a.sort(function(a, b){
return b - a; //降序
//return a - b; //升序
});