正文
很多人当谈到 JavaScript 中的
this
的时候会感到头疼,因为在 JavaScript 中,
this
是动态绑定,或称为运行期绑定的,这就导致 JavaScript 中的
this
关键字有能力具备多重含义,带来灵活性的同时,也为初学者带来不少困惑。
上下文 vs 作用域
每个函数调用都有与之相关的作用域和上下文。首先需要澄清的问题是上下文和作用域是不同的概念。很多人经常将这两个术语混淆。
作用域(scope) 是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。而上下文(context)是用来指定代码某些特定部分中
this
的值。
从根本上说,作用域是基于函数(function-based)的,而上下文是基于对象(object-based)的。换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的。上下文总是被调用函数中关键字
this
的值,是调用当前可执行代码的对象的引用。说的通俗一点就是:
this
取值,是在函数真正被
调用执行
的时候确定的,而
不是
在函数定义的时候确定的。
全局上下文
无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)
this
都指向全局对象。当然具体的全局对象和宿主环境有关。
在浏览器中,
window
对象同时也是全局对象:
console.log(this === window); // true
NodeJS 中,则是
global
对象:
console.log(this); // global
函数上下文
由于其运行期绑定的特性,JavaScript 中的
this
含义要丰富得多,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为函数调用,作为对象方法调用,作为构造函数调用,和使用
apply
或
call
调用。下面我们将按照调用方式的不同,分别讨论
this
的含义。
作为函数直接调用
作为函数直接调用时,要注意 2 种情况:
非严格模式
在非严格模式下执行函数调用,此时
this
默认指向全局对象。
function f1(){
return this;
}
//在浏览器中:
f1() === window; //在浏览器中,全局对象是window
//在Node中:
f1() === global;
严格模式 ‘use strict’;
在严格模式下,
this
将保持他进入执行上下文时的值,所以下面的
this
并不会指向全局对象,而是默认为 undefined 。
'use strict'; // 这里是严格模式
function test() {
return this;
};
test() === undefined; // true
作为对象的方法调用
在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,内部的
this
指向该对象。
var Obj = {
prop: 37,
getProp: function() {
return this.prop;
}
};
console.log(Obj.getProp()); // 37
上面的例子中,当
Obj.getProp()
被调用时,方法内的
this
将指向
Obj
对象。值得注意的是,这种行为根本不受函数定义方式或定义位置的影响。在前面的例子中,我们在定义对象
Obj
的同时,将成员
getProp
定义了一个匿名函数。但是,我们也可以首先定义函数,然后再将其附加到
Obj.getProp
。所以,下面的代码和上面的例子是等价的:
var Obj = {
prop: 37
};
function independent() {
return this.prop;
}
Obj.getProp = independent;
console.log(Obj.getProp()); // logs 37
JavaScript 非常灵活,现在我们把对象的方法赋值给一个变量,然后直接调用这个函数变量又会发生什么呢?
var Obj = {
prop: 37,
getProp: function() {
return this.prop;
}
};
var test = Obj.getProp
console.log(test()); // undefined
可以看到,这时候
this
指向全局对象,这个例子
test
只是引用了
Obj.getProp
函数,也就是说这个函数并不作为
Obj
对象的方法调用,所以,它是被当作一个普通函数来直接调用。因此,
this
指向全局对象。
一些坑
我们来看看下面这个例子:
var prop = 0;
var Obj = {
prop: 37,
getProp: function() {
setTimeout(function() {
console.log(this.prop) // 结果是 0 ,不是37!
},1000)
}
};
Obj.getProp();
正如你所见,
setTimeout
中的
this
向了全局对象,这里不是把它当作函数的方法使用吗?这一点经常让很多初学者疑惑;这种问题是很多异步回调函数中也会普遍会碰到,通常有个土办法解决这个问题,比如,我们可以利用 闭包 的特性来处理:
var Obj = {
prop: 37,
getProp: function() {
var self = this;
setTimeout(function() {
console.log(self.prop) // 37
},1000)
}
};
Obj.getProp();
其实,
setTimeout
和
setInterval
都只是在全局上下文中执行一个函数而已,即使是在严格模式下:
'use strict';
function foo() {
console.log(this); // Window
}
setTimeout(foo, 1);
记住
setTimeout
和
setInterval
都只是在全局上下文中执行一个函数而已,因此 this 指向全局对象。 除非你实用箭头函数,
Function.prototype.bind
方法等办法修复。至于解决方案会在后续的文章中继续讨论。
作为构造函数调用
JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。作为又一项约定通用的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。
当一个函数用作构造函数时(使用
new
关键字),它的
this
被绑定到正在构造的新对象,也就是我们常说的实例化出来的对象。
function Person(name) {
this.name = name;
}
var p = new Person('愚人码头');
console.log(p.name); // "愚人码头"
几个陷阱
如果构造函数具有返回对象的
return
语句,则该返回对象将是
new
表达式的结果。
function Person(name) {
this.name = name;
return { title : "前端开发" };
}
var p = new Person('愚人码头');
console.log(p.name); // undefined
console.log(p.title); // "前端开发"
相应的,JavaScript 中的构造函数也很特殊,如果不使用
new
调用,则和普通函数一样, this 仍然执行全局:
function Person(name) {
this.name = name;
console.log(this); // Window
}
var p = Person('愚人码头');
箭头函数中的 this
在箭头函数中,
this
与封闭词法上下文的
this
保持一致,也就是说由上下文确定。
var obj = {
x: 10,
foo: function() {
var fn = () => {
return () => {
return () => {
console.log(this); //{x: 10, foo: ƒ} 即 obj
console.log(this.x); //10
}
}
}
fn()()();
}
}
obj.foo();
obj.foo
是一个匿名函数,无论如何, 这个函数中的 this 指向它被创建时的上下文(在上面的例子中,就是
obj
对象)。这同样适用于在其他函数中创建的箭头函数:这些箭头函数的this 被设置为外层执行上下文。
// 创建一个含有bar方法的obj对象,bar返回一个函数,这个函数返回它自己的this,
// 这个返回的函数是以箭头函数创建的,所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,它反过来又设置返回函数的值。
var obj = {
bar: function() {
var x = (() => this);
return x;
}
};
// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// x所指向的匿名函数赋值给fn。
var fn = obj.bar();
// 直接调用fn而不设置this,通常(即不使用箭头函数的情况)默认为全局对象,若在严格模式则为undefined
console.log(fn() === obj); // true
// 但是注意,如果你只是引用obj的方法,而没有调用它(this是在函数调用过程中设置的)
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true
在上面的例子中,一个赋值给了
obj.bar
的函数(称为匿名函数 A),返回了另一个箭头函数(称为匿名函数 B)。因此,函数B的this被永久设置为
obj.bar
(函数A)被调用时的
this
。当返回的函数(函数B)被调用时,它this始终是最初设置的。在上面的代码示例中,函数B的
this
被设置为函数A的
this
,即
obj
,所以它仍然设置为
obj
,即使以通常将
this
设置为
undefined
或全局对象(或者如前面示例中全局执行上下文中的任何其他方法)进行调用。
填坑
我们回到上面 setTimeout 的坑:
var prop = 0;
var Obj = {
prop: 37,
getProp: function() {
setTimeout(function() {
console.log(this.prop) // 结果是 0 ,不是37!
},1000)
}
};
Obj.getProp();
通常情况我,我们在这里期望输出的结果是
37
,用箭头函数解决这个问题相当简单:
var Obj = {
prop: 37,
getProp: function() {
setTimeout(() => {
console.log(this.prop) // 37
},1000)
}
};
Obj.getProp();
原型链中的 this
相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么
this
指向的是调用这个方法的对象,就好像该方法本来就存在于这个对象上。
var o = {
f : function(){
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
在这个例子中,对象
p
没有属于它自己的f属性,它的f属性继承自它的原型。但是这对于最终在
o
中找到
f
属性的查找过程来说没有关系;查找过程首先从
p.f
的引用开始,所以函数中的
this
指向
p
。也就是说,因为f是作为p的方法调用的,所以它的
this
指向了
p
。这是 JavaScript
的原型继承中的一个有趣的特性。
你也会看到下面这种形式的老代码,道理是一样的:
function Person(name) {
this.name = name;
}
Person.prototype = {
getName:function () {
return this.name
}
};
var p = new Person('愚人码头');
console.log(p.getName()); // "愚人码头"
getter 与 setter 中的 this
再次,相同的概念也适用时的函数作为一个
getter
或者 一个
setter
调用。用作
getter
或
setter
的函数都会把
this
绑定到正在设置或获取属性的对象。
function sum() {
return this.a + this.b + this.c;
}
var o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
}
};
Object.defineProperty(o, 'sum', {
get: sum, enumerable: true, configurable: true});
console.log(o.average, o.sum); // logs 2, 6
作为一个DOM事件处理函数
当函数被用作事件处理函数时,它的
this
指向触发事件的元素(一些浏览器在使用非
addEventListener
的函数动态添加监听函数时不遵守这个约定)。
// 被调用时,将关联的元素变成蓝色
function bluify(e){
console.log(this === e.currentTarget); // 总是 true
// 当 currentTarget 和 target 是同一个对象是为 true
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i < elements.length; i++){
elements[i].addEventListener('click', bluify, false);
}
作为一个内联事件处理函数
当代码被内联on-event 处理函数调用时,它的this指向监听器所在的DOM元素: