你不知道的JavaScript - this

有关 JavaScript 流传最广、最持久的不实论点是,关键字 this 指向它所在的函数。这简直错的离谱。

本文是一篇读书笔记,对应《你所不知道的JavaScript(上卷)》—— “this 和对象原型”中的 this 部分。

什么是 this

this 是一个特别的关键字,被自动定义在所有函数的作用域中。它提供了一种优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计的更加简洁并且易于复用。

对于刚接触 JS 的朋友来说,经常会理解为 this 指向函数自身,其实这并不是准确的。

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

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

this 的绑定规则

分析 this 的绑定时,要时刻记住 this 是在调用时被绑定的,其绑定规则完全取决于函数的调用位置。而调用位置实际就是在当前正在执行的函数的前一个调用中。

下面将介绍 this 的四种绑定规则:

1. 默认绑定

this 所属的函数作为独立函数调用时,采用默认绑定规则:在非严格模式下,this 指向全局变量(windowglobal);在严格模式下,this 会绑定到 undefined

1
2
3
4
5
6
7
var a = 2;

function foo() {
console.log(this.a) // this -> 全局对象
};

foo() // [console] 2

2. 隐式绑定

当函数引用有上下文对象时,将采用隐式绑定规则:this 将绑定到这个上下文对象。

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1;

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

var obj = {
a: 2,
foo: foo
};

obj.foo(); // [console] 2

注意,这存在一个隐式丢失的问题:被隐式绑定的 this 会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(this.a);
}

function foFoo(fn) {
fn();
}

var obj = {
a: 2,
foo: foo
};

var a = 'global';

doFoo(obj.foo); // [console] 'global'

类似上述代码这种情况,通过参数传递、赋值等方式,被赋值的变量引用的是原函数本身(上述为 foo),调用时不再带有任何的上下文修饰,因此会应用回默认绑定。

3. 显示绑定

通过使用函数原型的 call(...)apply(...) 方法,可以直接指定 this 的绑定对象,因此我们称之为“显示绑定”。

1
2
3
4
5
6
7
8
9
function foo() {
console.log(this.a);
}

var obj = {
a: 2
};

foo.call(obj); // [console] 2

扩展 —— 硬绑定

硬绑定是一种显式的强制绑定,其典型的应用场景就是创建一个包裹函数,负责接收参数并返回值。

1
2
3
4
5
function bind(fn, obj) {
return function() {
fn.apply(obj, arguments);
}
}

4. new 绑定

在传统的面向对象函数中,“构造函数” 是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。

但是,在 JS 中的并不是这样定义的,JS 中,构造函数只是一些使用 new 操作符时被调用的函数,它们不会属于某个类,也不会实例化一个类。

可以这么说,当一个函数通过 new 操作符调用时,可以将它称之为 “构造函数”;不通过 new 调用时,它就是一个普通函数。在 JS 中,并不存在所谓的 ”构造函数“,只有对与函数的 ”构造调用“。

当使用 new 来调用函数时,会自动执行以下操作:

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

简而言之,通过 new 调用的函数,会将 this 绑定到新创建的对象。

一个问题,在构造函数 prototype 里定义的函数中的 this 指向谁呢?

答案依然是通过 new 创建出来的对象本身。其实,不仅仅是构造函数的 prototype,即使是在整个原型链中,this 代表的也都是指向 new 创建的对象。

判断 this

了解了 this 的四种绑定规则后,我们总结一下 this 的判断顺序(优先级):

  1. 函数是否通过 new 调用,如果是的话 this 绑定的是新创建的对象。(new 绑定)
  2. 函数是否通过 callapply 或者硬绑定调用,如果是的话,this 绑定的是指定的对象。(显示绑定)
  3. 函数是否在某个上下文对象中调用,是的话,this 绑定的是那个上下文对象。(隐式绑定)
  4. 如果都不是的话,采用默认绑定规则。

特殊情况

  1. nullundefined 作为 this 的绑定对象传入 callapply 或者 bind 时,这些值在调用时将被忽视,采用默认绑定规则。(这种经常会出现在不需要关心 this 绑定对象的情况,nullundefined 只是作为 callapply 的第一个参数占用符出现。比如在函数的柯里化中就经常出现)
  2. 函数的间接引用。如上文所述的隐式丢失问题,将函数通过赋值等方式赋给另一个变量,从而该变量获得了函数的间接引用。

箭头函数

ES6 提供了一种特殊的函数类型:箭头函数。在箭头函数内,上述介绍的四种 this 绑定规则均无法适用。在箭头函数中,他仅根据外层(函数或者全局)作用域来决定 this

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
// var self = this
return (a) => {
console.log(this.a);
};
}

var obj1 = {
a: 2
};

foo.call(obj1); // [console] 2

箭头函数内的 this 指向可以理解为继承箭头函数所处的作用域中的 this 的指向。如上例中注释的 var self = this,这是我们之前经常采用的一种写法,而箭头函数中的 this 实际就是替代这里的 self

还需要注意的一点是,箭头函数的 this 无法通过其他方式进行修改(applycallnew 等都不行)。