前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
代码开篇
本文以下列代码为例:
javascript
function test(paramA, paramB){
"use strict"
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, paramB, letC, fn);
}
test("paramA","paramB");
知识准备
简单参数 IsSimpleParameterList
是不是简单参数。协议内容罗列了一大堆, 简单归纳就是参数没有如下行为:
- 参数默认值
- 解构
- 剩余参数
在特异对象的章节有涉及到:如果是严格模式 或者非简单参数模式,是不会和形参进行联动变化的。
联动变化和不联动变化:
javascript
// 不联动变化, 非简单参数
function func(a = 55) {
a = 99;
console.log(arguments[0], a);
}
func(10); // 10 99
// 联动变化
function func2(a) {
a = 99;
console.log(arguments[0], a);
}
func2(10); // 99 99
函数严格模式下,参数默认值,解构,剩余参数等行为,均会报错。
原因,函数内部的严格模式,同时适用于函数体和函数参数。 但是,函数执行的时候,是先评估函数参数,然后再执行函数体,这样不合理的地方就是只有从函数体之中,才能知道参数是否应该以严格模式执行,这就已经晚了。
javascript
// 默认参数
function fn1(paramA, paramB = a) {
'use strict'
// ......code
}
// 解构
function fn2({propertyA, propertyB}) {
'use strict'
// ......code
}
// 剩余参数
const fn3 = (...rest) => {
'use strict'
// ......code
}

参数包含表达式 ContainsExpression of formals
你要是语法向的协议描述,难搞。 主要是两种行为
- 有赋值
- 有计算属性
javascript
// 默认值, 有赋值行为
function test(a = 1){
console.log(a);
}
function test({a = 1}){
console.log(a);
}
function test(a = (ccc=100)){
console.log(a);
}
// 计算属性
var a = "a";
function test({ [a]: b }) {
console.log(b);
}
function test({ [(ccc='a')]:b }){
console.log(b);
}
test({a:100})
这里就有点有意思的推断了, 还记得简单参数吗
- 无默认值
- 无解构
- 无剩余参数
而参数包含表达式的条件,稍作调整
- 有赋值
- 有计算属性 (解构时会出现)
而计算属性什么情况下会出现呢? 解构,对象解构和数组解构,均可能出现。
所以有下面的推断
- 参数包含表达式 一定 不是简单参数
[[ThisMode]]
定义如何在函数的形式参数和代码体中解释this引用。
协议描述参见 Internal Slots of ECMAScript Function Objects的表格。
[[ThisMode]] | lexical | 当前函数为箭头函,函数没有自己的this。 |
---|---|---|
strict | 严格模式。完全按照函数调用所提供的方式使用,不会对this进行包装或者改变。 | |
global | 值为 global 意味着当 null 或者 undefined 为 this 绑定值时会被解析为对全局对象的引用。 |
不管模式咋样,执行上下文都是通过 ResolveThisBinding从环境记录中取获取 this的值,更多细节单独的文章老说。
流程回顾
再回顾一下上一节提到的大体流程

接着上个章节,说今天的主角 [[Call]]
[[Call]] 是什么
[[Call]]
是协议内部方法,来识别是不是可以被调用,即函数。
在协议内部主要有两个提到
10.2.1 可以认为是开发者编写的函数调用逻辑, 10.3.1 是内置函数的调用。
大家都知道函数也是对象。怎么区别函数和对象的呢,和这个 [[Call]]
大有关系。
Function也是Object, 协议内部是怎么区分的呢? 很简单, 如果Object内部方法 [[Call]]
, 那么就是函数。 协议内部对应有个方法叫做 IsCallable,通常用来识别是不是可以被调用,即函数。

系统内置的函数,程序员编写的函数,以及前面两个章节的 <<Bound Function Exotic Objects>> 提到的绑定函数特异对象,均是如此。
[[Call]] 流程简析
借用上章节的图,[[Call]]
对应流程图红色圈出的部分的逻辑

结合流程图,再一起分析流程。
[[Call]] ( thisArgument, argumentsList )
- PrepareForOrdinaryCall ( F, newTarget )
切换上下文,初始化环境记录:词法环境记录,变量环境记录,私有环境记录等 - OrdinaryCallBindThis ( F, calleeContext, thisArgument )
根据调用函数的[[ThisMode]]
, 给函数环境记录绑定this的值等 - OrdinaryCallEvaluateBody ( F, argumentsList )
评估并调用函数体- EvaluateFunctionBody
这里会根据函数的类型不同,走不通的逻辑。主要分为普通函数,箭头函数,生成器函数,异步生 成器函数,异步函数,异步箭头函数 底层都会调用FunctionDeclarationInstantiation
的函 数,进行申明实例化操作。 - FunctionDeclarationInstantiation ( func, argumentsList )
函数申明实例化。 - Evaluation of FunctionStatementList
执行函数语句列表。
- EvaluateFunctionBody
- ReturnIfAbrupt
返回结果或者抛出异常。
[[Call]] ( thisArgument, argumentsList )
- 新建执行上下文,环境记录: 词法环境和变量环境,之后会被替换掉。
- 根据调用函数的
[[ThisMode]]
, 给函数环境记录绑定this的值。 - 函数调用 ,底层调用 OrdinaryCallEvaluateBody ( F, argumentsList )
- 恢复上下文
- 返回结果

PrepareForOrdinaryCall ( F, newTarget )
- 新建代码执行上下文,并进行一些设置
- 初始化默认的环境记录,后期会被更改
上下文的词法环境记录和变量环境记录的初始化都是函数环境记录
- 新上下文入栈,并且作为当前执行山上下文
OrdinaryCallBindThis ( F, calleeContext, thisArgument )
更多的细节在 <<函数的this之路>> 章节。
根据函数的内置属性[[ThisMode]]
和 传入的 thisArgument
来设置环境记录的[[ThisValue]]
, 也就是后面函数调用的 this。
详情可以看带标注的下图
这里是在环境记录上绑定 this 的值, 函数体语句执行时,如果用到this,当前上下文会负责获取 会通过 ResolveThisBinding ( ) 函数调用时this
的值,其逻辑为:
- 通过 GetThisEnvironment ( ) 获取具有有效
this
的环境记录 env。 这个过程会一直往上层的环境记录查找,当然最后的环境记录就是全局环境记录,其一定有this
。 - 再通过 环境记录 env.GetThisBinding() 方法获取 this 的值。因环境记录类型不一样,逻辑也会不一样。
- 函数环境记录GetThisBinding ( ) 是直接读取内置属性
[[ThisValue]]
- 全局环境记录 GetThisBinding ( ) 直接读取内置属性
[[GlobalThisValue]]
, 也就是常说的全局对象。
- 函数环境记录GetThisBinding ( ) 是直接读取内置属性
OrdinaryCallEvaluateBody ( F, argumentsList )
不同类型的函数的函数体走的逻辑是不一样的
函数类型 | 逻辑方法 |
---|---|
普通函数 | EvaluateFunctionBody |
箭头函数 | EvaluateConciseBody |
生成器函数 | EvaluateGeneratorBody |
异步生成器函数 | EvaluateAsyncGeneratorBody |
异步函数 | EvaluateAsyncFunctionBody |
异步箭头函数 | EvaluateAsyncConciseBody |
当然,本示例比较属于普通函数,走的是 EvaluateFunctionBody。
不管是那种函数的函数体,其底层都会调用 FunctionDeclarationInstantiation ( func, argumentsList ) 进行函数申明实例化,在之后在进行不同的定制化的操作。
EvaluateFunctionBody 逻辑很简单,所谓的字数越少,事情越大,下面每一行都是非常复杂的行为。当然都可以简单的概括总结。
- 函数申明实例化
- 评估执行函数体的语句

FunctionDeclarationInstantiation ( func, argumentsList )
函数申明实例化,这是重中之重!!!!!!!!!!!!!!!!!!!!!。
整个过程有兴趣的同学可以好好看看,如果看着头疼,没关系, 了解一下下图的内容,就可以跳过本小节,重点是后面的 场景分析。
简单说做了如下的操作操作(不表示按照顺序)

