ECMAScript 语法导向操作

61 阅读6分钟

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

阅读建议

对应协议 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: LexicallyDeclaredNames8.2.8 Static Semantics: TopLevelLexicallyDeclaredNames8.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。

杂项

  • Statement :语句
  • Declaration:申明

引用

Lexical Scope in JavaScript – What Exactly Is Scope in JS?

Lexical Scope in JavaScript – Beginner's Guide