'This' in JavaScript

Hacker News 看到的一篇博文,觉得还不错,遂翻译过来。原文链接: http://hn.premii.com/#/article/14916731

JavaScript 中的 this 困惑吗?不用担心,因为每个初学者对它都困惑,你并不是一个人。

但并不是永远都不用了解它的用法,你迟早都需要掌握它,到时你就会意识到它比你想的更简单。

通读这篇文章,你会发现它如此简单易懂,会知道它是什么,做什么和怎么用。

this 是什么

this 的值随着一个函数的调用方式的改变而改变。有如下六种不同的方式会对它重新赋值,它们是:

  1. 全局上下文中
  2. 对象构造函数中
  3. 一个对象的方法中
  4. 简单的函数调用中
  5. 箭头函数中
  6. 事件监听器中

你可能很困惑在每个上下文中的 this 到底是什么?为什么需要改变它?下文中列举了六种不同的方式中 this 是怎么被改变的。

全局上下文中的 this

this 在函数体外被调用,即全局上下文中时,默认值是浏览器中的 window 对象。

1
console.log(this); // window

全局上下文中的this

通常你不会在全局上下文中使用 this,因此此处它的值没有什么意义。继续看下一个。

对象构造函数中的 this

当你通过 new 关键字创建一个类的新实例时,this 是这个实例对象的引用。

1
2
3
4
5
6
7
function Human (age) {
this.age = age;
}
let greg = new Human(22);
let thomas = new Human(24);
console.log(greg); // this.age = 22
console.log(thomas); // this.age = 24

构造函数中的this

从上面的代码中可以看到,gregHuman 类的一个实例。现在,无论你何时访问 greg,都不会得到 thomas 的值。因此,this 指向对象实例非常好用。

接下来,继续看下一个 —— 对象方法中的 this

对象方法中的 this

一个对象中的函数被称为方法,例如:

1
2
3
4
let o = {
// A method
aMethod () {}
};

任何方法体中的 this 都指向它自身对象。

1
2
3
4
5
6
let o = {
sayThis () {
console.log(this);
}
};
o.sayThis(); // o

方法体中的this
你可以在方法中得到它对象实例的引用,例如:

1
2
3
4
5
6
7
8
9
10
11
function Human (name) {
return {
name,
getName() {
return this.name;
},
};
}
const zell = new Human('Zell');
const vincy = new Human('Vincy');
console.log(zell.getName()); // Zell

在两个不同的对象上下文中,你可以看到为了让你得到正确的实例对象 this 的改变,这也是面向对象编程的基础。

简单函数中的 this

如下方式的简单函数,同样形式的匿名函数也被看做是简单函数。

1
2
function hello () {// say hello!
}

在浏览器中,一个简单函数中的 this 总是指向全局 window。即使在一个对象方法中调用简单函数也是一样的。

1
2
3
4
5
6
7
8
9
10
function simpleFunction () {
console.log(this);
}
const o = {
sayThis() {
simpleFunction();
},
};
simpleFunction(); // Window
o.sayThis(); // Window

这可能和预想的不一样,看下面的代码。setTimeout 中的 this.speakLeet 函数是被稍晚执行的。

1
2
3
4
5
6
7
8
9
10
const o = {
doSomethingLater() {
setTimeout(function() {
this.speakLeet(); // Error
}, 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
},
};

上面的代码出现了一个错误,原因是 setTimeout 函数中的 this 被设置为 window 对象。而 window 对象并没有 speakLeet 方法。

一种快速解决的方法是存储下 this 的指向。

1
2
3
4
5
6
7
8
9
10
11
const o = {
doSomethingLater() {
const self = this;
setTimeout(function() {
self.speakLeet();
}, 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
};

另一种解决此问题的方式是采用接下来 ES6 中的箭头函数。

箭头函数中的 this

箭头函数中的 this 和包围它的 this 一样(在它的立即调用作用域里)。因此,当你在一个对象方法中采用箭头函数时,this 一直指向对象实例,而不是 window

通过箭头函数,上文中的 speakLeet 例子可以被改写成接下来的方式:

1
2
3
4
5
6
7
8
const o = {
doSomethingLater() {
setTimeout(() => this.speakLeet(), 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
},
};

第三种在函数中改变 this 的值的方式是采用 bindcall 或者 apply

时间监听器中的 this

事件监听器中的 this 是被设置为触发事件的元素的应用。

1
2
3
4
let button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // button
});

当编写更复杂的组件时,可能需要在一些方法中创建事件监听。

1
2
3
4
5
6
7
function LeetSpeaker(elem) {
return {
listenClick() {
elem.addEventListener('click', function () { // Do something here });
},
}
}

因为事件监听器中的 this 指向元素。假如你需要调用对象的其他方法,可能要提供一个指向对象的引用。
Since this refers to the element in the event listener, if you need to activate another method, you need to provide a reference to the object with.

1
2
3
4
5
6
7
8
9
10
11
function LeetSpeaker(elem) {
return {
listenClick() {
const self = this;
elem.addEventListener('click', function () { self.speakLeet() });
},
speakLeet() {
console.log(`1337 15 4W350M3`);
},
};
}

或者采用箭头函数。箭头函数中,你也可以通过 event.currentTarget 拿到元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
function LeetSpeaker(elem) {
return {
listenClick () {
elem.addEventListener('click', (e) => {
console.log(e.currentTarget); // elem
this.speakLeet();
});
},
speakLeet() {
console.log(`1337 15 4W350M3`);
},
};
}

但是上文中的两种方式都不足够好,因为都是匿名函数,不能移除事件监听。

为了移除事件监听,回调函数需要是一个命名函数。

1
2
3
4
5
function someFunction() {
console.log('do something'); // Removes the event listener
document.removeEventListener('click', someFunction);
}
document.addEventListener('click', someFunction);

此时,你需要通过 bind 来手动绑定 this 上下文。

1
2
function LeetSpeaker (elem) { return { listenClick () { this.listener = this.speakLeet.bind(this) elem.addEventListener('click', this.listener) }, speakLeet(e) { const elem = e.currentTarget console.log(`1337 15 4W350M3`) elem.removeEventListener('click', this.listener) } }
}

(注:原文中 bind 部分没译。请自行查阅。)

总结

JavaScript 中的 this 是一个有争议的关键字。它出现在很多的框架里面,因此你需要知道它做了什么。

文章中介绍了六种不同的上下文中 this 的不同值。这些都是你需要了解的 this, 完全掌握这些概念,你就再也不会困惑了。