Fork me on GitHub

this指向

图片

关于this

当一个函数被调用时,会创建一个活动记录(有时候也成为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性。

this全面解析

调用位置

首先清楚一个概念
调用位置:调用位置就是函数在代码中被调用的位置(而不是声明位置)。
例如:

1
2
3
4
function foo(){ // 声明位置
console.log("foo");
}
foo(); // 调用位置

某些编程模式会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了达到当前执行位置所调用的所有函数。也称为环境栈)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function baz(){
// 当前调用栈是:baz
// 因此当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar(){
// 当前调用栈是:baz -> bar
// 因此当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo(){
// 当前调用栈是 baz -> bar -> foo
// 因此当前调用位置在 bar 中
console.log( "foo" )
}
baz(); // <-- baz 的调用位置

首先,最后一句调用了 baz() ,所以那个位置就是baz() 的调用位置,将baz()添加到调用栈里。然后 baz()里面又调用了 bar(),所以 bar()的调用位置是在这,同样将bar()加入到调用栈里面。接着,bar()里面又调用了 foo(),所以 foo()的调用位置是在bar()里面,将 foo()加入到调用栈中。

明确调用位置对我们分析 this 的指向有很大的帮助。

绑定规则

默认绑定

默认绑定即独立的函数调用,当其他规则无法应用时的默认规则,如:

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

调用 foo()的时候其实相当于 window.foo(),所以 this.a 其实指向的是 window.a

隐式绑定

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

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文的对象。因为调用 foo()this 被绑定到obj,因此 this.aobj.a是一样的

显示绑定

即使用 apply()call()方法。它们的第一个参数是一个对象,在调用函数时将其绑定到 this。他们的主要区别就是第二个参数。
看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log( this.a );
}
var obj = {
a: 2,
};
var bar = function(){
foo.call( obj );
};
bar(); // 2
setTimeout(bar,100) // 2
// 显示绑定的 bar 不可能再修改它的 this
bar.call(window); // 2

我们创建了函数 bar(),并在内部调用了 foo.call(obj),因此强制把 foothis 绑定到了 obj。之后无论如何调用 bar(),它总会手动在 obj 上调用 foo。这种绑定是一种强制绑定,也成为硬绑定。

由于硬绑定是一种非常常用的模式,所以 ES5 提供了 bind() 函数。用法如下

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

new绑定

使用 new 调用函数只是对函数的 ” 构造调用 “,所有的函数都可以使用 new 来调用。
使用 new 来调用函数时,会自动执行如下操作

  • 创建(或者说构造)一个全新的对象
  • 这个新对象会被执行 [[Prototype]] 连接
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象

用代码表示就是如下步骤

1
2
3
4
5
6
7
8
9
10
11
12
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
// 其中 new foo(2) 进行的是类似如下的操作
{
var obj = new Object();
obj.__proto__ = foo.prototype;
var result = foo.call(obj,"2");
return result === 'object' ? result : obj
}

使用 new 来调用 foo( … ) 时,我们会构造一个新对象并把它绑定到 foo( … ) 调用中的 this

优先级

毫无疑问,默认绑定优先级最低
接下来,我们看看其他绑定的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(a){
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
// 显然,显式绑定优先级更高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(something){
this.a = something;
}
var obj1 = {
foo:foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
// 可以看到,new 绑定比隐式绑定优先级高
1
2
3
4
5
6
7
8
9
10
function foo(something){
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

判断this

  • 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象
    var bar = new foo()
  • 函数是否通过 call、apply(显式绑定)或者硬绑定调用(bind)?如果是的话,this 绑定的是指定的对象 var bar = foo.call()
  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象var bar = obj1.foo()
  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。var bar = foo()

箭头函数

箭头函数不适用 this 的四种标准规则,而是根据外层作用域来决定
来看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(){
// 返回一个箭头函数
return (a)=>{
// this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );// 调用位置
bar.call( obj2 ); // 2,不是 3!

foo() 内部创建的箭头函数会捕获调用foo()时的this。由于 foo()的 this 绑定到 obj1barthis 也会绑定到 obj1,箭头函数的绑定无法修改。(new 绑定也不行!)

ES6 标准入门里面对箭头函数 this 的指向有如下说法:

  • 函数体内的 this 对象就是定义时所在的对象,而不是调用时所在的对象。

总结

  • 箭头函数的 this 绑定看的是 this 所在的函数定义在哪个对象下,绑定到哪个对象则 this 就指向哪个对象
  • 一般情况下是默认绑定,如果有 new 绑定则 new 绑定优先级最高,其次是显式绑定,然后再是隐式绑定。如果有对象嵌套的情况,则 this 绑定到最近的一层对象上