根治 JavaScript 中的 this-ECMAScript 规范解读

  前言

  this是JavaScript中的著名月经题,每隔一段时间就有人翻出了拿各种奇怪的问题出来讨论,每次都会引发一堆口水之争。从搜索引擎搜了一下现在的比较热门的关于this的用法,如: Javascript的this用法 、 深入理解JavaScript中的this关键字 、 你不知道的this 等文章几乎都是从现象出发,总结this在不同场景下的指向结果,如同 江湖郎中 一般,都没有从根本上解释现象出现的原因,这就导致每次有了关于this的题层出不穷,因为经验总结只是教会了你现有的场景,而没有教会你如何解释新的场景。

  老司机都知道,发展到今天,有规范在,有源码在,早已经不是IE6时代,还需要总结使用经验场景也太不科学了。最近又在网上刷到关于this的讨论,正巧在规范中追寻过this的秘密,在这里分享一下个人经验。

  * 以下规范均引用ES5

  理论篇

  规范中之处ECMAScript有三种可执行代码:

  全局代码( Global code )

  eval代码( Eval code )

  函数代码( Function code )

  其中,对于全局代码直接指向global object ,eval代码由于已经不推荐使用暂不做讨论,我们主要关注函数代码中的 this 如何指定。

  进入函数代码

  The following steps are performed when control enters the execution context for function code contained in function object F, a caller provided thisArg, and a caller provided argumentsList

  规范指出,当执行流进入函数代码时,由 函数调用者 提供 thisArg 和 argumentsList 。所以我们需要继续寻找,查看函数调用时候this是如何传递进去的。

  函数调用

  The production CallExpression : MemberExpression Arguments is evaluated as follows:

  1. Let ref be the result of evaluating MemberExpression.

  …

  6. If Type(ref) is Reference, then

  a. If IsPropertyReference(ref) is true, then

  i. Let thisValue be GetBase(ref).

  b. Else, the base of ref is an Environment Record

  i. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

  7. Else, Type(ref) is not Reference.

  a. Let thisValue be undefined.

  从上述规范中,在函数调用发生时,step 1首先会对函数名部分进行计算并赋值给 ref 。

  step 6、7中 几个if else就决定了一个函数调用发生时,this会指向何方。

物联网

  在套用if else之前,我们还需要稍微了解一下 Type(ref) is Reference 中的 Reference是个什么东东。

  Reference type

  Reference type按字面翻译就是引用类型,但是它并不是我们常说的JavaScript中的引用类型,它是一个 规范类型 (实际并不存在),也就是说是为了解释规范某些行为而存在的,比如delete、typeof、赋值语句等。规范类型设计用于解析命名绑定的,它由三部分组成:

  base value,指向引用的原值

  referenced name,引用的名称

  strict reference flag,标示是否严格模式

  用大白话讲,就是规范中定义了一种类型叫做Reference用来引用其他变量,它有一个规定的数据结构。由于是规范类型,所以什么情况下会返回Reference规范上也会写得一清二楚。

  至此,我们就可以直接开始实战了 ^ ^

  实战篇

  我们通过上述理论来解释下面代码中的this:

  foo();

  foo.bar();

  (f = foo.bar)();

  如何解释foo()

  1 . 执行函数调用规范中的step1:

  Let ref be the result of evaluating MemberExpression.

  MemberExpression就是括号左边的部分,此处很简单就是 foo。foo我们知道就是一个标示符,那么执行foo的时候会发生什么呢? 答案都在规范中 :

  11.1.2 Identifier Reference

  An Identifier is evaluated by performing Identifier Resolution as specified in 10.3.1. The result of evaluating anIdentifier is always a value of type Reference.

  标示符被执行的时候会进行标示符解析(Identifier Resolution),看10.3.1。(连章节都标好了好伐,只需要点过去就行了。)

  10.3.1 Identifier Resolution

  1.Let env be the running execution context’s LexicalEnvironment.

  …

  3.Return the result of calling GetIdentifierReference function passing env, Identifier, and strict as arguments.

  看这个返回,返回了 GetIdentifierReference 方法的结果,所以我们还需要再走一步(要有耐心 - -)