前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
看点代码
javascript
var d = "d";
function test(a, b = 1){
let c = 2;
console.log(a, b, c);
}
test();
如上代码调用test()
是如何运作的呢? test在这里不过是文本罢了。
回顾之前 <<script加载和全局顶层代码申明实例化>> 并未细说函数的调用,仅仅涉及到了 各种申明的实例化。本文就详细讲解开发者眼中的函数对象(Function Object)是如何被初始化的。
先回顾一下 script从源码到被执行的基本流程

本文的重点就是 蓝色标注的流程中的 全局申明实例化的一部分逻辑, 函数对象实例化。
本文只阐述 普通函数。
Script Record
Script Record 脚本记录 之前提过,是源代码被 ParseScript ( sourceText, realm, hostDefined ) 初步解析后的数据结构。
这个过程底层是ParseText
把源代码转为 Script的解析树。请记住这个解析树,尤其重要。
Script 节点
一起来看看这个类型为Script的解析树到底长啥模样。 至于树怎么生成,不是本系列的内容。

这种结构,在协议本身是有规范的,语法可以参见 Scripts

有三个语句,和代码的对应关系如下:

可以通过代码获取来论证一下:

完整的解析树JSON版本如下
json
{
"type": "Script",
"location": {
"startIndex": 0,
"endIndex": 84,
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 6,
"column": 8
}
},
"strict": false,
"ScriptBody": {
"type": "ScriptBody",
"location": {
"startIndex": 0,
"endIndex": 84,
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 6,
"column": 8
}
},
"strict": false,
"StatementList": [
{
"type": "VariableStatement",
"location": {
"startIndex": 0,
"endIndex": 12,
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 12
}
},
"strict": false,
"VariableDeclarationList": [
{
"type": "VariableDeclaration",
"location": {
"startIndex": 4,
"endIndex": 11,
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 9
}
},
"strict": false,
"BindingIdentifier": {
"type": "BindingIdentifier",
"location": {
"startIndex": 4,
"endIndex": 5,
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 5
}
},
"strict": false,
"name": "d"
},
"Initializer": {
"type": "StringLiteral",
"location": {
"startIndex": 8,
"endIndex": 11,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 9
}
},
"strict": false,
"value": "d"
}
}
]
},
{
"type": "FunctionDeclaration",
"location": {
"startIndex": 13,
"endIndex": 76,
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 5,
"column": 1
}
},
"strict": false,
"BindingIdentifier": {
"type": "BindingIdentifier",
"location": {
"startIndex": 22,
"endIndex": 26,
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 10
}
},
"strict": false,
"name": "test"
},
"FormalParameters": [
{
"type": "SingleNameBinding",
"location": {
"startIndex": 27,
"endIndex": 28,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 15
}
},
"strict": false,
"BindingIdentifier": {
"type": "BindingIdentifier",
"location": {
"startIndex": 27,
"endIndex": 28,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 15
}
},
"strict": false,
"name": "a"
},
"Initializer": null
},
{
"type": "SingleNameBinding",
"location": {
"startIndex": 30,
"endIndex": 35,
"start": {
"line": 2,
"column": 18
},
"end": {
"line": 2,
"column": 22
}
},
"strict": false,
"BindingIdentifier": {
"type": "BindingIdentifier",
"location": {
"startIndex": 30,
"endIndex": 31,
"start": {
"line": 2,
"column": 18
},
"end": {
"line": 2,
"column": 18
}
},
"strict": false,
"name": "b"
},
"Initializer": {
"type": "NumericLiteral",
"location": {
"startIndex": 34,
"endIndex": 35,
"start": {
"line": 2,
"column": 22
},
"end": {
"line": 2,
"column": 22
}
},
"strict": false,
"value": 1
}
}
],
"FunctionBody": {
"type": "FunctionBody",
"location": {
"startIndex": 36,
"endIndex": 76,
"start": {
"line": 2,
"column": 24
},
"end": {
"line": 5,
"column": 1
}
},
"strict": false,
"directives": [],
"FunctionStatementList": [
{
"type": "LexicalDeclaration",
"location": {
"startIndex": 40,
"endIndex": 50,
"start": {
"line": 3,
"column": 3
},
"end": {
"line": 3,
"column": 12
}
},
"strict": false,
"LetOrConst": "let",
"BindingList": [
{
"type": "LexicalBinding",
"location": {
"startIndex": 44,
"endIndex": 49,
"start": {
"line": 3,
"column": 7
},
"end": {
"line": 3,
"column": 11
}
},
"strict": false,
"BindingIdentifier": {
"type": "BindingIdentifier",
"location": {
"startIndex": 44,
"endIndex": 45,
"start": {
"line": 3,
"column": 7
},
"end": {
"line": 3,
"column": 7
}
},
"strict": false,
"name": "c"
},
"Initializer": {
"type": "NumericLiteral",
"location": {
"startIndex": 48,
"endIndex": 49,
"start": {
"line": 3,
"column": 11
},
"end": {
"line": 3,
"column": 11
}
},
"strict": false,
"value": 2
}
}
]
},
{
"type": "ExpressionStatement",
"location": {
"startIndex": 53,
"endIndex": 74,
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 23
}
},
"strict": false,
"Expression": {
"type": "CallExpression",
"location": {
"startIndex": 53,
"endIndex": 73,
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 22
}
},
"strict": false,
"CallExpression": {
"type": "MemberExpression",
"location": {
"startIndex": 53,
"endIndex": 64,
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 11
}
},
"strict": false,
"MemberExpression": {
"type": "IdentifierReference",
"location": {
"startIndex": 53,
"endIndex": 60,
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 3
}
},
"strict": false,
"escaped": false,
"name": "console"
},
"IdentifierName": {
"type": "IdentifierName",
"location": {
"startIndex": 61,
"endIndex": 64,
"start": {
"line": 4,
"column": 11
},
"end": {
"line": 4,
"column": 11
}
},
"strict": false,
"name": "log"
},
"PrivateIdentifier": null,
"Expression": null
},
"Arguments": [
{
"type": "IdentifierReference",
"location": {
"startIndex": 65,
"endIndex": 66,
"start": {
"line": 4,
"column": 15
},
"end": {
"line": 4,
"column": 15
}
},
"strict": false,
"escaped": false,
"name": "a"
},
{
"type": "IdentifierReference",
"location": {
"startIndex": 68,
"endIndex": 69,
"start": {
"line": 4,
"column": 18
},
"end": {
"line": 4,
"column": 18
}
},
"strict": false,
"escaped": false,
"name": "b"
},
{
"type": "IdentifierReference",
"location": {
"startIndex": 71,
"endIndex": 72,
"start": {
"line": 4,
"column": 21
},
"end": {
"line": 4,
"column": 21
}
},
"strict": false,
"escaped": false,
"name": "c"
}
]
}
}
]
}
},
{
"type": "ExpressionStatement",
"location": {
"startIndex": 77,
"endIndex": 84,
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 7
}
},
"strict": false,
"Expression": {
"type": "CallExpression",
"location": {
"startIndex": 77,
"endIndex": 83,
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 6
}
},
"strict": false,
"CallExpression": {
"type": "IdentifierReference",
"location": {
"startIndex": 77,
"endIndex": 81,
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 1
}
},
"strict": false,
"escaped": false,
"name": "test"
},
"Arguments": []
}
}
]
}
}
你这说了半天,函数还是没初始化啊,别急。
InstantiateFunctionObject
在之前 <<script怎么加载和申明实例化>> 在全局申明实例化 GlobalDeclarationInstantiation协议内容的尾部有几行不显眼但是 极其重要的文字。

