前言
今天在阅读《你不知道的JavaScript》时候,其中关于this的讲解感觉很不错,在这里记录一下this相关知识。
关于this
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调
用时的各种条件。 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包
含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。 this 就是记录的
其中一个属性,会在函数执行的过程中用到
调用位置
通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,
因为某些编程模式可能会隐藏真正的调用位置。
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的
调用位置就在当前正在执行的函数的前一个调用中。
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 的调用位置
绑定规则
-
默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用
其他规则时的默认规则。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
如果使用严格模式( strict mode ),那么全局对象将无法使用默认绑定,因此 this 会绑定
到 undefined :
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
-
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包
含,不过这种说法可能会造成一些误导。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引
用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调
用 foo() 时 this 被绑定到 obj ,因此 this.a 和 obj.a 是一样的
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默
认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
-
显式绑定
JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过 foo.call(..) ,我们可以在调用 foo 时强制把它的 this 绑定到 obj 上
-
new 绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this 。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a )
优先级
隐式绑定和显式绑定哪个优先级更高?我们来测试一下:
function foo() {
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
可以看到,显式绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显式绑定
现在我们需要搞清楚 new 绑定和隐式绑定的优先级谁高谁低:
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 绑定比隐式绑定优先级高。但是 new 绑定和显式绑定谁的优先级更高呢?
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
出乎意料! bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a
修改为 3。相反, new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this 。因为使用了
new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
-
判断 this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:
- 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo() - 函数是否通过 call 、 apply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是
指定的对象。
var bar = foo.call(obj2) - 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上
下文对象。
var bar = obj1.foo() - 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到
全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。
ES6箭头函数
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定
this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这
其实和 ES6 之前代码中的 self = this 机制一样。
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 绑定到 obj1 ,
bar (引用箭头函数)的 this 也会绑定到 obj1 ,箭头函数的绑定无法被修改。( new 也不
行!)
小结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后
就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑
定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null) ,以保护全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定
this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这
其实和 ES6 之前代码中的 self = this 机制一样。