前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
阅读建议
- 知道 脚本顶层代码 和 非顶层代码
- 了解 VarDeclaredNames 和 LexicallyDeclaredNames,TopLevelVarDeclaredNames 和 TopLevelLexicallyDeclaredNames
对应协议 8 Syntax-Directed Operations。
了解即可,为了更方便阅读协议,很多概念会在后续的章节涉及。
脚本分为模块脚本和普通脚本,看代码,不说话,你能懂。
普通脚本
javascript
复制代码
<script src="script.js"></script>
<script>
console.log("a");
</script>
模块脚本
javascript
复制代码
<script src="module_script.mjs" type="module"></script>
javascript
复制代码
export const APP_NAME = "巨无霸";
本系列讲普通脚本,有机会再谈模块脚本。
前置概念
Static Semantics
静态语义:指一组规则,这些规则用于在编译时或代码执行之前分析程序文本的结构和含义,无需考虑运行时的上下文或状态。
Parse Node
解析节点:代码的源文本最终会被解析成为解析树(Parse Tree),以节点(Parse Node)为根的树形结构,其中每个节点都是一个解析节点。
比如如下代码
javascript
复制代码
var a = 1;
会生成类似如下的结构, 有type
属性的,均是 Parse Node。
javascript
复制代码
{
"type": "Script",
"ScriptBody": {
"type": "ScriptBody",
"StatementList": [
{
"type": "VariableStatement",
"VariableDeclarationList": [
{
"type": "VariableDeclaration",
"BindingIdentifier": {
"type": "BindingIdentifier",
"name": "a"
},
"Initializer": {
"type": "NumericLiteral",
"value": 1
}
}
]
}
]
}
}
用在线工具 vtree 可以转为如下,是不是一棵树!
8.1 Runtime Semantics: Evaluation
ECMAScript代码在执行时的语义定义,最后返回一个完成记录( Completion Record)。
在代码层面,可以理解为执行代码,得到结果或者是异常。
8.2 Scope Analysis
作用域分析。ECMAScript代码进行静态分析,以确定变量和其他符号的可见性,这是解释和执行代码之前的重要步骤之一。
下面这些名词需要知道是什么意思,因为到全局代码和函数代码等申明初始化的时候,都会涉及到这些。
8.2.1 Static Semantics: BoundNames
静态语义: 绑定名。举几个例子:
javascript
复制代码
var a = 10; // a 是绑定名
let b = {}; // b 是绑定名
function fn1(){} // fn1 是绑定名
function fn2(param1, param2){ // fn2, param1, param2 是绑定名
}
const {a1, b1 } = {a1:1, b1:2} // a1,b1 是绑定名
const [val1] = [1,2] // val1 是绑定名
更多可以参见 8.2.1 Static Semantics: BoundNames, 那个语法不太好懂。 可以参见 文法基础。
8.2.2 Static Semantics: DeclarationPart
返回的申明的解析节点 Parse Node。 包含下面几种情况的对应 Parse Node。
不包含
var
申明。
javascript
复制代码
function fun(){}
async function asyncFunction(){}
function *generatorFunction(){}
async function * asyncGeneratorFunction(){}
class demoClass{}
let aaa = 1;
const bbb = 2 ;
var a = 10; // 不包含其对应的解析节点
8.2.3 Static Semantics: IsConstantDeclaration
是不是常量申明。对应const
的申明。
javascript
复制代码
const a = 1;
8.2.4 Static Semantics: LexicallyDeclaredNames
词法申明的名字(列表),用来确定在该作用域内通过 let、const、或 class 关键字声明的所有变量或类的名称集合。其是包含 顶层代码词法申明名字(列表) TopLevelLexicallyDeclaredNames 场景的。
注意这句话:
脚本的顶层代码,函数申明被当做 var 申明,而不是词法申明。
顶层代码的函数申明是和var申明等同的,关键在于这个顶层代码
顶层代码包含
- 脚本顶层代码
- 函数顶层代码
- class 的静态代码块
红色圈出的三块都是顶层代码。
非顶层代码 (有block块)
javascript
复制代码
{
let a = 1;
const b = function (){};
class c{};
function d(){};
// 不包含
var e = 'e';
}
本示例代码, 进入 Block 块内,申明实例化时,如果获取LexicallyDeclaredNames的值,对应的就是 ['a','b','c', 'd']
。
不包含
- var 申明
包含
- let 申明
- const 申明
- class 申明
- function 申明
顶层代码
javascript
复制代码
let a = 1;
const b = function (){};
class c{};
// 不包含
function d(){};
// 不包含
var e = 'e';
不包含
- var 申明
- function 申明
包含
- let 申明
- const 申明
- class 申明
8.2.5 Static Semantics: LexicallyScopedDeclarations
和 LexicallyDeclaredNames 类似, 不过这里返回的是 解析节点( Parse Node)。
比如下面的普通脚本代码,返回的是 [ParseNodeA,ParseNodeB,ParseNodeC]
javascript
复制代码
{
let a = 1;
const b = function (){};
class c{};
function d(){};
// 不包含
var e = 'e';
}
类型和源码的信息如下
8.2.6 Static Semantics: VarDeclaredNames
var申明的名字(列表)。 包含 TopLevelLexicallyDeclaredNames 场景。
以下面的普通脚本代码为例:
- 当代码执行到
debugger
的时候,当前上下文的 VarDeclaredNames 是 ['a', 'b'] - 当执行到
console.log('init')
的时候,函数init的执行上下文的 VarDeclaredNames 是 ['c', 'h', 'd', 'e', 'fn'], 不包含块内的函数申明 ['f', 'g']。
javascript
复制代码
var a = 1;
var b = function (){};
debugger
; (function init(){
for(var c =1; c<100; c++){
function f(){}
var h = 'h'
}
for(var d =1; d<100; d++){
var e = 10;
}
{
function g(){}
}
function fn(){}
console.log('init');
})()
这里有些同学可能会有些疑惑,为啥 包含var申明h
, 而不包含 函数申明 'f', 'g'。
这里有很重要的一条规则:块内的函数申明不属于顶层代码,知道这点就能理解了。
至于为什么 包含 h
, VarDeclaredNames 静态语义解析时, 会查找 for 语句后面的 Statment中包含的var申明,知道这个逻辑即可。
8.2.7 Static Semantics: VarScopedDeclarations
和 VarDeclaredNames 类似, 下面的普通脚本代码返回的是 解析节点( Parse Node)。包含TopLevelVarScopedDeclarations 场景。
javascript
复制代码
var a = 1;
var b = function (){};
for(var c =1; c<100; c++){}
for(var d =1; d<100; d++){
var e = 10;
}
function fn(){}
节点和源码的对象关系如下:
细心的同学发现这个与LexicallyScopedDeclarations 相比,sourceText里面未出现 var 关键字,至于原因,有兴趣的同学可以进行探讨。
8.2.8 Static Semantics: TopLevelLexicallyDeclaredNames
顶级词法申明的名字列表。这和之前的 LexicallyDeclaredNames 关联密切。
这里的 对TopLevel
理解是很重要的,你觉得下面的代码会是 TopLevel
javascript
复制代码
<script>
var varA = 'varA';
function log(){
var varLogPrefix = 'log';
console.log(varLogPrefix, ...arguments);
}
class ClassA {
static {
var varC = 'varC';
}
}
<script>
实际上,三块红色标出的都是 TopLevel
,
- 普通脚本的顶层代码
- 各种函数内部的顶层代码
- class的静态块
在普通脚本和函数的顶层, 函数申明会被当做变量申明对待。这在协议的 8.2.4 Static Semantics: LexicallyDeclaredNames,8.2.8 Static Semantics: TopLevelLexicallyDeclaredNames,8.2.10 Static Semantics: TopLevelVarDeclaredNames 部分都有提到。
所以 TopLevelLexicallyDeclaredNames 其不包含
- var 申明
- function 申明 (全局代码执行的时候, function 申明等同var申明)
包含
- let 申明
- const 申明
- class 申明
javascript
复制代码
function fun(){} // 不包含
function asyncFunction(){} // 不包含
function *generatorFunction(){} // 不包含
async function * asyncGeneratorFunction(){} // 不包含
class demoClass(){}
let aaa = 1;
const bbb = 2
在函数或脚本的顶层,函数声明被视为 var 声明,而不是词法声明。
模块代码(Module)
这时候就有些区别了,函数申明是作为词法申明的。
8.2.9 Static Semantics: TopLevelLexicallyScopedDeclarations
和 TopLevelLexicallyDeclaredNames 功能相似,只不过返回的是 Parse Node。
8.2.10 Static Semantics: TopLevelVarDeclaredNames
顶层代码var申明的名字List。和之前的 VarDeclaredNames 有区别,顶级的包含可提升的函数申明的名字。
例如如下返回 ['a','b', 'c' , 'fun', 'asyncFunction', 'generatorFunction', 'asyncGeneratorFunction']。
javascript
复制代码
var a = 1;
var b = function (){};
for(var c =1; c<100; c++){}
function fun(){}
function asyncFunction(){}
function *generatorFunction(){}
async function * asyncGeneratorFunction(){}
// 不包含如下
let c = 'c';
cosnt d = 'd';
class e {}
这里你要和 TopLevelLexicallyDeclaredNames 关联起来,她那边不包含的,这边被包含了。
8.2.11 Static Semantics: TopLevelVarScopedDeclarations
和 TopLevelVarDeclaredNames 类似,不过返回的是 Parse Node。
杂项
- 可提升的申明 (HoistableDeclaration): 实际上就是各种函数申明
- Statement :语句
- Declaration:申明