前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]
以题开局
javascript
let name = "let-name";
const animal = {
name: "animal-Name",
getName() {
return this.name
}
};
const log = console.log;
const getName = animal.getName;
log('getName():',getName());
log('animal.getName():', animal.getName());
log('(animal.getName)():', (animal.getName)());
log('(0, animal.getName)():', (0, animal.getName)());
这段代码在浏览器和nodejs环境中执行还能获得不一样的结果。
浏览器
window.name在没有显式赋值的情况下,那么结果如下:
javascript
getName(): //
animal.getName(): // animal-Name
(animal.getName)(): // animal-Name
(0, animal.getName)(): //
分析:
- getName() 和 (0, person.getName)() 这两种方式执行, this是全局对象window
- person.getName() 和 (person.getName)() 这种方式执行, this是 animal对象
nodejs环境
javascript
getName(): // undefined
animal.getName(): // animal-Name
(animal.getName)(): // animal-Name
(0, animal.getName)(): // undefined
分析:
- getName() 和 (0, animal.getName)() 这两种方式执行, this是 空对象
{}
。 - animal.getName() 和 (animal.getName)() 这种方式执行, this是 animal对象
还要额外提醒一下,
- window 对象真的有name属性,而且默认值是空字符串
javascript
console.log('window.name:', window.name) // window.name:
- 浏览器环境下,let和const申明的变量(申明环境记录),不会挂到全局对象window上,使用var或者直接变量赋值的才会挂在到window对象上(对象环境记录)。 这里主要为了解释浏览器环境下 getName() 和 (0, person.getName)() 这两种调用返回是 空字符串。
javascript
var varName = 'varName';
let letName = 'letName';
xName = 'xName';
const log = console.log;
log(this.varName); // varName
log(this.letName); // undefined
log(this.xName); // xName
这种诡异的行为,与之相关的有两个运算符:
- ( ) 分组运算符
- ( , ) 逗号运算符
接下来,来揭开面纱。 在此之前先回忆几个协议概念
- 引用记录(Reference Record)
- 取值 GetValue
引用记录(Reference Record)
这个概念 在 协议的第六部分 6.2.5 The Reference Record Specification Type。
这个 Reference Record Type 不是数据类型说的基本类型和引用类型中那个引用类型,是ECMA协议内部的一个概念。
引用记录类型用于解释诸如 delete、 typeof、赋值运算符、 super 关键字和其他语言特性等运算符的行为。
例如,赋值的左边操作数应该生成一个引用记录。
对标志符取值的时候,也会生成引用记录。
obj.x
的x
和 var 的x
本质都是标志符,其背后逻辑,是先获取引用记录,然后进行取值操作。
javascript
const obj = {
x: x
}
var x = 'x';
console.log(obj.x);
console.log(x);
细看一下Reference Record的结构。

Reference Record 里面有四个Filed,主要看 [[Base]]
, 其
- 可以是值 ( ECMAScript language value)
就是编程中常见的值,比如 undefined, null , String , Boolean , Object等等
- 也可以是环境记录( Environment Record)
- 还可以是
unresolvable
。
unresolvable
,和他关联紧密有一个方法为 IsUnresolvableReference ( V ), 其主要就是判断引用可达不可以,其一种表现就是变量申明没有。

GetValue

其就是一个基本的取值操作,如果不是引用记录直接返回值,接下来就是对引用记录的取值操作,所以引用记录这个概念非常重要。
- 如果V不是引用记录,直接返回V
如下情况就是,细节到逗号运算符说
javascript
(0,1)
- 引用记录不可达, 报错
javascript
console.log(xxxxxxxxxxxxxxxxx) // Uncaught ReferenceError: a is not defined
- 如果是属性引用,进行取值。 其实其本质也就是检查 Reference Record 的
[[Base]]
, 如果不是环境记录,如果不是unresolvable
,那么就是属性引用。
比如 obj.x
javascript
const obj ={ x: 1 };
- 其他情况就是 环境记录
举个例子如下的 console.log(name)
javascript
function logName(){
var name = 'name';
return log(){
console.log(name);
}
}
logName().log();
与 GetValue 对应的还有一个 PutValue, 相应的这个是设置值,了解即可。
分组(圆括号)运算符()
最常用来改变运算优先级。
javascript
(1 + 2) * 3 = 9
额外说点有意思的, 如下的代码会抛出异常。
javascript
const num = 1
(num).toString()
// ncaught ReferenceError: num is not defined
(num)
会被解释为函数调用,解释为如下代码
javascript
const num = 1(num).toString()
类似的代码,数组字面量,也会抛出异常。
javascript
const num = 1
[1].toString()
// Uncaught TypeError: Cannot read properties of undefined (reading 'toString')
会被解释为如下代码
javascript
const num = 1[1].toString()
废话那么多,只想说一下,每个语句最后添加 ;
不是个坏习惯。
当然这些不是本节的重点,协议 13.2.9 The Grouping Operator 有一段备注。

