第二章-this、call 和 apply
# this
跟别的语言不一样的是,JavaScript 的 this 总是指向一个对象,具体指向哪个对象是由运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
# this 的指向
除去不常用的 with 和 eval 的情况,在实际应用中,this 的指向大致可以分为以下四种情况:
- 作为对象的方法调用;
- 作为普通函数调用;
- 构造器调用;
- Function.prototype.call 或 Function.prototype.apply 调用。
# 作为对象的方法调用
当哈数作为对象的方法被调用时,this 指向该对象:
var obj = {
a: 1,
getA: function () {
console.log(this === obj); // true
console.log(this.a); // 1
},
};
obj.getA();
2
3
4
5
6
7
8
# 作为普通函数调用
当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指向全局对象。在浏览器的 JavaScript 里,这个全局对象就是 window 对象。
window.name = 'globalName';
var getName = function() {
return this.name;
}
console.log(getName()); // globalName
2
3
4
5
6
7
或者
window.name = 'globalName';
var myObject = {
name: 'jack',
getName: function () {
return this.name;
},
};
var getName = myObject.getName;
console.log(getName()); // globalName
2
3
4
5
6
7
8
9
10
11
# 构造器调用
JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了 new 运算符,使得构造器看起来像一个类。除了自带的一些内置函数,大部分 JavaScript 函数都可以当做构造器使用。当用 new 运算符调用函数时,该函数会返回一个对象,通常情况下, 构造器里的 this 就指向返回的这个对象,如下代码:
var MyClass = function() {
this.name = 'jack';
}
var obj = new MyClass();
console.log(obj.name); // jack
2
3
4
5
6
使用 new 调用构造器时,还要注意一个问题,如果构造器显式的返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:
var MyClass = function() {
this.name = 'jack';
return {
name: 'tom'
}
};
var obj = new MyClass();
console.log(obj.name); // tom
2
3
4
5
6
7
8
9
如果构造器不显式的返回任何数据,或者是返回一个非对象类型的数据,就不会造成上面的问题。
# Function.prototype.call 和 Function.prototype.apply 的调用
跟普通的函数调用相比,用 Function.prototype.call 和 Function.prototype.apply 可以动态的改变传入函数的 this:
var obj1 = {
name: 'jack',
getName: function () {
return this.name;
},
};
var obj2 = {
name: 'tom',
};
console.log(obj1.getName()); // jack
console.log(obj1.getName.call(obj2)); // tom
2
3
4
5
6
7
8
9
10
11
12
13
call 和 apply 方法能够很好的体现 JavaScript 的函数式语言特性,在 JavaScript 中,几乎每一次编写函数式语言风格的代码,都离不开 call 和 apply。在 JavaScript 诸多版本的设计模式中,也用到了 call 和 apply。
# 丢失的 this
这是一个很常见的问题,我们先看下面的代码:
var obj = {
myName: 'jack',
getName: function () {
return this.myName;
},
};
console.log(obj.getName()); // jack
var getName2 = obj.getName;
console.log(getName2()); // undefined
2
3
4
5
6
7
8
9
10
11
当调用 obj.getName 时,getName 方法是作为 obj 对象的属性被调用的,所以此时的 this 指向 obj 对象,输出 jack。当用另外一个变量 getName2 来引用 obj.getName,并且调用 getName2 时,此时是普通函数调用方式,所以 this 指向全局 window,因此输出的结果是 undefined。
# call 和 apply
apply 接受两个参数,第一个参数指定了函数体内容的 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以是数组,也可以是类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数,如:
var func = function (a, b, c) {
console.log([a, b, c]); // [1, 2, 3]
};
func.apply(null, [1, 2, 3]);
2
3
4
5
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向、从第二个参数开始往后,每个参数一次传入函数,如:
var func = function (a, b, c) {
console.log([a, b, c]); // [1, 2, 3]
};
func.call(null, 1, 2, 3);
2
3
4
5
当使用 call 或者 apply 时,如果我们传入的第一个参数是 null,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window,如:
var func = function(a, b, c) {
console.log(this === window); // true
};
func.apply(null, [1, 2, 3]);
2
3
4
5
# call 和 apply 的用途
# 1. 改变 this 指向
var obj1 = {
name: 'seven',
};
var obj2 = {
name: 'jack',
};
window.name = 'tom';
var getName = function () {
console.log(this.name);
};
getName(); // tom
getName.call(obj1); // seven
getName.call(obj2); // jack
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2. Function.prototype.bind
大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this 指向,即使没有原生的实现,我们也可以自己来模拟一个,如:
Function.prototype.bind = function (context) {
var self = this;
return function () {
return self.apply(context, arguments);
};
};
var obj = {
name: 'sven',
};
var func = function () {
console.log(this.name);
}.bind(obj);
func();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply(context, arguments) 这句代码才是执行原来的 func 函数,并且指定 context 对象为 func 函数体内的 this。
# 3. 借用其他对象的方法
借用构造函数,实现类似继承的效果
var A = function (name) {
this.name = name;
};
var B = function () {
A.apply(this, arguments);
};
B.prototype.getName = function () {
return this.name;
};
var b = new B('sven');
console.log(b.getName()); // sven
2
3
4
5
6
7
8
9
10
11
12
13
14
借用构造函数的第二个场景,函数的参数列表 arguments 是一个类数组对象,如果我们想往 arguments 中添加一个新的元素,通常会借用 Array.prototype.push:
(function(){
Array.prototype.push.call(arguments, 3);
console.log(arguments); [1, 2, 3]
})(1, 2)
2
3
4