JavaScript 中的 this

前面一篇文章介绍了 JavaScript 执行上下文中两个重要的属性:VO/AO 和 scope chain,本文来看一下 this 属性。

  • this 是执行上下文中的一个重要属性,是一个与执行上下文相关的特殊对象,它也可以叫作上下文对象(context)。
  • 函数的每次调用都有一个与之密切相关的上下文,这个上下文是拥有或者说控制当前执行代码的对象,这个对象的引用绑定到了 this 关键字上。
  • 也可以这样理解,正在使用的那个对象就是 this 关键字绑定的对象。

在函数被调用之前 this 的指向是不知道的,只有在函数被调用的时候,this 才会进行绑定。具体的绑定规则如下:

1. 默认绑定

当函数前面不加任何修饰直接进行调用时,比如 a(),this 会采用默认绑定规则,指向全局对象。

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

上面代码尽管在 foo 函数体内也声明并初始化了一个变量 a,但是这里的 this 指向的是全局对象

function foo() {
"use strict";
var a = 1;
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined

但是在严格模式下,this 不能绑定到全局对象上,会被绑定到 undefined。这是因为这种绑定规则被认为是 JavaScript 语言的一个错误设计,没有实际用途,还会造成一些误解,比如下面:

var a = "global object";
var Foo = {
a: "Foo object",
method: function () {
function test() {
console.log(this.a);
}
test();
}
}
Foo.method(); // "global object"

一个常见的误解是 test 中的 this 会指向 Foo 对象,但实际上这里采用了默认绑定的规则,将 this 绑定到了全局对象上。
为了在 test 中获取对 Foo 对象的引用,我们需要在 method 函数内创建一个局部变量指向 Foo 对象:

var a = "global object";
var Foo = {
a: "Foo object",
method: function () {
var that = this;
function test() {
console.log(that.a);
}
test();
}
}
Foo.method(); // "Foo object"

2. 隐式绑定

当一个函数被作为对象的方法调用时,this 指向调用函数的对象。

var a = "global object";
var Foo = {
a: "Foo object",
method: function () {
console.log(this.a);
}
}
Foo.method(); // "Foo object"

这里有一个常见的隐式丢失的问题:

var a = "global object";
var Foo = {
a: "Foo object",
method: function aName() {
console.log(this.a);
}
}
var test = Foo.method;
Foo.method(); // "Foo object"
test(); // "global object"

test 虽然是 Foo.method 的一个引用,但是实际上,它引用的是 aName 函数本身,调用 test 实际上是调用 aName 函数,采用默认绑定。
传参实际上是一个隐式的赋值操作,和上面的结果是一样的。

3. 显示绑定

Function.prototype 上的 call 和 apply 方法(任何函数都有),可以用来间接地调用函数。两个方法都允许显示地指定调用时所需的 this 值(调用对象)。
也就是说,任何函数都可以被任何对象进行调用,不论这个函数是不是对象内的方法。

var a = 1;
function foo() {
console.log(this.a);
}
var obj = {
a:2
}
foo.call(obj); // 2

3.1 数组方法中的 this 绑定

var obj = {
id: "awesome"
};
[1,2,3].forEach(function (ele) {
console.log(el, this.id)
}, obj);
// 1 "awesome"
// 2 "awesome"
// 3 "awesome"

数组几乎所有方法都接受这样的第二个参数用于指定第一个函数参数中的 this 值

4. new 绑定

用 new 来调用函数时,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 将新对象绑定到 this 上
  3. 执行代码
  4. 如果函数没有返回其他对象,那么会自动返回这个新对象

5. 判断 this 的绑定

  1. 函数是否通过 new 调用,如果是的话,this 绑定到新创建的对象
  2. 函数是否通过显式绑定调用,如果是的话,this 绑定到指定的对象上
  3. 函数是否通过隐式绑定调用,如果是的话,this 绑定到调用函数的对象上
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。

6. 忽略 this 指向,保护全局对象

显示绑定中指定对象为 null 或者 undefined 时,函数中的 this 指向全局对象。
之所以要指定对象为 null 或 undefined,是因为有展开数组,或进行柯里化(将需要多个参数的函数变成需要一个参数的函数)的这种需要,这时不需要 this

function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
// 把数组展开成参数
foo.apply(null, [2, 3]); // "a:2, b:3"
// 使用 bind 进行柯里化
var bar = foo.bind(null, 2);
bar(3);// "a:2, b:3"

ES6 中 foo(...[1,2])foo(1,2) 是一样的,... 代替了 apply(..) 来展开数组。但是没有柯里化的新语法。
我们可以创建一个空对象,将函数中的 this 指向这个空对象:

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

7. ES6 中箭头函数的 this 指向

箭头函数并不会根据上面介绍的四条绑定规则决定 this 的指向,而是根据当前的词法作用域来决定,具体来说,箭头函数会继承外层函数调用的 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 !

本文参考:
【1】你不知道的 JavaScript(上)
【2】JavaScript中的this