关于 javascript 中 this 用法的笔记,来自 You Don’t Know JS: this & Object Prototypes 的读书笔记。内容包括 this 绑定规则的总结,以及遇到具体问题时,判断 this 指向的方法。

首先了解下 this 的绑定规则,分别是 默认绑定显式绑定隐式绑定new 绑定,说是绑定规则,我的理解是四种应用场景,不同的应用场景对应着不同的指向。

指向规则

默认绑定

默认绑定的方式很常见,举个例子

1
2
3
4
5
function fun() {
console.log(this.a);
}
var a = 1;
fun(); // 1

在默认绑定规则下,this 被绑定到了全局对象,所以例子中输出的 this.a 的值即全局变量 a 的值。

需要注意的是,绑定到全局对象只存在于非严格模式下,当使用严格模式时,this 会被绑定到 undefined。参见下面的案例。

1
2
3
4
5
6
"use strict";
function fun() {
console.log( this.a );
}
var a = 2;
fun(); // this is undefined

隐式绑定

相比于默认绑定只有两个选项,隐式绑定就含蓄的多,我们需要分析上下文来弄清 this 的指向。

1
2
3
4
5
6
7
8
function fun() {
console.log(this.a);
}
var obj = {
a: 2,
foo: fun
};
obj.foo(); // 2

foo 作为引用属性被调用,虽然 foo 不属于 obj 对象,但在被调用时,foo 落脚于 obj 对象,隐式绑定将 this 绑定到此时的上下文对象,所以 此时的 this.aobj.a 是一样的。

这一个例子还不能完全将隐式绑定解释清晰,为此,我们需要更多的例子,下面我们来讨论下引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function fun() {
console.log(this.a);
}
var obj1 = {
a: 1,
foo: fun
};
var obj2 = {
a: 2,
obj: obj1
};
obj1.foo(); //1
obj2.obj.foo(); //1

如果遇到这样的引用链,稍加分析不难理解,对象属性引用链中只有最顶层或者说最后一层会影响 this 的上下文,换而言之,在分析引用链中的 this 指向,只需要分析最后一层即可。

1
2
3
4
5
6
7
8
9
10
function fun() {
console.log(this.a);
}
var obj = {
a: 1,
foo: fun
};
var bar = obj.foo;
var a = 'global';
bar(); //global

在这段代码中,bar 引用 obj.fooobj.foo 引用 fun,归根结底,bar 引用的还是 fun,最后在调用时,和 obj 并没有关系,这只是开始,下面继续。

1
2
3
4
5
6
7
8
9
10
11
12
function fun() {
console.log(this.a);
}
function doDoo(fn) {
fn();
}
var obj = {
a: 1,
foo: fun
};
var a = 'global';
doFoo(obj.foo); // global

这段代码中的参数传递其实就是一种隐式传值,看到这里有没有觉得似曾相识,跟上一段代码异曲同工,表面上调用的是 obj.foo 其实与 obj 并无联系。doFoo() 中执行的 foo() 调用的是 fun()。趁热打铁,再来一个例子。

1
2
3
4
5
6
7
8
9
function fun() {
console.log(this.a);
}
var obj = {
a: 1,
foo: fun
};
var a = 'global';
setTimeout(obj.foo, 1000); // global

这段代码和上一段并无二致,只不过这种情形在开发中经常会遇到,this 丢失问题,有没有好办法,肯定有,往下看。

显式绑定

显式绑定通过 call(…)apply(…) 方法强制绑定 this

1
2
3
4
5
6
7
8
9
10
11
12
function fun() {
console.log(this.a);
}
var obj1 = {
a: 1,
foo: fun
};
var obj2 = {
a: 2,
};
obj1.foo(); //1
obj1.foo.call(obj2); //2

call(…) 方法将本来指向 obj1 上下文对象的 this 绑定到了 obj2,光明正大的挖墙脚……

为了解决之前的 this 丢失问题,在显式绑定的基础上升级出了硬绑定,此招一出,墙脚硬到磕坏牙,再也不用担心 this 会丢失了。

1
2
3
4
5
6
7
8
9
10
11
12
13
function fun() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
fun.call(obj);
};
var a = 'global';
bar(); // 2
setTime(bar, 1000); //2
bar.call(window); // 2

看到没,稳如泰山。硬绑定在 ES5 之后有了现成可用的方法 Function.prototype.bind,其中一种应用场景如下:

1
2
3
4
5
6
7
8
9
10
function fun(something) { 
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = fun.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

new 绑定

考虑下面的代码

1
2
3
4
5
function fun(a) {
this.a = a;
}
var bar = new fun(2);
console.log(bar.a); //2

使用 new 来调用 fun(..) 时,会构造一个新对象并把它绑定到 fun(..) 调用中的 this
上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

New 绑定也是四种绑定中优先级最高的一种,连硬绑定也能掰弯

1
2
3
4
5
6
7
8
9
10
function fun(a) {
this.a = a;
}
var obj = {};
var bar = fun.bind(obj);
bar(2);
console.log(obj.a); // 2
var baz = new bar(3);
console.log(obj.a); // 2
console.log(baz.a); // 3

在 new 对象时,预期 obj.a 值应该赋为 3,结果并没有修改,而是 new 出来一个新的对象,说明在 new 的过程中,this 的指向改变了。

判断方法

常规

四种规则优先级先后顺序为: new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

  1. 函数是否在new中调用 ? 如果是的话this绑定的是新创建的对象。

    var bar = new fun();

  2. 函数是否通过call、apply或者硬绑定调用 ? 如果是的话,this绑定的是 指定的对象。
    var bar = fun.call(obj);

  3. 函数是否在某个上下文对象中调用 ? 如果是的话,this 绑定的是那个上 下文对象。
    var bar = obj.foo();

  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
    var bar = fun();