网上有不少文章讲到了javascript词法环境以及执行环境,可是大多数都是说的ES5时期的词法环境,不多是提到了ES6以及最新的ES8中有关词法环境的介绍。相比ES5,ES6以及以后的规范对词法环境有了不同的说明,甚至在词法环境以外新增了领域(Realms)、做业(Jobs)这两全新概念。这致使我在阅读ES8的规范时遇到了很多问题,虽然最后都解决了,但为此付出很多时间。因此我在这专门把我对词法环境以及领域的理解写出了。我但愿经过这篇文章能对正在了解这一方面或对javascript有兴趣的人有所帮助。好了,废话很少说了,开始进入正题。javascript
官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。一般,词法环境与ECMAScript代码的特定语法结构相关联,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,而且每次执行这样的代码时都会建立新的词法环境。
环境记录记录了在其关联的词法环境做用域内建立的标识符绑定。它被称为词法环境的环境记录。环境记录也是一种规范类型。规范类型对应于在算法中用来描述ECMAScript语言结构和ECMAScript语言类型的语义的元值。
全局环境是一个没有外部环境的词法环境。全局环境的外部环境引用为null。
模块环境是一个包含模块顶层声明绑定的词法环境。模块环境的外部环境是一个全局环境。
函数环境是一个对应于ECMAScript函数对象调用的词法环境。
上面这些话是官方的说明,我只是稍微简单的翻译了一下(原谅我英语学的很差,都是谷歌的功劳)。
可能光这么说一点都不形象,我举个例子:html
var a,b=1; function foo(){ var a1,b1; }; foo();
看上面这一简单的代码,js在执行这段代码的时候作了以下操做:java
注意:全部建立词法环境以及环境记录都是不可见的,编译器内部实现。node
用图简单解释一下LE1和LE2的关系就是以下:算法
上面的步骤都是简化步骤,当讲解完以后的环境记录、领域、执行上下文、做业时,我会给出一个详细的步骤。数组
ES8规范中主要使用两种环境记录值:声明性环境记录和对象环境记录。环境记录是一个抽象类,它具备三个具体的子类,分别是声明式环境记录,对象环境记录和全局环境记录。其中全局环境记录在逻辑上是单个记录,可是它被指定为封装对象环境记录和声明性环境记录的组合。浏览器
每一个对象环境记录都与一个对象联系在一块儿,这个对象被称为绑定对象(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语句的时候,性能
注意:对象环境记录不是指Object里面的环境记录。普通的Object内部不存在新的环境记录,它的环境记录就是定义该对象所在的环境记录。this
每一个声明性环境记录都与包含变量,常量,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中的顶级做用域,此外若是函数不是一个箭头函数(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指代函数自己*/
全局环境记录用于表示在共同领域(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等声明的变量却不会成为全局对象的属性。
模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部做用域。除了正常的可变和不可变绑定以外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另外一个环境记录中存在的目标绑定。
在执行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]]是模板在领域中的存储信息,每一个模板文字在领域中对应一个惟一的模板对象。具体的模板存储方式我简单说明一下:
在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]]中的,步骤以下:
循环,while index<count
每一个模板都对应一个惟一且不可变的模板对象,每次获取模板对象都是先从Realms中寻找,若是有返回模板对象,若是没有按上面步骤添加到领域中,再返回模板对象。
因此下列tp1和tp2模板其实对应的是同一个模板对象:
var template='template'; var othertemplate='othertemplate'; var tp1=`This is a ${template}.`; var tp2=`This is a ${othertemplate}.`;
注:我不是很清楚为何要把模板信息存入[[TemplateMap]]中,多是考虑性能的缘由。若是有了解这方面的,但愿能留言告知。
想进一步了解TV(模板值)和TRV(模板原始值)的不一样请戳这里查看具体说明。
到这里领域的描述就告一段落了。开始进入执行上下文也称执行环境的讲解了。
执行上下文是一种规范设备,经过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
在这里咱们能够看到执行上下文之间的转换一般以堆栈式的后进/先出(LIFO)方式进行。
全部执行上下文都有下表的组件:
组件 | 含义 |
---|---|
代码评估状态 | 任何须要去执行,暂停和恢复与此执行上下文相关的代码评估状态。 |
Function | 若是这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。若是上下文正在评估脚本或模块的代码,则该值为空。 |
Realm | 关联代码访问ECMAScript资源的领域记录。 |
ScriptOrModule | 模块记录(Module Record)或脚本记录(Script Record)相关代码的来源。若是不存在来源的脚本或模块,则值为null。 |
正在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具备下表列出的其余状态组件。
组件 | 含义 |
---|---|
LexicalEnvironment | 标识在此执行上下文中用于解析有代码所作的标识符引用的词法环境。 |
VariableEnvironment | 标识在此执行上下文中的词法环境,它的环境记录保存了由VariableStatements建立的绑定。 |
当建立执行上下文时,它的LexicalEnvironment和VariableEnvironment组件最初具备相同的值。
做业和领域同样都是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 ( )方法,全部东西的确立都是从这个方法出来的。
执行SetRealmGlobalObject(realm, global, thisValue)方法,正常状况下global为undefined,thisValue为undefined。
依赖编译器方式,在零个或多个ECMAScript脚本和/或ECMAScript模块中获取ECMAScript源文本和任何关联的host-defined的值。为每个sourceText和hostDefined作以下操做:
循环
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 ):
ParseScript(sourceText, realm, hostDefined):
早期错误有不少,我举个例子:使用关键词做为标识符就是典型的早期错误。
ScriptEvaluation ( scriptRecord )大体流程:
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章必定要看!必定要看!这是一个过来人的忠告。