JavaScript中的this全解析

针对this的使用已经看过好多文章,但每次遇到this还是要想半天,今天尝试总结下我们可能遇到的所有情况,不一定完善,有问题请指出。

what’s this?

谈及 Javascript 中的 this,竟然让人觉得头疼,它不像 Java,C++ 中的 this 指向调用 this 的对象。

首先,this 是 JavaScript中的一个keyword(关键词),以下是MDN的解释。

A function’s this keyword behaves a little differently in JavaScript compared to other languages. It also has some differences between strict mode and non-strict mode.

与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。

In most cases, the value of this is determined by how a function is called. It can’t be set by assignment during execution, and it may be different each time the function is called. ES5 introduced the bind method to set the value of a function’s this regardless of how it’s called, and ES2015 introduced arrow functions which don’t provide their own this binding (it retains the this value of the enclosing lexical context).

在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。ES5引入了bind方法来设置函数的this值,而不用考虑函数如何被调用的,ES2015 引入了支持this词法解析的箭头函数(它在闭合的执行上下文内设置this的值)。

理解上面这段话,我们来看两个例子,第一个例子中,同一个函数由于调用方式的不同,this指向了不一样的对象:

1
2
3
4
5
6
7
8
9
10
11
var a = 10;
var obj = {
a: 20
}

function fn () {
console.log(this.a);
}

fn(); // 10
fn.call(obj); // 20

除此之外,this 为保留字,你不能重写 this,也就是this一旦被确定,执行期间就不可更改了:

1
2
3
4
5
6
7
8
9
10
11
var a = 10;
var obj = {
a: 20
}

function fn () {
this = obj; // 这句话试图修改this,运行后会报错
console.log(this.a);
}

fn();

接下里我们要牢记:在函数中 this 到底取何值,是在函数真正被调用执行的时候确定下来的,函数定义的时候确定不了。 也就是执行上下文被创建时确定的。因此,一个函数中的this指向,可以是非常灵活的,接下来来看我们可能会遇到的情况。

全局中的this

在全局环境中,this 永远指向全局对象(宿主环境为web时即 window,在nodejs中为 global),不需过多考虑。

1
console.log(this === window);     //true

普通函数中的this

首先我们复习一下刚才的话,在一个函数上下文中,this由调用者提供,由调用函数的方式来决定

其次我们直接抛出结论:如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined(但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象,也就是window),从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用就变得十分关键。

1
2
3
4
5
6
7
8
// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
'use strict';
console.log(this);
}

fn(); // fn是调用者,独立调用
window.fn(); // fn是调用者,被window所拥有

在上面的简单例子中,fn()作为独立调用者,按照定义的理解,它内部的 this 指向就为 undefined。而window.fn()则因为fn被window所拥有,内部的this就指向了window对象。

掌握了这个规则大部分问题都可以解决。

构造函数中的this

所谓的构造函数就是一种特殊的方法,主要用来在创建对象时初始化对象,一般构造函数的函数名首字母大写,例如像 Object,Function,Array 这些都属于构造函数。

1
2
3
4
5
6
7
8
function Person(name, age) {
// 这里的this指向了谁?
this.name = name;
this.age = age;
console.log(this);
}

var p1 = new Person('Nick', 20);

我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。

通过new操作符调用构造函数,会经历以下4个阶段:

  • 创建一个新的对象;
  • 将构造函数的this指向这个新对象;
  • 指向构造函数的代码,为这个对象添加属性,方法等;
  • 返回新对象

因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。

原型上函数的this

原型上函数的this,其实没有什么特殊之处。在下面的例子中,借用我们上面的规则,foo.getX()中getX为调用者,他被foo所有,因此getX中的 this 指向了foo对象。

1
2
3
4
5
6
7
8
9
function Foo(){
this.x = 10;
}
Foo.prototype.getX = function () {
console.log(this); //Foo {x: 10, getX: function}
console.log(this.x); //10
}
var foo = new Foo();
foo.getX();

箭头函数中的this

箭头函数本身没有this,需要通过查找作用域链来确定 this 的值

这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
x: 10,
foo: function() {
var fn = () => {
return () => {
return () => {
console.log(this); // Object {x: 10}
console.log(this.x); //10
}
}
}
fn()()();
fn.bind({x: 14})()()();
fn.call({x: 14})()();
}
}
obj.foo();

所以,用 call() 或者 apply() 调用箭头函数时,无法对 this 进行绑定,即传入的第一个参数被忽略。

如果使用箭头函数,以前的这种 hack 写法:var self = this ,就不再需要了。

使用call、apply或bind显示指定this

1
2
3
4
5
6
7
8
9
10
var obj = {
x: 10
}
function foo(){
console.log(this); //{x: 10}
console.log(this.x); //10
}
foo.call(obj);
foo.apply(obj);
foo.bind(obj)();

JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。

call()、apply()可以改变函数运行时的执行环境,foo.call()、foo.apply()这样的语句可以看作执行foo(),只不过foo()中的this指向了后面的第一个参数。

foo.bind({a:1})却并不如此,执行该条语句仅仅得到了一个新的函数,新函数的this被绑定到了后面的第一个参数,亦即新的函数并没有执行。