其主要作用就是按需创建环境记录,按需创建arguments
对象,在环境记录上创建绑定关系,函数实例化等。
- 实例化各种内部用的变量

- 遍历var申明解析节点,对其中的函数申明节点做处理。

- 判断是不是需要创建
arguments
对象

- 根据严格模式和是否有参数表达式,确定是否新建申明环境记录。

- 在环境记录上创建形参的绑定关系

- 按需初始化
arguments
对象

- 给函数参数对应的绑定关系初始化(赋值)

左边是执行前,右边是执行后
- 函数如果没有参数表达式的情况,在环境记录上实例化 var申明

- 如果有参数表达式,新建环境记录

- 按需创建新的函数申明环境。

创建绑定关系(实例化)和初始化是两个操作。 CreateMutableBinding ( N, D ) 只是实例化,其状态为uninitialized
。InitializeBinding ( N, V ) 初始化,可以设置值,状态转为initialized
- 根据词法申明的解析节点,在词法环境记录上创建绑定关系

- 设置私有环境记录, 实例化函数对象以及在变量环境记录中创建函数绑定关系。

Evaluation of FunctionStatementList
这个就是每个语句进行解析执行。略过。
函数申明实例化究竟创建了几个环境记录(重点,重点,重点)
函数申明实例化FunctionDeclarationInstantiation ( func, argumentsList ) 过程,有几处新建了申明环境记录,并有多处进行绑定,如果纯从文字上去理解,虽然有些地方有Note, 依然非常,非常,非常难于理解。
在 PrepareForOrdinaryCall ( F, newTarget ) 的时候创建了一个函数环境记录,本例的函数test
是在全局代码下创建的,所以函数环境记录的外部环境是全局环境。
javascript
function test(paramA, paramB){
"use strict"
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, paramB, letC, fn);
}
test("paramA","paramB");

此时的环境记录如下:

FunctionDeclarationInstantiation ( func, argumentsList ) 函数申明实例化的过程会根据一定条件新建环境记录:
-
步骤20时,如果非严格模式 + 有函数参数表达式
新建申明环境记录作为上下文的 LexicalEnvironment,
[[OuterEnv]]
为上下文旧的 LexicalEnvironment -
步骤28,如果有函数参数表达式
新建申明环境记录作为上下文的 VariableEnvironment,
[[OuterEnv]]
为上下文旧的 LexicalEnvironment -
步骤30 ,如果非严格模式
新建申明环境记录作为上下文的 LexicalEnvironment
[[OuterEnv]]
为上下文旧的 VariableEnvironment
所以呢? 除了函数环境记录本身外, 还可能创建 0 - 3,确切的是0,1或者3 个申明环境记录,至于为什么要创建新的环境记录,挨个进行分析。
场景分析
从协议上看,新建环境记录的情况如下:
- 非严格模式 + 有参数表达式 环境记录 +1 步骤20
- 有参数表达式 环境记录 + 1 步骤28
- 非严格模式 环境记录 +1 步骤30
如果按照 非严格模式 和 有参数表达式 两个条件,实际可以组合四种场景
序号 | 非严格模式 | 有参数表达式 | 实际新建申明环境记录数量 |
---|---|---|---|
1 | ✔️ | ✔️ | 1 + 1 + 1 = 3 |
2 | ❌ 即严格模式 | ✔️ | 不存此情况 |
3 | ✔️ | ❌ | 1 |
4 | ❌ 即严格模式 | ❌ | 0 |
严格模式只允许简单参数,函数参数如果有表达式(就一定不是简单参数),就一定不是简单参数, 所以不会出现2。
步骤20 why? 非严格模式 + 有参数表达式
首先这种场景和函数参数表达式中有 eval
有关。
- 函数严格模式不允许有 参数表达式,所以只可能出现在非严格模式
- 无参数表达式就不可能有
eval
调用, eval 出现在 参数默认值,计算属性等场景
函数参数表达式出现 eval
的条件就是:非严格模式 + 有参数表达式
比如下面的代码 eval就会产生新的var申明 paramC
。
javascript
function test(paramA = eval("var paramC = 'paramC'"), paramB = paramC){
var varA = "varA";
let letA = "letA";
console.log(paramA, paramB, varA, letA);
}
test(); // undefined 'paramC' 'varA' 'letA'
接下来就是 eval 的直接调用了,eval的调用比较复杂,就简单说 非严格模式 下的直接调用, 这种情况下
- eval 被调用时使用的上下文是 调用eval函数的执行上下文,即当前执行上下文,不会新建执行上下文
- eval 被调用时 var 申明的变量会绑定到 调用eval函数的上下文的环境记录中
调用 eval的上下文,就是test
函数对应的上下文,在函数申明初始化之前,环境记录和词法环境记录都指向函数环境, 关系图如下:

