从 js 函数执行过程看 this

938 阅读4分钟
原文链接: m2mbob.cn

本文是在读完 es5.1 规范前14章后对于this的总结,不过更像是对整个函数调用过程的整理,懒得改了,看之前最好对于规范引用类型、词法环境等名词有一定的了解。

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。下面是完整过程:

函数调用

产生式CallExpression : MemberExpression Arguments如下执行:

  1. 令 ref 为解释执行 MemberExpression 的结果。
  2. 令 func 为 GetValue(ref)。(得到函数的引用)
  3. 令 argList 为解释执行 Arguments 的结果,产生参数值们的内部列表。
  4. 如果 Type(func) is not Object,抛出一个 TypeError 异常。
  5. 如果 IsCallable(func) is false,抛出一个 TypeError 异常。
  6. 如果 Type(ref) 为 Reference,那么如果IsPropertyReference(ref) 为 true,那么令 thisValue 为 GetBase(ref)。否则,ref 的基值是一个环境记录项令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果
  7. 否则,假如 Type(ref) 不是 Reference。令 thisValue 为 undefined。
  8. 返回调用 func 的 [[Call]] 内置方法的结果,传入 thisValue 作为 this 值和列表 argList 作为参数列表。

这是函数调用产生式真正的执行过程,它确定了 this 和 argments,然后传入函数的 [[Call]] 进行调用,[[Call]] 类似于我们平时使用的 Function.prototype.call,我猜测就是Function.prototype.call的内部实现。

然后我们来看看 this 在上面的过程是如何确定的,分两类情况:ref是规范内部引用对象或者不是内部引用对象。

规范内部引用对象,由三部分组成,基(base)值,引用名称(referenced name)和布尔值严格引用 (strict reference) 标志。通过 getValue 方法可以得到基值上的对应引用名称的真实值。

如果是引用对象,并且 base 是一个对象,那么 this 就是这个 base,如果 base 是一个环境记录项,那么 this 就是环境记录的ImplicitThisValue 方法的返回值。

ImplicitThisValue 方法在 with 以外的时候都是 undefined。

如果不是引用对象,那么 this 就是 undefined。

其实到这里 this 已经确定了,但是我还想看看后面发生了什么?

[[call]]

当用一个 this 值,一个参数列表调用函数对象 F 的 [[Call]] 内部方法,采用以下步骤:

  1. 用 F 的 [[FormalParameters]] 内部属性值,参数列表 args, this 值来建立函数代码的一个新执行环境,令 funcCtx 为其结果。
  2. 令 result 为 FunctionBody(也就是 F 的 [[Code]] 内部属性)解释执行的结果。如果 F 没有 [[Code]] 内部属性或其值是空的 FunctionBody,则 result 是 (normal, undefined, empty)。
  3. 退出 funcCtx 执行环境,恢复到之前的执行环境。
  4. 如果 result.type 是 throw 则抛出 result.value。
  5. 如果 result.type 是 return 则返回 result.value。
  6. 否则 result.type 必定是 normal。返回 undefined。

函数执行环境的建立

从 [[call]] 的执行过程可以看到,在真正执行函数代码前,要建立执行环境,也就是网上很多博客所说的预编译,个人认为用预编译来称呼不是很合适,这应该是和执行阶段串联起来的一个过程。

当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入函数代码的执行环境时,执行以下步骤:

  1. 如果函数代码是严格模式下的代码,设 this 绑定为 thisArg。
  2. 否则如果 thisArg 是 null 或 undefined,则设 this 绑定为 全局对象 。
  3. 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定为 ToObject(thisArg)。
  4. 否则设 this 绑定为 thisArg。
  5. 以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
  6. 设词法环境为 localEnv。
  7. 设变量环境为 localEnv。
  8. 令 code 为 F 的 [[Code]] 内部属性的值。
  9. 使用函数代码 code 和 argumentList 执行定义绑定初始化。

看了上面的过程,知道为什么 this 会从 undefined 变成全局变量了吧!也知道为什么严格模式下,是如何做到不变的全局变量的了吧!

初始化绑定

这个过程很多,简单的描述就是如下顺序:

  1. 参数绑定及初始化;
  2. 函数声明绑定及初始化;
  3. 变量声明绑定。

具体过程很复杂,可以通过传送门看。

小结

  1. 函数调用(确定this和arguments) → 调用内部函数 [[call]] → [[call]]在执行代码前创建函数执行上下文。

  2. this在函数调用阶段确定,确定规则见上文。