无人不识又无人不迷糊的 this
关于 this
this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。
为什么要用 this
随着开发者的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。
比如下面的例子:
打印一下结果:
上面的代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify()和 speak(),不用针对每个对象编写不同版本的函数。如果不使用 this,那就需要给 identify()和 speak()显式传入一个上下文对象。
误解
有两种常见的对于 this 的解释,但是它们都是错误的。
1、指向自身
人们很容易把 this 理解成指向函数自身。
那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。
看下面这段代码,思考 foo 会被调用了多少次?
打印结果:
console.log 语句产生了 4 条输出,证明 foo(..)确实被调用了 4 次,但是 foo.count 仍然是 0。显然从字面意思来理解 this 是错误的。
执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象。
2、它的作用域
第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。
this 在任何情况下都不指向函数的词法作用域。
直接打印上面的代码会得到一个报错:
这段代码试图通过 this.bar()来引用 bar()函数。这是不可能实现的,使用 this 不可能在词法作用域中查到什么。
每当开发者想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
this 到底是什么
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性,会在函数执行的过程中用到。
this 全面解析
调用位置
在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
寻找调用位置就是寻找“函数被调用的位置”。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。
通过下面的代码来看什么是调用栈和调用位置:
打印的结果如下:
绑定规则
来看看在函数的执行过程中调用位置如何决定 this 的绑定对象。
首先必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。
充分理解四条规则之后,再理解多条规则都可用时它们的优先级如何排列。
1、默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
打印结果是 2。也就是当调用 foo()时,this.a 被解析成了全局变量 a。函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。
2、隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。
思考下面的代码:
当 foo()被调用时,它的前面确实加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo()时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
3、显式绑定
JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..)和 apply(..)方法。
它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。
因为可以直接指定 this 的绑定对象,因此我们称之为显式绑定。
思考下面的代码:
通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
4、new 绑定
在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。通常的形式是这样的:
在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
优先级
1、四条规则的优先级
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
2、判断 this
可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
(1)函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
(2)函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
(3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
(4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
绑定例外
在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。
被忽略的 this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
那么什么情况下会传入 null 呢?
一种非常常见的做法是使用 apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用。
间接引用
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo()而不是 p.foo()或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。
软绑定
如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
可以看到,软绑定版本的 foo()可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj。
this 词法
ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
箭头函数的词法作用域:
foo()内部创建的箭头函数会捕获调用时 foo()的 this。由于 foo()的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。
总结
我们来总结一下本篇的主要内容:
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
文章转载自:华为云开发者联盟
评论