Published on

函数对象的实例化

在 ECMAScript 规范中,函数是头等公民,其本质是一种支持内部 [[Call]] 方法的特殊对象。一个函数从源代码文本到成为可执行的函数对象,其创建过程并非单一操作,而是遵循一套由规范定义的、严谨的实例化算法 (instantiation algorithm)。深入理解这一过程,能够从根本上揭示 JavaScript 中闭包 (Closure)、this 绑定、new 关键字以及 prototype 机制的底层原理。

核心抽象操作

函数实例化的过程依赖于两个核心的、由规范定义的抽象操作。

OrdinaryFunctionCreate

OrdinaryFunctionCreate(Function.prototype, sourceText, ParameterList, Body, thisMode, env, privateEnv) 负责创建函数对象的基础结构。它会:

  1. 创建一个继承自 Function.prototype 的新对象 F
  2. F 填充一系列内部插槽 (internal slots),这些插槽是引擎内部使用的数据,无法直接访问。其中最重要的包括:
    • [[Environment]]: 捕获函数创建时所处的词法环境 (Lexical Environment) env,这是闭包的实现基础。
    • [[PrivateEnvironment]]: 记录私有成员的词法环境 privateEnv
    • [[ECMAScriptCode]]: 存储函数的参数列表 ParameterList 和函数体 Body
    • [[ThisMode]]: 根据 thisModeBody 是否为严格模式,设定 this 的绑定模式(lexical, strict, 或 global)。
    • [[Call]]: 定义该函数对象被调用时的行为。
MakeConstructor

MakeConstructor(F, writablePrototype?, prototype?) 负责为一个已创建的函数对象 F 赋予作为构造函数的能力

  1. F 添加 [[Construct]] 内部方法,定义其通过 new 关键字被调用时的行为。
  2. F 创建一个 prototype 属性。这个属性的值是一个普通对象,该对象上会自动包含一个 constructor 属性,指回函数 F 自身。

普通函数实例化 (InstantiateOrdinaryFunction)

一个常规的 function 声明或表达式,其实例化流程如下。

算法流程

  1. 捕获环境: 首先,捕获当前执行上下文的词法环境 env 和私有环境 privateEnv
  2. 创建函数对象: 调用 OrdinaryFunctionCreate,并传入 %Function.prototype% 作为原型,以及函数的源码、参数、函数体、this 模式(non-lexical-this)和第一步捕获的环境,生成一个基础函数对象 F
  3. 设置名称: 调用 SetFunctionName,根据函数的声明标识符(或推断的名称)为 F 设置 .name 属性。
  4. 赋予构造能力: 调用 MakeConstructor(F),为 F 添加 [[Construct]] 内部方法,并为其创建一个 .prototype 对象。
  5. 返回: 最终返回这个功能完备的函数对象 F
MakeConstructor 的关键步骤

MakeConstructor 在赋予函数构造能力时,执行了我们熟知的原型创建过程:

// 伪代码,描述 MakeConstructor 的核心行为

function MakeConstructor(F) {
  // a. 为 F 添加 [[Construct]] 内部方法,使其支持 new 调用
  F.[[Construct]] = internalConstructorLogic;

  // b. 创建一个全新的普通对象作为 F 的 prototype
  let prototype = Object.create(Object.prototype);

  // c. 在这个新对象上,添加一个 constructor 属性,指回 F 自身
  Object.defineProperty(prototype, 'constructor', {
    value: F,
    writable: true,
    enumerable: false,
    configurable: true
  });

  // d. 将这个新对象赋值给 F 的 prototype 属性
  Object.defineProperty(F, 'prototype', {
    value: prototype,
    writable: true,
    enumerable: false,
    configurable: false
  });
}

箭头函数实例化 (InstantiateArrowFunctionExpression)

箭头函数的实例化流程更简单,因为它被设计为不具备某些普通函数的功能。

算法流程

  1. 捕获环境: 与普通函数一样,捕获当前的词法环境 env 和私有环境 privateEnv
  2. 创建函数对象: 调用 OrdinaryFunctionCreate,但 thisMode 参数被明确指定为 lexical-this
  3. 设置名称: 调用 SetFunctionName,根据其被赋值的变量名等上下文推断并设置 .name 属性。
  4. 返回: 直接返回创建的函数对象 closure

与普通函数的根本差异

实例化过程决定了行为差异

  • MakeConstructor 调用: 箭头函数的实例化流程完全跳过了 MakeConstructor 这一步。这是它没有 prototype 属性不能作为构造函数(没有 [[Construct]] 内部方法) 的根本原因。
  • 词法 this: 在调用 OrdinaryFunctionCreate 时,thisMode 被硬编码为 lexical-this。这导致其 [[ThisMode]] 内部插槽被设置为 lexical,从而在执行时,其 this不是动态绑定的,而是从其被定义时捕获的 [[Environment]] 中静态解析的。
  • arguments 绑定: 箭头函数在创建时,其内部 [[ECMAScriptCode]] 的解析方式也不同,规范规定不为其创建本地的 arguments 对象绑定。任何对 arguments, super, thisnew.target 的引用,都必须从其捕获的词法环境 env 中解析。