InstantiateFunctionObject 的作用就是把函数申明解析节点转为 函数对象 (Function Object), 参数env和privateEnv分别是环境记录和私有环境记录。
这里的 functionsToInitialize
可还知道是什么吗? 这就是抽象操作 VarScopedDeclarations 结果中包含的各种函数申明节点
你也许会诧异,为什么函数申明跑到变量申明中取了。因为在全局顶层代码中,函数申明表现和var是一样的,这协议中是有明确提到的。

在本例的代码中,需要 通过InstantiateFunctionObject 转为函数对象的解析节点就一个FunctionDeclaration, 其对应如下的代码。
javascript
function test(a, b = 1){
let c = 2;
console.log(a, b, c);
}
InstantiateOrdinaryFunctionObject
InstantiateFunctionObject 这个抽象操作把 解析节点转为 函数对象Function Object
, 根据函数类型不同,解析逻辑也是不同的
- InstantiateOrdinaryFunctionObject 实例化普通函数
- InstantiateGeneratorFunctionObject 实例化生成器函数
- InstantiateAsyncGeneratorFunctionObject 实例化异步生成器函数
- InstantiateAsyncFunctionObject 实例化异步函数
这里实例化后的函数对象,才是可以被调用的,这里可以对开发者代码中的function
概念了。
test
也是如此,在全局申明实例化之后,test有了对应的 Function Object
,并在全局环境记录存在绑定关系。
本示例是一个普通函数,走的逻辑就是 InstantiateOrdinaryFunctionObject, 实例化普通函数,基本流程:

