You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Doesn't inherit from anything.letdict=Object.create(null);console.log(dict.toString);// undefined
动态分配机制允许继承链的完全可变性,提供了改变委托对象的能力:
letprotoA={x: 10};letprotoB={x: 20};// Same as `let objectC = {__proto__: protoA};`:letobjectC=Object.create(protoA);console.log(objectC.x);// 10// Change the delegate:Object.setPrototypeOf(objectC,protoB);console.log(objectC.x);// 20
全局环境的记录就是对象环境记录的一个例子。这样的记录也有关联的绑定对象,该绑定对象会存储一些来自该记录的属性,但是不会存储来自其它记录的属性,反之亦然。绑定对象也可以被提供为 this 值。
// Legacy variables using `var`.varx=10;// Modern variables using `let`.lety=20;// Both are added to the environment record:console.log(x,// 10y,// 20);// But only `x` is added to the "binding object".// The binding object of the global environment// is the global object, and equals to `this`:console.log(this.x,// 10this.y,// undefined!);// Binding object can store a name which is not// added to the environment record, since it's// not a valid identifier:this['not valid ID']=30;console.log(this['not valid ID'],// 30);
classPoint{constructor(x,y){this._x=x;this._y=y;}getX(){returnthis._x;}getY(){returnthis._y;}}letp1=newPoint(1,2);letp2=newPoint(3,4);// Can access `getX`, and `getY` from// both instances (they are passed as `this`).console.log(p1.getX(),// 1p2.getX(),// 3);
// Generic Movable interface (mixin).letMovable={/** * This function is generic, and works with any * object, which provides `_x`, and `_y` properties, * regardless of the class of this object. */move(x,y){this._x=x;this._y=y;},};letp1=newPoint(1,2);// Make `p1` movable.Object.assign(p1,Movable);// Can access `move` method.p1.move(100,200);console.log(p1.getX());// 100
作为替代方案,mixin还可以应用在原型级,而不是像上例中那样在每个实例上。
为展示this值的动态性质,请思考以下例子,我们留给读者作为要解决的一个练习:
functionfoo(){returnthis;}letbar={
foo,baz(){returnthis;},};// `foo`console.log(foo(),// global or undefinedbar.foo(),// bar(bar.foo)(),// bar(bar.foo=bar.foo)(),// global);// `bar.baz`console.log(bar.baz());// barletsavedBaz=bar.baz;console.log(savedBaz());// global
varx=10;letfoo={x: 20,// Dynamic `this`.bar(){returnthis.x;},// Lexical `this`.baz: ()=>this.x,qux(){// Lexical this within the invocation.letarrow=()=>this.x;returnarrow();},};console.log(foo.bar(),// 20, from `foo`foo.baz(),// 10, from globalfoo.qux(),// 20, from `foo` and arrow);
constvm=require('vm');// First realm, and its global:constrealm1=vm.createContext({x: 10, console});// Second realm, and its global:constrealm2=vm.createContext({x: 20, console});// Code to execute:constcode=`console.log(x);`;vm.runInContext(code,realm1);// 10vm.runInContext(code,realm2);// 20
// Enqueue a new promise on the PromiseJobs queue.newPromise(resolve=>setTimeout(()=>resolve(10),0)).then(value=>console.log(value));// This log is executed earlier, since it's still a// running context, and job cannot start executing firstconsole.log(20);// Output: 20, 10
asyncfunctionlater(){returnawaitPromise.resolve(10);}(async()=>{letdata=awaitlater();console.log(data);// 10})();// Also happens earlier, since async execution// is queued on the PromiseJobs queue.console.log(20);// Output: 20, 10
// In the `index.html`:// Shared data between this agent, and another worker.letsharedHeap=newSharedArrayBuffer(16);// Our view of the data.letheapArray=newInt32Array(sharedHeap);// Create a new agent (worker).letagentSmith=newWorker('agent-smith.js');agentSmith.onmessage=(message)=>{// Agent sends the index of the data it modified.letmodifiedIndex=message.data;// Check the data is modified:console.log(heapArray[modifiedIndex]);// 100};// Send the shared data to the agent.agentSmith.postMessage(sharedHeap);
如下是worker的代码:
// agent-smith.js/** * Receive shared array buffer in this worker. */onmessage=(message)=>{// Worker's view of the shared data.letheapArray=newInt32Array(message.data);letindexToModify=1;heapArray[indexToModify]=100;// Send the index as a message back.postMessage(indexToModify);};
作者: Dmitry Soshnikov
原文地址:JavaScript. The Core: 2nd Edition
这是第二版JavaScript核心的概述讲稿,用于介绍ECMAScript编程语言及其运行时系统的核心组件。
受众:高级工程师、专家。
本文的第一版涵盖了JS语言的通用特性,使用的概念也主要来自旧版的ES3规范,并适当地参考了ES5和ES6(即ES2015)的一些变化。
从ES2015开始,ECMA-262规范修改了一些核心组件的描述和结构,引入了新的模型等等。在这个版本中,我们会关注更新的概念,更新的术语,但是仍然会保留在所有规范版本中都保持一致的最基本的JS结构。
本文还涵盖了ES2017+运行时系统。
让我们的讨论从ECMAScript最基础的概念——对象开始。
对象(Object)
ECMAScript是一门基于原型的面向对象编程语言,它以对象为核心概念。
我们来看一个基本的对象实例。一个对象的原型被内部属性 [[Prototype]] 所引用,并通过__proto__属性暴露给用户级代码。
对于如下代码:
其结构中带有两个显式的自有属性和一个隐式的__proto__属性,__proto__属性是对point的原型的引用:
图1:带有原型的基本对象
原型(Prototype)
每个对象被创建时都会接收到它的原型。如果原型没有显式设置,对象会接收到默认原型作为其继承对象。
原型可以通过__proto__属性或Object.create方法显式设置:
所有对象都可以被用作另一个对象的原型,而这个原型本身也有它自己的原型。如果一个原型对其原型有非null引用,依此类推,就称之为原型链。
图2:原型链
规则很简单:如果一个属性在对象自身找不到,就会尝试在原型中解析它;或者在原型的原型等等。直到整个原型链被解析完毕。
从技术上讲,这种机制被称为动态分配或委托。
而且,如果一个属性在最终的原型链中找不到,就返回undefined:
正如我们所见,默认对象实际上永远不会是空的——它总会从Object.prototype继承一些东西。如果要创建一个无原型的词典,我们必须显式将其原型设置为null:
动态分配机制允许继承链的完全可变性,提供了改变委托对象的能力:
以Object.prototype为例,我们看到同样的原型可以在多个对象之间共享。在这个原则的基础上,ECMAScript中实现了基于类的继承。让我们通过下面实例,看看JS中"类"概念的底层原理。
类(Class)
当多个对象共享相同的初始状态及行为时,它们就形成了一种分类(classification)。
假如我们有多个对象需要继承自同一个原型,我们自然会先创建这个原型,然后显式从新创建的对象继承它:
我们可以从以下图中看到它们的关系:
图3:共享原型
然而,这明显太笨重了。而类——作为一种语法糖(即在语义上做同样事情的构造,不过是以更好的语法形式)正是为解决这个而提出的,它允许用方便的模式创建这样的多个对象:
从技术上讲,一个类被表示为一对“构造函数+原型”。因此,构造函数创建对象,同时还自动为它新创建的实例设置原型。这个原型被存储在<ConstructorFunction>.prototype属性中。
可以显式使用构造函数。而且,在引入类的概念之前,JS开发者过去也没有更好的替代品(我们依然可以在互联网上找到很多这样的遗留代码):
虽然创建单层构造函数很容易,不过这种从父类继承的模式需要相当多的样板代码。目前这个样板是作为实现细节隐藏的,而这恰好就是在JavaScript创建类时背后发生的事情。
下面我们来看看对象及其类的关系:
图4:构造函数与对象的关系
上图表明,每个对象都有一个相关的原型。甚至构造函数(类)Letter也有它自己的原型Function.prototype。注意,这个Letter.prototype是Letter的实例(即a、b和z)的原型。
我们可以在ES3. 7.1 面向对象编程(OOP):通用理论这篇文章中找到有关面向过程编程(OPP)概念的详细讨论(包括基于类、基于原型等的详细描述)。
现在我们已经理解了ECMAScript对象之间的基本关系,下面我们深入看看JS运行时系统。我们将会看到,那里几乎所有东西都可以被表示为对象。
执行上下文(Execution context)
为执行JS代码,并跟踪其运行时求值,ECMAScript规范定义了执行上下文的概念。从逻辑上讲,执行上下文是用栈(接下来将会看到的执行上下文栈的简写)来维护的,这里的栈与调用栈这个通用概念相对应。
ECMAScript代码有几种类型:全局代码(the global code)函数代码(function code),eval代码(eval code)和模块代码(module code);每种代码都是在其执行上下文中求值。不同的代码类型及其对应的对象可能会影响执行上下文的结构:比如,generator函数将其generator对象保存在上下文中。
下面我们来看一个递归函数调用:
当函数被调用时,就创建了一个新的执行上下文,并被压到栈中 —— 此时,它变成一个活动的执行上下文。当函数返回时,其上下文被从栈中弹出。
调用另一个上下文的上下文被称为调用者(caller)。被调用的上下文相应地被称为被调用者(callee)。在我们的例子中,recursive函数在递归调用它本身时,同时扮演了这两个角色:既是调用者,又是被调用者。
对于上面的例子,有如下的栈“压入-弹出”变动图:
图5:执行上下文栈
从图中我们还可以看到,全局上下文(Global context)总是在栈的底部,它是在所有其它上下文执行之前创建的。
你可以在相应的章节找到关于执行上下文的更多细节。
通常,一个上下文的代码会一直运行到结束,不过正如我们上面提到过的,有些对象,比如generator,可能会违反栈的LIFO顺序。一个generator函数可能会挂起它正在执行的上下文,并在结束前将其从栈中删除。一旦generator再次激活,它上下文就被恢复,并再次压入栈中:
这里的yield语句将值返回给调用者,并弹出上下文。在第二个next调用时,同一个上下文被再次压入栈中,并恢复。这样的上下文可能会比创建它的调用者活得长,所以会违反LIFO结构。
下面我们要讨论执行上下文最重要的部分;特别是我们将会看到ECMAScript运行时如何管理变量存储以及由嵌套代码块创建的作用域。这就是词法环境的通用概念,它用来在JS中存储数据,并用闭包的机制解决 “Funarg问题”。
环境(Environment)
每个执行上下文都有一个相关联的词法环境。
所以,环境就是定义在一个作用域中的变量、函数和类的仓库(storage)。
从技术上讲,环境是由环境记录(Environment Record)(一个将标识符映射到值的实际存储表格)以及对父环境的引用(可能是null)组成的一对。
对于如下代码:
全局上下文以及foo函数的上下文的环境结构看起来会像下面这样:
图6:环境链
在逻辑上讲,这会让我们想起了上面已经讨论过的原型链。而标识符解析的规则是很相似的:如果一个变量在自己的环境中找不到,就试着在父环境、父环境的父环境中查找它,依此类推,直到整个环境链查找完毕。
这就解释了为什么变量x被解析为100,而不是10?因为它是直接在foo的自身环境中找到的;为什么可以访问参数z?因为它也是只存储在激活环境(activation environment)中;为什么我们还可以访问变量y?因为它是在父环境中找到的。
与原型类似,同一个父环境可以被几个子环境共享:比如,两个全局函数共享同一个全局环境。
环境记录根据类型而有所不同。有对象环境记录(object environment records)和声明式环境记录(declarative environment records)。在声明式记录之上,还有函数环境记录(function environment records)和模块环境记录(module environment records)。每种类型的记录都有其特定的属性。不过,标识符解析的通用机制对于所有环境都是一致的,且不依赖于记录的类型。
全局环境的记录就是对象环境记录的一个例子。这样的记录也有关联的绑定对象,该绑定对象会存储一些来自该记录的属性,但是不会存储来自其它记录的属性,反之亦然。绑定对象也可以被提供为 this 值。
以上代码可以用下图来描述:
图7:绑定对象
注意,绑定对象的存在是为了覆盖(诸如var声明和with语句等)遗留结构,这些结构也将它们的对象作为绑定对象提供。这些是环境被表示为简单对象时的历史原因。当前的环境模型更加优化,不过结果是我们再也不能将绑定当作属性来访问了。
我们已经看到环境是如何通过父链接关联。下面我们将看到环境如何比创建它的上下文存活得更久,这是我们将要讨论的闭包机制的基础。
闭包(Closure)
ECMAScript中函数是头等对象。这个概念是函数式编程的基础,而JavaScript是支持函数式编程的。
与头等函数概念相关的是我们称之为“Funarg问题”(或者“函数式实参问题”)。这个问题在函数不得不处理自由变量时候出现。
下面我们来看看Funarg问题,看看在ECMAScript中如何解决这个问题。
考虑如下的代码段:
对于函数foo,变量 x 就是自由变量。当foo函数被激活时(通过形参funArg),它应该在哪里解析x绑定呢?是从创建函数的外层作用域,还是从调用者作用域,还是从函数被调用的地方?正如我们看到的,调用者bar函数也提供了对x的绑定,其值为20。
上面描述的用例称为向下funarg问题,即在确定绑定的正确环境时的不确定性:它应该是创建时的环境,还是调用时的环境?
这可以通过使用静态作用域来解决,静态作用域即创建时的作用域。
静态作用域有时也称为词法作用域,这也是词法环境这个名称的由来。
从技术上讲,静态作用域是通过捕获函数创建所在的环境来实现的。
在我们的例子中,foo函数捕获的环境是全局环境:
图8:闭包
我们可以看到,一个环境引用一个函数,而这个函数又反过来引用该环境。
Funarg问题的第二种子类型被称为向上funarg问题。这里唯一的区别是捕获的环境比创建它的上下文存活得更久。
下面我们看一个例子:
同样,从技术上讲,它与捕获定义环境的相同确切机制没有什么区别。就在这种情况下,如果没有闭包,foo的激活环境将被销毁。但我们捕获了它,所以它不能被释放,并保留下来,以支持静态作用域语义。
开发者对闭包的理解经常是不完整的,他们通常只考虑闭包的向上funarg问题(实际上它确实更有意义)。不过,正如我们所见,向下和向上funarg问题的技术机制是完全相同的,就是静态作用域的机制。
正如我们上面所提到的,与原型相似,同一个父环境可以在几个闭包之间共享。这样,就可以访问和修改共享的数据:
因为两个闭包increment和decrement都是在包含count变量的作用域内创建的,所以它们共享这个父作用域。即,捕获总是通过引用发生的,也就是说对整个父环境的引用被存储下来了。
我们可以在下图看到:
图9:一个共享环境
有些语言会通过值捕获,给被捕获的变量做个副本,并且不允许在父作用域中修改它。不过在JS中,重复一遍,它总是存在对父作用域的引用。
你可以在相应的章节中找到有关闭包和Funarg问题的详细讨论。
所以所有标识符都是静态作用域的。不过,在ECMAScript中有一个值是动态作用域的。就是this的值。
this
this值是一个动态并隐式传给一个上下文代码的特殊对象。我们可以把它当作是一个隐式的额外形参,能够访问,但是不能修改。
this值的用途是为多个对象执行相同的代码。
主要的用例是基于类的OOP。一个实例方法(在原型中定义的)存在于一个模本中,但是在该类的所有实例中共享。
当getX方法被激活时,就会创建一个新的环境去存储本地变量和参数。此外,函数环境记录得到了传过来的[[ThisValue]],这个this值是根据函数调用的方式动态绑定的。当该函数是用p1调用时,this值就是p1,而第二种情况下就是p2。
this的另一种应用就是通用的接口函数,可以用在mixins或者traits中。
在以下例子中,Movable接口包含通用函数move,其中_x和_y属性留给这个mixin的用户实现:
作为替代方案,mixin还可以应用在原型级,而不是像上例中那样在每个实例上。
为展示this值的动态性质,请思考以下例子,我们留给读者作为要解决的一个练习:
因为当foo在一个特定调用中时,仅通过查看foo函数的源代码,我们不能判断this的值是什么,所以我们说this值是动态作用域。
箭头函数的this值是特殊的:其this是词法(静态)的,而不是动态的。即,它们的函数环境记录不会提供this值,而是来自于父环境。
就像我们说过的那样,在全局上下文中,this是全局对象(全局环境记录的绑定对象)。以前只有一个全局对象,而在当前版本的规范中,可能有多个全局对象,这些全局对象是代码域(code realms)的一部分。下面我们来讨论一下这种结构。
域(Realm)
在求值之前,所有ECMAScript代码必须与一个域关联。从技术上讲,域只是为一个上下文提供全局环境。
定义16 域:代码域是一个封装了单独的全局环境的对象。
当一个执行上下文被创建时,就与一个特定的代码域关联起来。这个代码域为该上下文提供全局环境。而且这种关联保持不变。
当前版本的规范并没有提供显式创建域的能力,不过它们可以通过实现隐式地创建。不过已经有一个提案要暴露这个API给用户代码。
不过从逻辑上讲,从栈中的每个上下文总是与它的域关联:
图10:上下文和域的关联
让我们通过vm模块来看看独立的域的例子:
现在我们正在接近ECMAScript运行时的蓝图了。不过,我们依然需要看看代码的入口点,以及初始化过程。这是由作业(job)和作业队列(job queues)的机制管理的。
作业(Job)
有些操作可以推迟,并在执行上下文栈上有可用的位置时立刻执行。
作业在作业队列中排队,在当前版本的规范中,有两种作业队列:ScriptJobs和PromiseJobs。
而ScriptJobs队列上的初始作业是我们程序的主入口点——加载和求值的初始脚本:创建一个域,创建一个全局上下文并与该域关联在一起,压到栈中,执行全局代码。
注意,ScriptJobs队列同时管理脚本和模块。
此外,这个上下文可以执行其它上下文,或者将其他作业排入队列。一个可以生成并且加入队列的作业的例子就是promise。
当没有正在运行的执行上下文,并且执行上下文栈为空时,ECMAScript的实现(如JavaScript、ActionScript、JScript等)会从作业队列中移除第一个挂起的作业,创建一个执行上下文,并开始其执行。
实例:
async函数可以等待promise,所以它们也可以排队promise作业:
现在我们已经很接近当前JS领域的最终蓝图了。我们将看到我们所讨论过的所有这些组件的主要所有者:代理(the Agents)。
代理(Agent)
ECMScript中并发和并行是用代理模式(Agent Pattern)实现的。代理模式很接近于参与者模式(Actor Patter) ——一个带有消息传递通讯风格的轻量级进程。
依赖代理的实现可以在同一个线程上运行,也可以在单独的线程上运行。浏览器环境中的Worker代理就是代理概念的一个例子。
代理之间是状态相互隔离的,而且可以通过发送消息进行通讯。有些数据可以在代理之间共享,比如SharedArrayBuffer。代理还可以组合成代理集群(agent clusters)。
在下例中,index.html调用agent-smith.js的worker,传递共享的内存块:
如下是worker的代码:
上例的完整代码可以在这个 gist 中找到。
所以下面是ECMAScript运行时的概述图:
图11:ECMAScript运行时
而这就是ECMAScript引擎背后发生的事情!
到这里我们就结束了。这是我们可以在一篇综述文章中讲解有关JS核心的所有信息了。正如我们所提到的,js代码可以被分组到模块中,对象的属性可以通过Proxy对象进行跟踪,等等——我们可以在JavaScript语言的各种文档中找到很多用户级的信息。
这里我们试着表示一个ECMAScript程序本身的逻辑结构,希望它阐明了这些细节。如果你有任何疑问、建议或者反馈——我很乐意像以前一样在评论中讨论。
感谢TC-39的代表和规范的编辑帮助澄清本文。有关讨论可以在这个推特跟帖中找到。
祝学习ECMAScript顺利!
The text was updated successfully, but these errors were encountered: