24

网上有很多文章讲到了javascript词法环境以及执行环境,但是大多数都是说的ES5时期的词法环境,很少是提到了ES6以及最新的ES8中有关词法环境的介绍。相比ES5,ES6以及之后的规范对词法环境有了不一样的说明,甚至在词法环境之外新增了领域(Realms)、作业(Jobs)这两全新概念。这导致我在阅读ES8的规范时遇到了不少问题,虽然最后都解决了,但为此付出不少时间。所以我在这专门把我对词法环境以及领域的理解写出了。我希望通过这篇文章能对正在了解这一方面或对javascript有兴趣的人有所帮助。好了,废话不多说了,开始进入正题。

词法环境(Lexical Environments)

官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。通常,词法环境与ECMAScript代码的特定语法结构相关联,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,并且每次执行这样的代码时都会创建新的词法环境。
环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。它被称为词法环境的环境记录。环境记录也是一种规范类型。规范类型对应于在算法中用来描述ECMAScript语言结构和ECMAScript语言类型的语义的元值。
全局环境是一个没有外部环境的词法环境。全局环境的外部环境引用为null。
模块环境是一个包含模块顶层声明绑定的词法环境。模块环境的外部环境是一个全局环境。
函数环境是一个对应于ECMAScript函数对象调用的词法环境。
上面这些话是官方的说明,我只是稍微简单的翻译了一下(原谅我英语学的不好,都是谷歌的功劳)。
可能光这么说一点都不形象,我举个例子:

var a,b=1;
function foo(){
   var a1,b1;
};
foo();

看上面这一简单的代码,js在执行这段代码的时候做了如下操作:

  1. 创建了一个词法环境我把它记为LE1(这里的LE1其实是一个global environment)。
  2. 确定LE1的环境记录(我在这不细说环境记录,只知道它里面包含了{a,b,foo}标识符的记录,我会在之后详细介绍)。
  3. 设置外部词法环境引用,因为LE1已经在最外面了,于是外部词法环境引用就是null,到此LE1就确立完毕了。
  4. 接着执行代码,当执行到foo()这句话时,js调用了foo函数。此时foo函数是一个FunctionDeclaration,于是js开始执行foo函数。
  5. 创建了一个新的词法环境记为LE2.
  6. 设置LE2的外部词法环境引用,很明显LE2的外部词法环境引用就是LE1
  7. 确定LE2的环境记录{a1,b1} 。
  8. 最后继续执行foo函数,知道函数执行完毕。

注意:所有创建词法环境以及环境记录都是不可见的,编译器内部实现。

用图简单解释一下LE1LE2的关系就是如下:
图画的真是丑

上面的步骤都是简化步骤,当讲解完之后的环境记录、领域、执行上下文、作业时,我会给出一个详细的步骤。

环境记录(Environment Record)

ES8规范中主要使用两种环境记录值:声明性环境记录和对象环境记录。环境记录是一个抽象类,它具有三个具体的子类,分别是声明式环境记录,对象环境记录和全局环境记录。其中全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合。

对象环境记录(Object Environment Record)

每个对象环境记录都与一个对象联系在一起,这个对象被称为绑定对象(binding object)。一个对象环境记录绑定一组字符串标识符名称,直接对应于其绑定对象的属性名称。无论绑定对象自己的和继承的属性的[[Enumerable]]设置如何,它们都包含在集合中。由于可以动态地从对象中添加和删除属性,因此对象环境记录绑定的一组标识符可能会因为任何添加或删除对象属性操作的副作用而改变。即使相应属性的Writable的值为false。因此由于这种副作用而创建的任何绑定都将被视为可变绑定。对象环境记录不存在不可变的绑定。
with语句用到的就是对象环境记录,我们看一下简单的例子:

var withObject={
    a:1,
    foo:function(){
        console.log(this.a);
    }
}

with(withObject){
    a=a+1;
    foo();                    //2
}

在js代码执行到with语句的时候,

  1. 创建新的词法环境。
  2. 接着创建了一个对象环境记录即为OEROER包含withObject这个绑定对象,OER中的字符串标识符名称列表为withObject中的属性«a,foo»,在with语句中的变量操作默认在绑定对象中的属性中优先查找。
  3. OER设置外部词法环境引用。

注意:对象环境记录不是指Object里面的环境记录。普通的Object内部不存在新的环境记录,它的环境记录就是定义该对象所在的环境记录。

声明性环境记录(Declarative Environment Record)

每个声明性环境记录都与包含变量,常量,let,class,module,import和/或function的声明的ECMAScript程序作用域相关联。声明性环境记录绑定了包含在其作用域内声明定义的标识符集。这句话很好理解,举个例子如下:

import x from '***';
var a=1;
let b=1;
const c=1;
function foo(){};
class Bar{};
//这时声明性环境记录中就有了«x,a,b,c,foo,Bar»这样一组标识符,当然实际存放的结构肯定不是这个样子的,还要复杂。

函数环境记录(Function Environment Record)

函数环境记录是一个声明性环境记录,它用来表示function中的顶级作用域,此外如果函数不是一个箭头函数(ArrowFunction),则为这个函数提供一个this绑定。如果一个函数不是一个ArrowFunction函数并引用了super,则它的函数环境记录还包含从该函数内执行super方法调用的状态。
函数环境记录有下列附加的字段

字段名称 含义
[[ThisValue]] Any 用于该函数调用的this值
[[ThisBindingStatus]] "lexical" ,"initialized" ,"uninitialized" 如果值是“lexical”,这是一个ArrowFunction,并且没有一个本地的this值。
[[FunctionObject]] Object 一个函数对象,它的调用导致创建该环境记录
[[HomeObject]] Object或者undefined 如果关联的函数具有super属性访问权限,并且不是一个ArrowFunction,则[[HomeObject]]是该函数作为方法绑定的对象。 [[HomeObject]]的默认值是undefined。
[[NewTarget]] Object或者undefined 如果该环境记录是由[[Construct]]的内部方法创建的,则[[NewTarget]]就是[[Construct]]的newTarget参数的值。否则,它的值是undefined。

我简单介绍一下这些字段,[[ThisValue]]这个字段的值就是函数中的this对象,[[ThisBindingStatus]]中"initialized" ,"uninitialized"看字面意思也知道了,主要是“lexical”这个状态为什么是代表ArrowFunction,我的理解是ArrowFunction中是没有一个本地的this值,所以ArrowFunction中的this引用不是指向调用该函数的对象,而是根据词法环境进行查找,本地没有就向外部词法环境中查找this值,不断向外查找,直到查到this值,所以[[ThisBindingStatus]]的值是“lexical”。看下面例子:

var a = 'global.a';
var obj1 = {
    a:'obj1.a',
    foo: function(){
     console.log(this.a);
    }
}
var obj2 = {
    a:'obj2.a',
    arrow:()=>{
     console.log(this.a);
    }
}
obj1.foo()                  //obj1.a
obj2.arrow()                //global.a不是obj2.a
obj1.foo.bind(obj2)()       //obj2.a
obj2.arrow.bind(obj1)()     //global.a  强制绑定对ArrowFunction没有作用

对ArrowFunction中this的有趣的说法就是:我没有this,你送我个this我也不要,我就喜欢拿别人的this用,this还是别人的好。
[[FunctionObject]]:在上一个例子中指得就是obj1.foo、obj1.arrow。
[[HomeObject]]:只有函数有super访问权限且不是ArrowFunction才有值。看个MDN上的例子:

var obj1 = {
  method1() {
      console.log("method 1");
  }
}