真正实例化函数的是 OrdinaryFunctionCreate, 为了进一步了解流程,还是得需要进一步了解一点解析节点FunctionDeclaration的信息。
属性名 | 备注 |
---|---|
BindingIdentifier | 标志符信息,本例为函数名的信息 test |
FormalParameters | 形参的信息,本例为形参a , b 的信息 |
FunctionBody | 函数体,即函数一对大括号里面的内容信息 |
location | 位置信息,基本上每个解析节点都有这个信息。记录了节点对应代码的起始位置,可以快速的获取节点对应的源码部分。 |
type | 解析节点的类型 |
strict | 是否是严格模式 |

实际FunctionBody是如下内容,是包含前后两个大括符的,上面不好标注,敬请谅解。
javascript
{
let c = 2;
console.log(a, b, c);
}
具备这些知识后,再看看函数申明FunctionDeclaration 实例化为函数对象的协议描述
%Function.prototype%
就是开发者常用的Function.prototype
- sourceText 是 FunctionDeclaration 对应的源码
- FormalParameters 形参
- FunctionBody 函数体,包含大括号
- non-lexical-this: lexical表示是非箭头函数 ,non-lexical-this 即是 strict 或者 global
- env (环境记录)和 privateEnv (私有环境记录)都是从 GlobalDeclarationInstantiation 传入的
所以,这个中间商的主要作用就是取参数,让OrdinaryFunctionCreate生成函数对象,然后设置一下名字,把函数转为构造函数。
现在把灯光给 OrdinaryFunctionCreate。
OrdinaryFunctionCreate
作用是创建普通函数, 基本逻辑如下:
- 创建标准的对象
- 设置
[[Call]]
,[[SourceText]]
,[[FormalParameters]]
,[[ECMAScript]
,[[Script]]
,[[ThisMode]]
等等各种函数内置参数 - 设置的长度属性 length。

17,18,20
都是和class相关的,暂时无需关注。
到此为止,函数对象基本初始化完毕,基本经理了下面三个阶段
- 源代码 (文本)
- 解析节点
- 函数对象
这里记住,函数对象本身也只是记录了一些外部信息,函数内部的代码是什么样子的,除了简单的前期静态语法检查,目前为止还是一个黑盒。
里面具体的细节,都是函数调用时,动态分析的。
函数表达式 FunctionExpression
当然函数对象可以从 函数申明 FunctionDeclaration 实例化,也可以从 函数表达式 FunctionExpression 初始化。
如下的代码 function test() {}
部分就是函数表达式。
javascript
const fn = function test() {
};

协议 InstantiateOrdinaryFunctionExpression 部分描述了普通函数表达式的初始化逻辑。
下图与 普通函数表达式的初始化逻辑做了个对比,除了执行上下文和环境记录有区别外,函数本身实例化的逻辑是基本一致的。 至于具名的函数表达式为什么会多一个环境记录,在 闭包 章节有详细的解答。
左边:函数申明 vs 右边 函数表达式

当然除了普通函数表达式 FunctionExpression, 还有
- 异步函数表达式: AsyncFunctionExpression
- 生成器(函数)表达式:GeneratorExpression
- 异步生成器(函数)表达式:AsyncGeneratorExpression
额,希望后会有期把。
属性方法定义 MethodDefinition
当然,如下的代码也会实例化函数对象,这种方式协议称为 MethodDefinition。
javascript
var obj = {
fn() {
}
}
大致的节点信息如下图标注
- 上面只有一个语句,变量申明语句 VariableStatement,包含一个变量申明(可包含多个)
- 变量申明的初始化器(Initializer)是一个 对象字面量 (ObjectLiteral)
- 对象字面量 (ObjectLiteral)属性定义列表(PropertyDefinitionList) 中有一个
fn
的 方法定义MethodDefinition。

方法定义有好多种语法格式,本示例对应的 红色标记处的部分。

协议描述流程如下:

与普通的函数申明实 例化的函数对象有一些区别
- 其没有被转为构造函数MakeConstructor(F), 不可以被 new
- 其有通过MakeMethod(closure, object) 设置
[[HomeObject]]
, 所有使用 super。


一起用代码验证一下:
javascript
var obj = {
fn(){
console.log(super.toString)
}
};
// 可以使用super
obj.fn() // ƒ toString() { [native code] }
// 不可以被new
new obj.fn() // Uncaught TypeError: obj.fn is not a constructor
当然除了普通方法定义外, 还有
- 异步方法: AsyncMethod
- 生成器(函数)方法:GeneratorMethod
- 异步生成器(函数)方法:AsyncGeneratorMethod
额,希望后会有期把。
函数申明 vs 函数表达式 vs 属性方法定义
一起看看 通过 函数申明 FunctionDeclaration,函数表达式 FunctionExpression ,方法定义 MethodDefinition 实例化的函数对象, 从下面的维度比较
方式 | 节点 | 可以被new | 可以super |
---|---|---|---|
函数申明 | FunctionDeclaration | ✔ | ✘ |
函数表达式 | FunctionExpression | ✔ | ✘ |
方法定义 | MethodDefinition | ✘ | ✔ |
函调调用流程
javascript
var d = "d";
function test(a, b = 1){
let c = 2;
console.log(a, b, c);
}
test();
当 test()
执行时,实际是先找 test
标志符对应的函数对象,然后评估函数对象属性[[ECMAScriptCode]]
里面的语句列表 FunctionStatementList(等于如下红色圈出部分),协议描述的流程大致如下:


函数调用前序
首先 Script 节点是怎么执行的呢?前文提到的 ScriptEvaluation ( scriptRecord ) 也有一段不起眼的脚本。

Evaluation of script
简简单单几个字,其后面非常复杂,就是对整个树进行解析,遇到可以执行的代码就进行调用,然后函数嵌套函数,懂的都懂,新的上下文,新的环境记录统统走起,...............。
当然解析树的节点类型有很多,函数调用关联最密切的就是CallExrpression
。
本例中的三个语句中的第三个语句ExpressionStatement
, 对应源码 test();
。
其属性 Expression 是一个 CallExrpression,她被解析的时候,就会进行函数调用。具体见下图

CallExrpression
在协议 13.3.6 Function Calls 能找到函数调用的相关描述,调用形式有多种。
以下面的代码为例:
javascript
var eName = 'global eName';
function log(){
return this.eName
}
log();
函数调用是从对 函数对应的解析节点(CallExpression)的解析开始的。 结合源码一起先了解一下 CallExpression的结构。

type为CallExpression
节点有个叫做 type为IdentifierReference
的 CallExpression
节点,两个节点对应的源码部分已经用不不同颜色标记。
所以,如何通过解析节点找到其对应的 函数对象呢? 答案也是很明显的,
- 通过type为
IdentifierReference
的CallExpression
节点的name属性,能得到函数的标志符 - 上下文用标志符通过 ResolveBinding 查找对应的引用记录
- 引用记录 通过 GetValue从属性上或者环境记住中取的 函数对象(Function Object)
具体的协议有些不好懂, 配合如下的图更加好理解:
Evaluate(memberExpr)
的流程如下:


再简单汇总一下

到此,已经获得了函数对象的,在此之后函数本身真正执行之前,剩余链路如下
EvaluateCall ( func, ref, arguments, tailPosition )
作用很简单,
- 取this的值,
- 检查函数对象是否可以被调用
- 调用内部函数 Call(func, thisValue, argList)

Call ( F, V [ , argumentsList ] )
这个的作用就更简单了,
- 按需设置参数,
- 检查函数对象是否可以调用,
- 调用函数自身的 F.[[Call]]
到此为止,就要真正进入函数调用了,你准备好了吧,激动的时刻就要到来了。
F.[[Call]]
哦豁,请见下篇。
小结
对于本例来说:
函数从文本到函数对象的蜕变过程:
- 源代码 => parseText
- Script解析树(此时函数函数还是Script解析节点上的 Statement 解析节点 )
- 全局申明实例化过程 GlobalDeclarationInstantiation ( script, env )中,执行 => InstantiateFunctionObject 函数初始化
- 函数被实例化,生成函数对象(Function Object),同时在全局环境记录(Global Enviroment Record)生成绑定,以及挂载到全局对象上
函数被调用的过程
- 通过标志符 查找到 对应的函数对象
通过标志符,从全局环境记录(Global Enviroment Record)查找并成成引用记录,以及取出函数对象(Function Object)
取函数的this,判断是否可调用
按需设置参数,判断是否可调用
F.[[Call]]
下文见。