翻译一下: 此算法不将 GetValue 应用于表达式计算。这样做的主要原因是,诸如 delete 和 typeof 之类的运算符可以应用于带括号的表达式,毕竟delete是可以用来删除引用关系的。
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
delete (animal.getName)
console.log(animal.getName); // undefined getName被删除
console.log(Object.keys(animal)); // ['name']
如果 (animal.getName)
进行了取值, 上一节有阐述delete
后面跟的是值的 (非引用记录),是直接返回 true 的,没有任何实质的操作,自然不能从 animal 上删除 getName
属性
javascript
"use strict"
delete true // true
delete function a(){} // true
console.log(`function a:`, a) // Uncaught ReferenceError: a is not defined
这里拓展一下, delete 后面的 function a(){}
属于函数申明,还是函数表达式呢?
答案是: 函数表达式。
可以逆向思维一下,如果是函数申明,delete
执行完毕后,函数a
应该是存在的。
javascript
delete (function a (){}, 1)
console.log("function a:", a) // Uncaught ReferenceError: a is not defined
这是什么意思呢? 就是不会进行取值(GetValue)操作,而是拿着引用进行操作。
(animal.getName)()
就等同拿着 animal.getName
的引用直接进行操作。
javascript
(animal.getName)()
// 等同于如下操作
animal.getName()
到这里,如下的代码也就不难理解了。
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
(animal.getName)(); // animal-Name
逗号运算符( , )

备注中说GetValue是必须被调用的, 啥意思,就是进行了取值操作。
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
(0, animal.getName)() // ''
上面调用代码可以如下伪代码
javascript
(0, const x = animal.getName)();
同理,如果这个时候进行delete ,那么是删除不了 getName 属性的。
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
delete (0, animal.getName)
console.log("animal.getName:", animal.getName); // ƒ getName(){ return this.name }
至此,解释了开局的题目, 此节完。
对比
对两段代码做个对比
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
(animal.getName)();
javascript
const animal = {
name: "animal-Name",
getName(){
return this.name
}
};
(0, animal.getName)()
解析树对比
第一个语句是完全一致的,重点看第二个语句。
- 左边: ParenthesizedExpression 的Expression为 MemberExpression 即成员表达式
- 右边: ParenthesizedExpression 的Expression为 CommaOperator 逗号运算符,其ExpresssionList 里面除了多了一数字字面量,同左边一样一个 MemberExpression 即成员表达式
所以核心的操作,还是在于 CommaOperator 逗号运算符。

环境记录
本章节示例代码,当getName
函数内部语句被执行时。
仅仅是有分组运算符()
的图示:
- this 的值,就是从函数环境记录取 ThisValue
- thisValue 的值 是 animal 对象

逗号运算符(,)
图示:
- this 的值,就是从函数环境记录取 ThisValue
- thisValue 的值 是 全局对象。

不知道大家眼中函数调用的 this 是什么样的?
依照协议描述:
- 函数调用时,在真正执行函数体的语句前,会依据函数的
[[ThisMode]]
即this的模式,先获取 this的值。函数调用一定有this, 箭头函数的this ,是从外面借来的。 - 然后把 this 的值,绑定到函数环境记录,
- 函数体的语句执行时,用到
this
,再从 函数环境记录中 取 this 的值。
更多细节,后面的 函数this之路有详细的解答。