var obj2 = {
  method2() {
      super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2();                          //method 1

//在这里obj2就是[[HomeObject]]
//注意不能这么写:
var obj2 = {
  foo:function method2() {
      super.method1();                 //error,function定义下不能出现super关键字,否则报错。
  }
}                 

[[NewTarget]]:构造函数才有[[Construct]]这个内部方法,如用new关键词调用的函数就会有[[Construct]],newTarget参数我们可以通过new.target在函数中看到。

function newTarget(){
   console.log(new.target);
}

newTarget()             //undefined
new newTarget()         /*function newTarget(){
                              console.log(new.target);
                        }
                        new.target指代函数本身*/

全局环境记录(Global Environment Records)

全局环境记录用于表示在共同领域(Realms)中处理所有共享最外层作用域的ECMAScript Script元素。全局环境记录提供了内置全局绑定,全局对象的属性以及所有在脚本中发生的顶级声明。
全局环境记录有下表额外的字段。

字段名称 含义
[[ObjectRecord]] Object Environment Record 绑定对象是一个全局对象。它包含全局内置绑定以及关联领域的全局代码中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定。
[[GlobalThisValue]] Object 在全局作用域内返回的this值。宿主可以提供任何ECMAScript对象值。
[[DeclarativeRecord]] Declarative Environment Record 包含在关联领域的全局代码中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定之外的所有声明的绑定
[[VarNames]] List of String 关联领域的全局代码中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration声明绑定的字符串名称。

这里提一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,这也解释了为什么在全局代码中用var、function声明的变量自动的变为全局对象的属性而let、const、class等声明的变量却不会成为全局对象的属性。

模块环境记录(Module Environment Records)

模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部作用域。除了正常的可变和不可变绑定之外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另一个环境记录中存在的目标绑定。

领域(Realms)

在执行ECMAScript代码之前,所有ECMAScript代码都必须与一个领域相关联。从概念上讲,一个领域由一组内部对象,一个ECMAScript全局环境,在该全局环境作用域内加载的所有ECMAScript代码以及其他相关的状态和资源组成。通俗点讲领域就是老大哥,在领域下的小弟都必须等大哥把事情干完才能做。领域被表示为领域记录(Realm Record),有下表的字段:

字段名称 含义
[[Intrinsics]] 一个记录,它的字段名是内部键,其值是对象 与此领域相关的代码使用的内在值。
[[GlobalObject]] Object 这个领域的全局对象。
[[GlobalEnv]] Lexical Environment 这个领域的全局环境。
[[TemplateMap]] 一个记录列表 { [[Strings]]: List, [[Array]]: Object}. 模板对象使用Realm Record的[[TemplateMap]]分别对每个领域进行规范化。
[[HostDefined]] Any, 默认值是undefined. 保留字段以供需要将附加信息与Realm Record关联的宿主环境使用。

[[Intrinsics]]:我举几个在[[Intrinsics]]中对你来说很熟悉的字段名%Object%(Object构造器),%ObjectPrototype%(%Object%的原型数据属性的初始值),相似的有%Array%(Array构造器),%ArrayPrototype%、%String%、%StringPrototype%、%Function%、%FunctionPrototype%等等的内部方法,可以说全局对象上的属性和方法的值基本都是从[[Intrinsics]]来的(不包括宿主环境提供的属性和方法如:console、location等)。想查看所有的内部方法请查看官方文档内部方法列表

[[GlobalObject]]和[[GlobalEnv]]一目了然,在浏览器中[[GlobalObject]]就是值window了,node中[[GlobalObject]]就是值global。[[HostDefined]] 值宿主环境提供的附加信息。我在这重点说一下[[TemplateMap]]。

[[TemplateMap]]

[[TemplateMap]]是模板在领域中的存储信息,每个模板文字在领域中对应一个唯一的模板对象。具体的模板存储方式我简单说明一下:
在js中模板是用两个反引号(`)进行引用;在js进行解析时模板文字被解释为一系列的Unicode代码点。,具体看如下例子:

var tpObject = {name:'fqf',desc:'programmer'};
var template=`My name is${tpObject.name}. I am a ${tpObject.desc}.`;
//根据模板语法这个模板分三个部分组成:
//TemplateHead:(`My name is${),TemplateMiddle:(}. I am a ${),TemplateTail:(}.)
//tpObject.name,tpObject.desc是表达式,不存储在模板中。
//其中如果模板文字是纯字符串,则这是个NoSubstitutionTemplate。
//js是按顺序解析模板文字,其中`、${、} ${、}、`被认为是空的代码单元序列。
//模板文字被解析成TV(模板值),TRV(模板原始值),它们之间的区别在于TRV中的转义序列被逐字解释,如果你的模板中不带有(\)转义符,你可以认为TV与TRV是一样的。
//具体字符对应的编码存储你可以先对字符做charCodeAt(0),然后通过toString(16)转化为16进制,你就知道对应的编码单元了。

//比如字符a
('a').charCodeAt(0).toString(16);              //61,对应编码就是0x0061

模板文字变成Unicode代码点后,会将Unicode代码点分段存入List,按TemplateHead,TemplateMiddleList,TemplateTail顺序存入(TemplateMiddleList是多个TemplateMiddle组成的顺序列表),具体表示可以是这样«TemplateHead,TemplateMiddle1,TemplateMiddle2,...,TemplateTail»。了解这个之后再来看模板信息具体是如何存入Realms的[[TemplateMap]]中的,步骤如下:

  1. 让rawStrings成为模板按TRV进行解析返回的结果。
  2. 让cookedStrings成为模板按TV进行解析返回的结果。
  3. 让count成为cookedStrings这个List中的元素数量。
  4. 让template成为ArrayCreate(count)。(ArrayCreate)是js用来创建数组的内部方法
  5. 让rawObj成为ArrayCreate(count)。
  6. 让index=0。
  7. 循环,while index<count

    1. 让prop成为ToString(index)。
    2. cookedValue成为cookedStrings[index]。
    3. 调用template.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    4. 让rawValue成为rawStrings[index]。
    5. 调用rawObj.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    6. 让index=index+1。
  8. 冻结rawObj,类似于调用了Object.frozen(rawObj)。
  9. 调用template.[[DefineOwnProperty]]("raw", PropertyDescriptor{[[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false})。
  10. 冻结template。
  11. 添加Record{[[Strings]]: rawStrings, [[Array]]: template}到领域的[[TemplateMap]]中。

每个模板都对应一个唯一且不可变的模板对象,每次获取模板对象都是先从Realms中寻找,如果有返回模板对象,如果没有按上面步骤添加到领域中,再返回模板对象。
所以下列tp1和tp2模板其实对应的是同一个模板对象:

var template='template';
var othertemplate='othertemplate';
var tp1=`This is a ${template}.`;
var tp2=`This is a ${othertemplate}.`;

注:我不是很清楚为什么要把模板信息存入[[TemplateMap]]中,可能是考虑性能的原因。如果有了解这方面的,希望能留言告知。

想进一步了解TV(模板值)和TRV(模板原始值)的不同请戳这里查看具体说明。
到这里领域的描述就告一段落了。开始进入执行上下文也称执行环境的讲解了。

执行上下文(Execution Contexts)

执行上下文是一种规范设备,通过ECMAScript编译器来跟踪代码的运行时评估。在任何时候,每个代理(agent)最多只有一个正在执行代码的执行上下文。这被称为代理的运行执行上下文(running execution context)。本规范中对正在运行的执行上下文(running execution context)的所有引用都表示周围代理的正在运行的执行上下文(running execution context)。
这看起来有点混乱,在这里需要明白一个东西:执行上下文不是表示正在执行的上下文,你可以把它看成一个名词就比较好理解了。
执行上下文栈用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶层元素。每当从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关的可执行代码时新的执行上下文被创建。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
用代码加步骤说明:

1. var a='running execution context';
2. function foo(){
3.     console.log('new running execution context');4.
4. }
5.
6. foo();
7. console.log(a);

我把全局的执行上下文记为ec1,
我把foo函数的执行上下文记为ec2,
执行上下文栈记为recList;
正在运行的执行上下文rec

  1. 首先recList是空的,rec=recList[0]。
  2. 运行全局代码时ec1被创建,并unshift到recList中,recList=[ec1],rec=recList[0]。
  3. 当执行到第6句,进入foo函数里时,ec2被创建并unshift到recList中,recList=[ec2,ec1],rec=recList[0]。
  4. foo函数执行完毕,recList.shift(),ec2从recList中删除,recList=[ec1],rec=recList[0]。
  5. 到第7句执行完毕,ec1从recList中删除,recList又变为空了,rec=recList[0]。

在这里我们可以看到执行上下文之间的转换通常以堆栈式的后进/先出(LIFO)方式进行。
所有执行上下文都有下表的组件:

组件 含义
代码评估状态 任何需要去执行,暂停和恢复与此执行上下文相关的代码评估状态。
Function 如果这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。如果上下文正在评估脚本或模块的代码,则该值为空。
Realm 关联代码访问ECMAScript资源的领域记录。
ScriptOrModule 模块记录(Module Record)或脚本记录(Script Record)相关代码的来源。如果不存在来源的脚本或模块,则值为null。

正在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具有下表列出的其他状态组件。

组件 含义
LexicalEnvironment 标识在此执行上下文中用于解析有代码所做的标识符引用的词法环境。
VariableEnvironment 标识在此执行上下文中的词法环境,它的环境记录保存了由VariableStatements创建的绑定。

当创建执行上下文时,它的LexicalEnvironment和VariableEnvironment组件最初具有相同的值。

作业和作业队列(Jobs and Job Queues)

作业和领域一样都是ES6新增的东西。作业是一个抽象操作,当没有其他ECMAScript计算正在进行时,它将启动ECMAScript计算。一个作业抽象操作可以被定义为接受任意一组作业参数。只有当没有正在运行的执行上下文并且执行上下文堆栈为空时,才能启动作业的执行。一旦启动了一个作业的执行,作业将始终执行完成。在当前正在运行的作业完成之前,不能启动其他作业。PendingJob是未来执行Job的请求。PendingJob是内部记录,其字段如下表:

字段名称 含义
[[Job]] 作业抽象操作的名称 这是在执行此PendingJob时执行的抽象操作。
[[Arguments]] 一个List 当[[Job]]激活时要传递给[[Job]]的参数值的列表。
[[Realm]] 一个领域记录 此PendingJob启动时,最初执行上下文的领域记录。
[[ScriptOrModule]] 一个Script Record或Module Record 此PendingJob启动时,用于初始执行上下文的脚本或模块。
[[HostDefined]] any,默认undefined 保留字段供需要将附加信息与 pending Job相关联的宿主环境使用。

我们可以把[[Job]]看成一个函数,[[Arguments]]是这个函数的参数。
一个作业队列是一个PendingJob记录的FIFO队列。每个作业队列都有一个名称和由ECMAScript编译器定义的一整套可用的作业队列。每个ECMAScript编译器至少具有下表中定义的作业队列。

名称 目的
ScriptJobs 验证和评估ECMAScript脚本和模块源文本的作业。
PromiseJobs 回应一个承诺的解决的作业

Promise的回调就是与PromiseJobs有关。

执行流程

有关javascript中词法环境、领域、执行上下文以及作业,基本简单的介绍了一下。那么ECMAScript编译器怎么把它们之间关联起来的呢,下面我大致写了一个简单的流程:
ECMAScript中有一个RunJobs ( )方法,所有东西的确立都是从这个方法出来的。

  1. 让realm成为CreateRealm()。CreateRealm()主要是创建了一个领域,初始化了领域中字段的值,并返回创建的领域。
  2. 让newContext成为一个新的执行上下文。
  3. 设置newContext的Function为null,newContext的Realm为realm,newContext的ScriptOrModule为null。
  4. 把newContext放到执行上下文栈,现在newContext是一个正在运行的执行上下文。
  5. 执行SetRealmGlobalObject(realm, global, thisValue)方法,正常情况下global为undefined,thisValue为undefined。

    • SetRealmGlobalObject方法执行,我在这里默认global和thisValue为undefined:
    1. 让intrinsics成为realmRec.[[Intrinsics]]。
    2. 让globalObj等于ObjectCreate(intrinsics.[[%ObjectPrototype%]])。
    3. 让thisValue等于globalObj。
    4. 设置realmRec.[[GlobalObject]]是globalObj。
    5. 设置newGlobalEnv为新的词法环境。
    6. 让objRec成为一个新的包含globalObj为绑定对象的对象环境记录。
    7. 让dclRec成为没有任何绑定的新的声明性环境记录。
    8. 让globalRec成为一个新的全局环境记录。
    9. 设置globalRec.[[ObjectRecord]]为objRec,设置globalRec.[[GlobalThisValue]]为 thisValue,设置globalRec.[[DeclarativeRecord]]为dclRec,设置globalRec.[[VarNames]]是一个空的List,设置newGlobalEnv的环境记录为globalRec,newGlobalEnv的外部词法环境为null。
  6. 设置realmRec.[[GlobalEnv]]为newGlobalEnv。
  7. 让globalObj变为SetDefaultGlobalBindings(realm)得返回值。SetDefaultGlobalBindings的方法主要是把realm的[[Intrinsics]]中的内部方法拷贝到全局对象中。
  8. 在globalObj上创建任何编译器定义的全局对象属性。
  9. 依赖编译器方式,在零个或多个ECMAScript脚本和/或ECMAScript模块中获取ECMAScript源文本和任何关联的host-defined的值。为每一个sourceText和hostDefined做如下操作:

    1. 如果sourceText是script的源代码, 那么执行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »)。
    2. 如果sourceText是module的源代码,那么执行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)。
  10. 循环

    1. 挂起正在运行的执行上下文并将其从执行上下文堆栈中移除。
    2. 确定:执行上下文堆栈现在是空的。
    3. 让nextQueue是以编译器定义的方式选择的非空作业队列。如果所有作业队列都为空,则结果是编译器定义的,nextQueue里的记录是上面通过EnqueueJob方法放到作业队列中的记录。
    4. 让nextPending成为nextQueue前面的PendingJob记录。从nextQueue中删除该记录。
    5. 让newContext成为一个新的执行上下文。
    6. 设置newContext的Function为null,newContext的Realm为nextPending.[[Realm]],newContext的ScriptOrModule为nextPending.[[ScriptOrModule]]。
    7. 将newContext推入执行上下文堆栈; newContext现在是正在运行的执行上下文。
    8. 使用nextPending执行任何编译器或宿主环境定义的作业初始化。
    9. 让result成为使用nextPending.[[Arguments]]元素作为nextPending.[[Job]]的参数进行抽象操作的结果,这里指运行上面EnqueueJob中的ScriptEvaluationJob或TopLevelModuleEvaluationJob方法。
    10. 如果result是突然完成的,比如throw扔出异常, 执行HostReportErrors(« result.[[Value]] »),HostReportErrors方法就是报错误的,比如SyntaxError和ReferenceError等。

2017-11-27新增
突然发现这么一长串的步骤不易阅读和理解,我在这做一些笼统的说明:

领域(Realm)只创建一次,领域创建后开始创建全局词法环境(包括全局词法环境中的声明性环境记录和对象环境记录以及全局对象),SetDefaultGlobalBindings方法中global和thisValue为undefined意味着全局环境记录中的[[GlobalThisValue]]就是全局对象(这也表示了在浏览器中全局环境下this就是window对象)。

步骤9中的script中的sourceText表示用<script></script>引入的js代码的Unicode编码。EnqueueJob方法你可以认为是把脚本信息按执行顺序放到队列中。

步骤10,你可以认为是从队列中拿出脚本进行执行(该循环的第9步就是执行脚本(指ScriptEvaluationJob方法),脚本的执行都是在领域和全局词法环境创建之后的)。


我这里说一下ScriptEvaluationJob方法的执行过程(TopLevelModuleEvaluationJob方法只在评估module时运行)正常都是运行的ScriptEvaluationJob方法。
ScriptEvaluationJob ( sourceText, hostDefined ):

  1. 确定: sourceText是ECMAScript源文本。
  2. 让realm成为当前的领域记录。
  3. 让s成为ParseScript(sourceText, realm, hostDefined)。
  4. 如果s是一个errors列表, 那么执行HostReportErrors(s),返回 NormalCompletion(undefined)(一个完成记录值,值为undefined)。
  5. 返回ScriptEvaluation(s)。

ParseScript(sourceText, realm, hostDefined):

  1. 使用脚本解析sourceText作为目标符号,并分析任何早期错误条件的解析结果。如果解析成功并且没有发现早期错误,那么让body成为所得到的分析树,否则body是一个包含一个或多个早期错误的列表。
  2. 如果body是错误列表,则返回body。
  3. 返回脚本记录(Script Record){[[Realm]]: realm, [[Environment]]: undefined, [[ECMAScriptCode]]: body, [[HostDefined]]: hostDefined}。

早期错误有很多,我举个例子:使用关键词作为标识符就是典型的早期错误。

ScriptEvaluation ( scriptRecord )大致流程:

  1. 让globalEnv成为scriptRecord.[[Realm]].[[GlobalEnv]]。
  2. 让scriptCxt成为一个新的ECMAScript代码执行上下文。
  3. 设置scriptCxt的Function为null, scriptCxt的Realm为scriptRecord.[[Realm]],设置scriptCxt的ScriptOrModule为scriptRecord。
  4. 设置VariableEnvironment和LexicalEnvironment为scriptCxt的globalEnv
  5. 挂起当前正在运行的执行上下文。
  6. 把scriptCxt放到执行上下文栈中,scriptCxt是一个正在运行的执行上下文。
  7. 让scriptBody成为scriptRecord.[[ECMAScriptCode]]。
  8. 让result成为运行GlobalDeclarationInstantiation(scriptBody, globalEnv)返回的结果。
  9. 如果result.[[Type]]是normal,那么设置result是执行scriptBody的结果.
  10. 如果result.[[Type]]是normal且result.[[Value]]是empty, 那么设置result为NormalCompletion(undefined).
  11. 挂起scriptCxt并将其从执行上下文堆栈中删除。
  12. 将当前位于执行上下文堆栈顶部的上下文恢复为正在运行的执行上下文。
  13. 返回Completion(result),一个记录值。

GlobalDeclarationInstantiation()方法是对全局环境中的标识符定义进行实例化。比如var、function、let、const、class声明的标识符。该方法执行成功返回的result.[[Type]]为normal。注意这时候的我们能看到的js代码还没有执行,真正执行我们的代码的是步骤9。这也是为什么我们用var和function声明的标识符会出现变量提升(Hoisting)现象。let、const、class声明也在步骤9之前,之所以没有变量提升是因为let、const、class声明的标识符只进行实例化而没有初始化,在下一篇文章中我会重点介绍它们之间的不同之处(所以我认为那些说var和function声明存在变量提升,而let、const、class声明的变量不提升的说法是不对的)。


2017-11-27新增
ScriptEvaluation你可以简单的认为它做了两件:1.对标识符实例化以及初始化,2.执行javascript脚本。

GlobalDeclarationInstantiation方法只对当前脚本的标识符定义进行实例化,不能跨脚本。比如script1在script2之前引用,那么script2中的声明的变量只有通过GlobalDeclarationInstantiation实例化后才能在script1中引用,这也表示var和function声明的标识符不能跨脚本进行变量提升。


结束语

到这里本篇文章也快结束了,本文章所有的说法都是以最新的ECMAScript的语言规范(ES8)为基础。希望这篇文章可以帮助大家更加深入的了解javascript,如果本文有不当之处请指出。还有我不得不吐槽一下ECMAScript的语言规范写得真是太不友好了,看得我心好累啊(说到底还是自己当初在英语课上睡觉的锅)。最后如果你想看ECMAScript的语言规范,那么第5章和第6章一定要看!一定要看!这是一个过来人的忠告。


fanqifeng
536 声望24 粉丝

javascript语言爱好者,喜欢发掘js中的特异之处。现阶段沉迷购书无法自拔。