JavaScript中的this

指向调用者

一般情况下,this指代的对象都是调用的对象,即谁调用,则this指向谁;我们来看如下示例。

作为函数调用

JavaScript中,作为函数调用的意思就是,调用对象是全局对象。

1
2
3
4
5
6
// 浏览器环境下
function foo() {
return this;
}
foo() === window;
// true
1
2
3
4
5
6
// node环境下
function foo(){
return this;
}
foo() === global;
// true

在上面的代码中,由于是全局对象调用的foo函数,因此this就指向了全局对象。在浏览器环境中,全局对象即window对象,而在node环境下,全局对象是global对象,因此这里需要注意。

也因此,如setTimeout和setInterval这种本身就属于浏览器全局对象的函数,不管在哪个对象中调用,this都是指向window(除非使用call,apply和bind,后面会讲到)。我们可以理解为实际调用是前面加了window对象。

1
2
// setTimeout(foo, time) 等同于 window.setTimeout(foo, time)
// setInterval(bar, time) 等同于 window.setInterval(bar, time)

指向自定义的当前对象

1
2
3
4
5
6
7
let foo = {
bar() {
return this;
}
}
foo.bar() === foo;
// true

当一个方法具体从属于一个对象的时候,this就变成了该对象,如上述例子所示。我们再来看一个这种情况的一个经典应用例子。

1
2
3
4
5
6
7
8
9
function User(name) {
this.name = name;
}
User.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
}
const u = new User('Chap');
u.sayHello();
// Hello, my name is Chao

在之前的文章我们讲过JavaScript中的OOP的实现是基于原型的,当我们需要为一个类添加成员方法的时候就需要在该构造器函数的prototype对象中添加方法,而且可以在其中直接使用this关键字代表当前调用对象。实际上由于JavaScript的原型链逐级往上寻找属性,我们最后调用的成员方法都是在上级(或者更上级)的prototype中定义的,所有对象共享同一套方法,也正是因为this指代当前对象,才能使这种面向对象编程的设计能够实现。

嵌套函数内的this

如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象不是全局对象(非严格模式下)就是undefined(严格模式下)。很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。(参考2

1
2
3
4
5
6
7
8
9
10
11
'use strict';
function foo() {
console.log(this);
function bar() {
console.log(this);
};
bar();
}
foo();
// undefined
// undefined

严格模式和非严格模式

严格模式下如果不手动绑定this(使用call(),apply(),或者bind()),则this的值一定是undefined。

1
2
3
4
(function() {
'use strict';
console.log(this); // undefined
}())

非严格模式下非嵌套函数,且不手动绑定this,则this的值为全局对象。

1
2
3
(function() {
console.log(this); // window or global
})

ES6中的箭头函数

之前写ES6中的箭头函数时,有讲过箭头函数需要特别注意this的作用域。

1
2
3
4
5
6
7
function foo() {
const _this = this;
const bar = () => console.log(_this === this);
bar();
}
foo();
// true

上面的示例代码中,箭头函数bar内的this和箭头函数bar外的this指向的是同一个对象,都是foo的当前调用对象(具体到上面的例子中的this,仔细分析一下的话应该都是全局对象)。

call,apply和bind

由于this指向的对象总是不确定的,所以有时候我们需要为一个函数绑定一个this,这个时候我们就需要考虑使用call,apply和bind了。

1
2
3
4
5
6
7
8
9
10
const o = {
v: 1,
foo(){
setTimeout(function() {
console.log(this.v);
}, 1000);
},
}
o.foo();
// undefined
1
2
3
4
5
6
7
8
9
10
const o = {
v: 1,
foo(){
setTimeout(function() {
console.log(this.v);
}.bind(o), 1000);
},
}
o.foo();
// 1

上例中,我们为传入setTimeout的第一个参数(函数)绑定了对象o,作为该函数的this,也就是说该函数的this永远指向o,而不是默认的当前对象。

apply和call与bind作用相似,不过bind是永久性的,调用以后返回的就是this经过指定后的原函数,而apply和call是立刻执行。

1
2
3
4
5
6
7
8
9
// 默认情况下
const o = {
v: 1,
}
function foo() {
console.log(this.v);
}
foo();
// undefined
1
2
3
4
5
6
7
8
9
// 使用call的情况
const o = {
v: 1,
}
function foo(a) {
console.log(a + this.v);
}
foo.call(o, 2);
// 3
1
2
3
4
5
6
7
8
9
// 使用apply的情况
const o = {
v: 1,
}
function foo(a) {
console.log(a + this.v);
}
foo.apply(o, [2]);
// 3

由上我们可以看出,apply和call这两个函数与bind的区别在于bind绑定永久,而apply和call立刻执行。此外,使用apply调用函数的时候参数放在第二个参数的位置,作为数组传入;使用call调用函数的时候参数放在第二个参数以及之后的位置传入。

构造器函数中的this(使用new关键字)

JavaScript中如果使用new关键字,一个函数还可以作为构造器(constructor)构建一个新的对象。

1
2
3
4
5
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = new Person('Charles', 25);

使用new关键字创建对象时,构造器中的this都会被指向当前新创建的对象(注意构造器不需要返回任何显式创建的对象)。新对象的__proto__属性将会指向Person.prototype属性。

DOM事件处理函数

在DOM事件处理中,this指向当前事件的发生对象。

1
2
3
4
5
const btn = document.getElementsByTagName('button')[0];
btn.addEventListener('click', function(e){
console.log(this === e.target);
});
// true

上例中,点击页面的第一个按钮将在控制台输出true

参考

  1. this - MDN
  2. JavaScript权威指南(原书第6版) - Amazon