你不知道的JavaScript - this
有关 JavaScript 流传最广、最持久的不实论点是,关键字
this指向它所在的函数。这简直错的离谱。
本文是一篇读书笔记,对应《你所不知道的JavaScript(上卷)》—— “this 和对象原型”中的 this 部分。
什么是 this
this 是一个特别的关键字,被自动定义在所有函数的作用域中。它提供了一种优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计的更加简洁并且易于复用。
对于刚接触 JS 的朋友来说,经常会理解为 this 指向函数自身,其实这并不是准确的。
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(执行上下文)。这个纪录会包含函数在哪里被调用、函数的调用方式、传入的参数等信息。this 就是这个纪录的一个属性,会在函数执行的过程中用到。
this 的绑定规则
分析 this 的绑定时,要时刻记住 this 是在调用时被绑定的,其绑定规则完全取决于函数的调用位置。而调用位置实际就是在当前正在执行的函数的前一个调用中。
下面将介绍 this 的四种绑定规则:
1. 默认绑定
当 this 所属的函数作为独立函数调用时,采用默认绑定规则:在非严格模式下,this 指向全局变量(window、global);在严格模式下,this 会绑定到 undefined。
1 | var a = 2; |
2. 隐式绑定
当函数引用有上下文对象时,将采用隐式绑定规则:this 将绑定到这个上下文对象。
1 | var a = 1; |
注意,这存在一个隐式丢失的问题:被隐式绑定的 this 会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined。
1 | function foo() { |
类似上述代码这种情况,通过参数传递、赋值等方式,被赋值的变量引用的是原函数本身(上述为 foo),调用时不再带有任何的上下文修饰,因此会应用回默认绑定。
3. 显示绑定
通过使用函数原型的 call(...)、apply(...) 方法,可以直接指定 this 的绑定对象,因此我们称之为“显示绑定”。
1 | function foo() { |
扩展 —— 硬绑定
硬绑定是一种显式的强制绑定,其典型的应用场景就是创建一个包裹函数,负责接收参数并返回值。
1 | function bind(fn, obj) { |
4. new 绑定
在传统的面向对象函数中,“构造函数” 是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。
但是,在 JS 中的并不是这样定义的,JS 中,构造函数只是一些使用 new 操作符时被调用的函数,它们不会属于某个类,也不会实例化一个类。
可以这么说,当一个函数通过 new 操作符调用时,可以将它称之为 “构造函数”;不通过 new 调用时,它就是一个普通函数。在 JS 中,并不存在所谓的 ”构造函数“,只有对与函数的 ”构造调用“。
当使用 new 来调用函数时,会自动执行以下操作:
- 创建(构造)一个全新的对象
- 这个新对象会被执行 [[Prototype]] 连接
- 这个新对象会绑定到函数调用的
this - 如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象。
简而言之,通过 new 调用的函数,会将 this 绑定到新创建的对象。
一个问题,在构造函数 prototype 里定义的函数中的 this 指向谁呢?
答案依然是通过 new 创建出来的对象本身。其实,不仅仅是构造函数的 prototype,即使是在整个原型链中,this 代表的也都是指向 new 创建的对象。
判断 this
了解了 this 的四种绑定规则后,我们总结一下 this 的判断顺序(优先级):
- 函数是否通过
new调用,如果是的话this绑定的是新创建的对象。(new 绑定) - 函数是否通过
call、apply或者硬绑定调用,如果是的话,this绑定的是指定的对象。(显示绑定) - 函数是否在某个上下文对象中调用,是的话,
this绑定的是那个上下文对象。(隐式绑定) - 如果都不是的话,采用默认绑定规则。
特殊情况
- 将
null、undefined作为this的绑定对象传入call、apply或者bind时,这些值在调用时将被忽视,采用默认绑定规则。(这种经常会出现在不需要关心this绑定对象的情况,null、undefined只是作为call、apply的第一个参数占用符出现。比如在函数的柯里化中就经常出现) - 函数的间接引用。如上文所述的隐式丢失问题,将函数通过赋值等方式赋给另一个变量,从而该变量获得了函数的间接引用。
箭头函数
ES6 提供了一种特殊的函数类型:箭头函数。在箭头函数内,上述介绍的四种 this 绑定规则均无法适用。在箭头函数中,他仅根据外层(函数或者全局)作用域来决定 this。
1 | function foo() { |
箭头函数内的 this 指向可以理解为继承箭头函数所处的作用域中的 this 的指向。如上例中注释的 var self = this,这是我们之前经常采用的一种写法,而箭头函数中的 this 实际就是替代这里的 self。
还需要注意的一点是,箭头函数的 this 无法通过其他方式进行修改(apply、call、new 等都不行)。