所以 eval
生成var申明的绑定关系就会保存到了 函数环境记录。
步骤20也有注释, 需要一个独立的上下文来保存形参上因为eval直接调用而产生的绑定关系,并且这个环境应该在形参环境所在环境的外部。

在步骤28的时候,
- 你会知道形参也会独立保存到一个新的环境记录,
- 而形参环境记录其实又可能从 形参eval产生变量申明和函数申明所以在环境记录 中查找绑定关系的。
例如如下函数:b在调用的时候,会从形参eval产生的环境记录中去查找c。所以才会输出结果为8。
javascript
function t(a = eval('var c = 8'), b = () => c) {
console.log(b())
}
t() // 8
环境记录关系如下:

所以,简单一句话: 利用函数环境记录保存eval创建的var申明
,新建一个申明环境记录去保存 函数环境记录原来该保存的申明。

是什么时候建立对应的绑定关系呢?实际是在后面的步骤25.56。

有兴趣的同学,可以研究一下:
javascript
function t(a = eval('var c = 8'), b = c) {
console.log(b)
}
t() // 8
步骤28 Why? 有参数表达式

需要一个单独的环境记录,来确保由形式参数列表中的表达式创建的闭包(函数),不能访问函数体中声明。
比如下面的代码:
javascript
function foo1(a, b1 = () => c ) {
var c = 1
console.log(b())
}
foo(2) // ReferenceError: c is not defined
function foo2(a, b2 = () => a ) {
var c = 1
console.log(b())
}
foo(2) // 2
- 闭包(函数)b1 是不能访问 函数 foo1 函数体内的变量申明
c
的 - 闭包(函数)b2 是可以访问 函数 foo2 的函数参数
a
的
为了实现这种隔离, 如果 foo1 函数体内的变量申明 和 foo1 函数的参数 保存在同一个环境,显然是不行的,因此这里才需要一个环境记录来达到这种隔离的目的。
新建一个申明环境记录来单独保存函数形参数的绑定关系,当 b1
执行时,从形参所在的环境记录去查找绑定关系,而不是从 foo1 函数体内部变量申明保存的环境去查找。
小结: 步骤28创建的申明环境记录被用于保存形参的绑定关系,新建一个申明环境记录、
这一步之后的环境记录关系图如下:

步骤30 why ? 非严格模式

非严格模式的函数用一个单独的环境记录来保存顶级词法申明,因此 eval的直接调用可以确定 eval 代码引入的任何 var 作用域声明是否与预先存在的顶级词法作用域声明冲突。
举个例子, eval产生的 var 申明 a
与函数顶级词法申明 a
冲突, 新的环境记录就是用于保存词法申明的。
js
function test(){
let a = 'letA'
eval(`var a = 'varA'`)
}
test(); // Uncaught SyntaxError: Identifier 'a' has already been declared
这里需要了解的是,词法环境记录和变量环境记录可能是同一个环境记录,比如:
- 比如全局顶层代码执行时
- 严格模式 + 无函数参数表达式的函数执行时,后面的场景4会提到。
所以是因为要用单独的环境记录来保存顶级词法申明,注意顶级,因为函数内部可以有多级词法申明, 下面的两个a属于不同level的词法申明。
javascript
function test(){
let a = 1; // 顶级
{
let a = 1 // 非顶级
}
}
所以这里新建的的申明环境记录就是用于保存函数顶级词法申明,而步骤28创建申明环境的用于函数内部 var明和函数申明。
新建申明环境记录之后,关系结构如下:

下面的场景示例,不考虑参数表达式中有eval
的情况。
场景1: 非严格模式 + 有函数参数表达式 (新建3个环境记录)
javascript
function test(paramA = eval('var evalC = "evalC"')){
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, evalC, varA, letA, fn);
}
test();
// undefined 'evalC' 'varA' 'letA' ƒ fn(){
// console.log("i am a function");
// }
- 步骤20时,如果非严格模式 + 有函数参数表达式,到步骤28之前环境记录关系如下:

- 步骤28,如果有函数参数表达式,30步骤之前环境记录关系如下:

- 步骤30 ,如果非严格模式, 到函数申明实例化完毕

小结四个环境记录的作用, 从函数申明环境环境倒推:
序号 | 类型 | 创建时机 | 本示例保存的申明 | 保存的申明绑定关系 |
---|---|---|---|---|
第一个 | 函数申明环境 | 函数准备调用 | evalC | 参数表达式 eval 创建的var申明 |
第二个 | 申明环境记录 | 步骤20 | paramA,arguments | 函数参数的绑定关系 |
第三个 | 申明环境记录 | 步骤28 | varA,fn | var申明和函数申明 |
第四个 | 申明环境记录 | 步骤30 | letA | let,const,class 等词法申明 |
场景3: 非严格模式 + 无函数参数表达式 (新建1个环境记录)
javascript
function test(paramA, paramB){
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, paramB, letC, fn);
}
test("paramA","paramB");
(步骤30 ,如果非严格模式) , 到函数申明实例化完毕

小结2个环境记录的作用, 从函数申明环境环境倒推:
序号 | 类型 | 创建时机 | 本示例保存的申明 | 保存的申明绑定关系 |
---|---|---|---|---|
第一个 | 函数申明环境 | 函数准备调用 | paramA, paramB arguments varA fn | 形参 变量申明 函数申明 |
第二个 | 申明环境记录 | 步骤30 | letA | let/const等词法申明 |
场景4: 严格模式 + 无函数参数表达式 (新建0个环境记录)
示例代码如下。
javascript
function test(paramA, paramB){
"use strict"
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, paramB, letC, fn);
}
test("paramA","paramB");
步骤20,步骤28, 步骤30均没有走,没有新建环境记录。

小结1个环境记录的作用, 从函数申明环境环境倒推:
序号 | 类型 | 创建时机 | 本示例保存的申明 | 保存的申明绑定关系 |
---|---|---|---|---|
第一个 | 函数申明环境 | 函数准备调用 | paramA, paramB arguments varA fn letA | 形参 变量申明 函数申明 let/const等词法申明 |
小结
- 严格模式 + 参数表达式 这两个条件额外的环境记录的创建
- 非严格模式 + 有参数表达式 3
- 非严格模式 + 无参数表达式 1
- 严格模式 + 无参数表达式 0
- 函数申明环境记录,会 根据 严格模式 + 参数表达式 组合模式,保存不同形式的申明绑定关系。
- 执行上下文词法环境和变量环境 有可能相等
函数新建三个环境记录示例
上下文通过 ResolveBinding ( name [ , env ] ) 来查找标志符的并生成引用记录,绝大多情况都是没有传 env参数时(解构,剩余参数等情况会传递), 就默认会从执行上下文的词语环境记录开始查找。 之后就会通过环境记录的 [[outerEnv]]
一层一层往外查找。

如下示例标志符的查找是从执行上下文的词语环境开始的。
javascript
function test(paramA = eval('var evalC = "evalC"')){
var varA = "varA";
let letA = "letA";
function fn(){
console.log("i am a function");
}
console.log(paramA, evalC, varA, letA, fn);
}
test();
其执行上下文和环境记录的关系图如下:
console.log(paramA, evalC, varA, letA, fn)
调整为 console.log(evalC, test.name);
这里就牵涉两个标志符的查找。
实际上画出了关系图,查找嘛,三岁小孩子都会。
- evalC

- test

引用
步骤20, eval
eval
declaration instantiation when calling context is evaluating formal parameter initializers
Normative: Eliminate extra environment for eval in parameter initializers #1046
步骤28,
Where are arguments positioned in the lexical environment?