【译】JavaScript:核心 - 第二版 - 网络埋伏纪事

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 这是JavaScript:核心概述讲稿的第二版,致力于ECMAScript编程语言及其运行时系统的核心组件。目标人群:有经验的程序员、专家。本文的第一版涵盖了JS语言的通用方面,主要讲解了旧式ES3规范中的概念,并参考了在ES5和ES6(即ES2015)中的一些变化。

这是JavaScript:核心概述讲稿的第二版,致力于ECMAScript编程语言及其运行时系统的核心组件。

目标人群:有经验的程序员、专家。

本文的第一版涵盖了JS语言的通用方面,主要讲解了旧式ES3规范中的概念,并参考了在ES5和ES6(即ES2015)中的一些变化。

从ES2015开始,规范修改了一些核心组件的描述和结构,引入了新的模型等等。所以在这个版本中,我们会关注较新的概念以及更新了的术语,但是依然保留在规范各个版本中保持一致的最基本的JS结构。

本文涵盖了ES2017+运行时系统。

注: ECMAScript规范的最新版本可以在TC-39网站上找到。

我们从讨论ECMAScript最基础的概念对象开始。

对象

ECMAScript是一门面向对象的编程语言,它基于原型,以对象作为其核心概念。

定义. 1: 对象: 对象属性的集合,并且有一个原型对象。原型要么是一个对象,要么是null值。

我们来看一个简单的对象示例。一个对象的原型是被内部的[[Prototype]]属性引用,通过__proto__属性暴露给用户级代码。

对于如下代码:


let point = {
    x: 10,
    y: 20,
};
dts

其结构中带有两个 显式的自有属性 和一个 隐式 __proto__ 属性, __proto__ 属性是对 point 的原型的引用:
6ae0e31c7521151281c4b3dd05426c915a9dd9e9

图 1. 带有原型的基本对象

注:对象也可以存储 symbol。有关symbol的更多信息,请参考 这份文档

原型对象用于以动态调度机制实现继承。下面我们研究一下原型链的概念,详细看看这种机制是怎么回事。

原型

每个对象在创建时都会得到其原型(prototype)。如果原型没有显式设置,对象会以默认原型作为其继承对象

定义 2. 原型:*原型是用于实现基于原型的继承*的委托对象。

原型可以通过用__proto__属性或者Object.create()方法显式设置:


// 基对象
let point = {
    x: 10,
    y: 20,
};
// 继承自point对象
let point3D = {
    z: 30,
    __proto__: point,
};
console.log(
    point3D.x,  // 10, 继承来的
    point3D.y,  // 20, 继承来的
    point3D.z   // 30, 自有的
);
qml

注:默认情况下,对象以Object.prototype作为其继承对象。

所有对象都可以作为另一个对象的原型,而且原型本身也可以有自己的原型。如果一个原型有一个对其原型的非空引用,依此类推,就称为原型链

定义 3:原型链:原型链是用于实现继承共享属性有限对象链。

b6cb13b9876a693ef3d43303ae5054dc408887f8

图 2. 原型链

规则很简单:如果一个属性在对象本身中找不到,就试图在原型中解析;如果还找不到,就到原型的原型中找,等等 - 直到找完整个原型链。

从技术上讲,这种机制被称为动态调度(dynamic dispatch)或者委托

定义 4:委托(Delegation):一种用于在继承链中解析一个属性的机制。这个过程发生在运行时,因此也称为动态调度(dynamic dispath)。

注:*静态调度 是在编译时 解析引用,而动态调度 是在运行时*解析引用。

 
 

并且,如果一个属性最终在原型链中找不到,就返回undefined值:


// 一个"空"对象
let empty = {};
console.log(
  // 来自默认运行的函数
  empty.toString,
  // undefined
  empty.x, 
);
zephir

 
 

正如我们所见,默认对象实际上永远不会是空的 - 它总会从Object.prototype继承一些东西。如果要创建一个无原型的词典,我们必须显式将其原型设置为null


// 不继承任何东西。
let dict = Object.create(null);
console.log(dict.toString); // undefined
gauss

动态调度机制允许继承链的 完全可变性,提供改变委托对象的能力:


let protoA = {x: 10};
let protoB = {x: 20};
// 与`let objectC = {__proto__: protoA};`相同:
let objectC = Object.create(protoA);
console.log(objectC.x); // 10
// 改变委托:
Object.setPrototypeOf(objectC, protoB);
console.log(objectC.x); // 20
javascript


注:即使现在 __proto__属性被标准化了,并且它更容易用于解释,但是在实践中对原型操作更喜欢用API方法,比如 Object.createObject.getPrototypeOfObject.setPrototypeOf以及类似的 Reflect模块。

Object.prototype示例中,我们看到同样的原型可以在多个对象之间共享。在这个原则的基础上,ECMAScript中就实现了基于类的继承。下面我们看看这个示例,看看JS中"类"概念背后的机制。

当多个对象共享相同的初始状态以及行为时,它们就形成了一种分类(classification)。

定义 5:类(class):一个类是一种形式化的概念集合,指定其对象的初始状态和行为。

假如我们需要有多个对象,这些对象都继承自同一个原型,我们自然会先创建这个原型,然后显式从新创建的对象继承它:


// 所有字母的通用原型
let letter = {
  getNumber() {
    return this.number;
  }
};
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);
typescript

我们可以在下图看到这些关系:

079bbd58ff4052001a75b5c4bbf6aabda69c936f

图 3. 共享的原型

不过,这显然很麻烦。而类正是干这事的,它作为一种语法糖(即在语义上做同样事情的构造,不过是以更好的语法形式),允许用方便的模式创建这样的多个对象:


class Letter {
  constructor(number) {
    this.number = number;
  }
  getNumber() {
    return this.number;
  }
}
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);
typescript

注:在ECMAScript中, 基于类的继承是在 基于原型的代理基础上实现的。

注:*'类'只是一个理论上的概念。从技术上讲,它可以用像在Java或者C++那样,用静态调度实现,或者像在JavaScript、Python、Ruby等中那样,用动态调度(委托)*实现。

从技术上讲,一个类被表示为一对构造函数+原型。因此,构造函数创建对象,同时还自动为它新创建的实例设置原型。这个原型被存储在<ConstructorFunction>.prototype属性中。

定义 6:构造函数:*构造函数*是一个用于创建实例,并自动设置实例的原型的函数。

 
 

可以显式使用构造函数。而且,在引入类的概念之前,JS开发者过去也没有更好的替代品(我们依然可以在互联网上找到很多这样的遗留代码):


function Letter(number) {
  this.number = number;
}
Letter.prototype.getNumber = function() {
  return this.number;
};
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);
javascript

而且虽然创建单层构造函数很容易,不过这种从父类继承的模式需要相当多的样板代码。目前这个样板代码是作为实现细节隐藏的,而这恰好就是在JavaScript创建类时背后发生的事情。

注:*构造函数 只是基于类的继承的实现细节*。
下面我们来看看对象及其类的关系:
a7f14a319d24ad02063a10a62e61a73a82107ed8

图 4. 构造函数和对象的关系

上图表明,每个对象都有一个相关的原型。甚至构造函数(类)Letter也有它自己的原型Function.prototype。注意,这个Letter.prototype是Letter的实例(即abz)的原型。

注:*任何 对象的实际原型总是 __proto__引用。而构造函数上的显式 prototype属性只是一个对其实例*的原型的引用;在实例上,它依然是被 __proto__引用。详情请参见 这里

我们可以在ES3. 7.1 OOP:通用理论 这篇文章中找到有关通用OOP概念的详细讨论(包括基于类、基于原型等的详细描述)。

现在我们已经理解了ECMAScript对象之间的基本关系,下面我们深入看看JS运行时系统。我们会看到,这里几乎所有东西也都可以被表示为对象。

执行上下文

为执行JS代码,并跟踪其运行时求值,ECMAScript规范定义了执行上下文的概念。从逻辑上讲,执行上下文是用执行上下文栈的简写)来维护的,栈与调用栈这个通用概念有关。

定义 7:执行上下文(Execution Context):执行上下文是用于跟踪运行时代码求值的一个规范设备。

ECMAScript代码有几种类型:全局代码函数代码eval代码和模块代码;每种代码都是在其执行上下文中求值。不同的代码类型及其对应的对象可能会影响执行上下文的结构:比如,generator函数将其generator对象保存在上下文中。

 
 

下面我们考虑一个递归函数调用:


function recursive(flag) {
  // 退出条件
  if (flag === 2) {
    return;
  }
  // 递归调用。
  recursive(++flag);
}
// Go.
recursive(0);
actionscript

当函数被调用时,就创建了一个新的执行上下文,并被到栈中 - 此时,它变成一个活动的执行上下文。当函数返回时,其上下文被从栈中弹出

调用另一个上下文的上下文被称为调用者(caller)。被调用的上下文相应地被称为被调用者(callee)。在我们的例子中,recursive函数在递归调用它本身时,同时扮演了这两个角色:既是调用者,又是被调用者。

定义 8:执行上下文栈:*执行上下文栈*是一种LIFO(后进先出)结构,用于维护控制流程和执行顺序。

对于上面的例子,有如下的栈"压入-弹出"变动图:

209203f942f1596749255782794c38f9f5abb10e

图 5. 执行上下文栈

从图中我们还可以看到,全局上下文(Global context)总是在栈的底部,它是由之前任何其它上下文的执行创建的。

我们可以在对应的章找到执行上下文的更多详细资料。

 
 

通常,一个上下文的代码会一直运行到结束,不过正如我们上面提到过的,有些对象,比如generator,可能会违反栈的LIFO顺序。一个generator函数可能会挂起其正在执行的上下文,并在结束前将其从栈中删除。一旦generator再次激活,它上下文就被回复,并再次压入栈中:


function *gen() {
  yield 1;
  return 2;
}
let g = gen();
console.log(
  g.next().value, // 1
  g.next().value, // 2
);
javascript

这里的yield语句将值返回给调用者,并弹出上下文。在第二个next调用时,同一个上下文被再次压入栈中,并恢复。这样的上下文可能会比创建它的调用者活得长,所以会违反LIFO结构。

注:我们可以在 这个文档中阅读有关generator和iterator的更多资料。

下面我们要讨论执行上下文的最重要的部分;特别是我们应该看到ECMAScript运行时如何管理变量存储以及由嵌套代码块创建的作用域。这就是词法环境的通常概念,它用来在JS中存储数据,并用闭包的机制解决'Funarg问题'

环境

每个执行上下文都有一个相关联的词法环境

定义 9:词法环境(lexical environment):词法环境是一种用于定义出现在上下文中的标识符与其值之间的关联的结构。每个环境有一个对可选的父环境的引用。

所以,环境就是定义在一个作用域中的变量、函数和类的仓库(storage)。

从技术上讲,环境是由一对**环境记录(Environment Record)(一个将标识符映射到值的实际存储表)以及对父的引用(可能是null)组成的。

对于如下代码:


let x = 10;
let y = 20;
function foo(z) {
  let x = 100;
  return x + y + z;
}
foo(30); // 150
javascript

全局上下文以及 foo函数的上下文的环境结构看起来会像下面这样:

d0300aa099b99473f0fe5f194822bd04ffb91b58

图 6. 环境链

在逻辑上讲,这会让我们回想起了上面已经讨论过的原型链。而标识符解析的规则是很相似的:如果一个变量在自己的环境中找不到,就试着在父环境、父环境的父环境中查找它,依此类推,直到查完整个环境链

定义 10:标识符解析(Identifier Resolution):在一个环境链中解析一个变量(绑定)的过程。一个解析不出来的绑定会导致ReferenceError

这就解释了为什么变量x被解析为100,而不是10?因为它是直接在foo自身环境中找到的;为什么可以访问参数z?因为它也是只存储在激活环境(activation environment)中;为什么我们还可以访问变量y?因为它是在父环境中找到的。

与原型类似,同一个父环境可以被几个子环境共享:比如,两个全局函数共享同一个全局环境。

注:有关词法环境的详细信息可以参考 本文.

环境记录根据类型而有所不同。有对象环境记录和声明式环境记录。在声明式记录之上,还有函数环境记录和模块环境记录。每种类型的记录都有其特定的属性。不过,标识符解析的通用机制对于所有环境都是通用的,并且不依赖于记录的类型。

全局环境的记录就是对象环境记录的一个例子。这样的记录也有关联的绑定对象,该对象会存储一些来自该记录的属性,但是不会存储来自其它记录的属性,反之亦然。绑定对象也可以被提供为this值。


// 旧式用`var`声明的变量。
var x = 10;
// 现代用`let`声明的变量。
let y = 20;
// 二者都被添加到环境记录:
console.log(
  x, // 10
  y, // 20
);
// 但是只有`x`被添加到"绑定对象"。
// 全局环境的绑定对象是鳏居对象,等于`this`:
console.log(
  this.x, // 10
  this.y, // undefined!
);
// 绑定对象可以存储一个名称,该名称不添加到环境记录,
// 因为它不是一个有效的标识符:
this['not valid ID'] = 30;
console.log(
  this['not valid ID'], // 30
);
javascript

可以用下图来描述:

e814417be64a35464814d1b00bdfb92497d80824

图 7. 绑定对象

注意,绑定对象的存在是为了涵盖旧式构造(比如var声明和with语句),这种构造也将其对象作为绑定对象提供。这些是环境被表示为简单对象时的历史原因。当前的环境模型更加优化,不过结果是我们再也不能将绑定当作属性来访问了。

我们已经看到环境是如何通过父链接关联。下面我们将看到环境如何比创建它的上下文存活得更久,这是我们将要讨论的闭包机制的基础。

闭包

ECMAScript中的函数是一等公民。这个概念是函数式编程的基础,而JavaScript中是支持函数式编程的。

定义. 11:一等函数(First-class Function):可以作为普通数据参与的一个函数:可以存储在一个变量中,作为实参传递、或者作为另一个函数的返回值返回。

与一等函数相关的是所谓"Funarg问题"(或者"函数式实参问题")。这个问题是在函数不得不处理自由变量时候出现的。

定义. 12:自由变量(Free Variable):一个既不是函数的形参,也不是函数的局部变量的变量。

下面我们来看看Funarg问题,看看在ECMAScript中如何解决这个问题。

 
 

考虑如下的代码段:


let x = 10;
function foo() {
  console.log(x);
}
function bar(funArg) {
  let x = 20;
  funArg(); // 10, 而不是20!
}
// 将 `foo` 作为实参传给 `bar`。
bar(foo);
javascript

对于函数foo,变量x就是自由变量。当foo函数被激活时(通过funArg形参),它在哪里解析x绑定呢?是从创建函数的外层作用域,还是从调用者作用域,还是从函数被调用的地方?我们可以看到,调用者bar函数也提供了对x的绑定(值为20)。

上面描述的情况称为向下funarg问题,即在判断绑定的正确环境时的歧义性:它应该是创建时的环境,还是调用时的环境?

这是通过达成协议使用静态作用域来解决的,静态作用域是创建时的作用域。

定义 13:静态作用域:如果一个语言只通过查找源代码,就可以判断绑定在哪个环境中解析,那么该语言就实现了静态作用域

静态作用域有时也称为词法作用域,这也是词法环境这个名称的由来。

从技术上讲,静态作用域是通过捕获函数创建所在的环境来实现的。

注:可以在 本文中阅读有关静态和动态作用域知识。

在我们的例子中,foo函数捕获的环境是全局环境

b1900097cfcfa297c4ba7ae48fef725060dd9f93

图 8. 闭包

我们可以看到,一个环境引用一个函数,而这个函数又引用该环境。

定义. 14:闭包:*闭包是一个函数捕获它被定义时所在的环境。这个环境被用于标识符解析*。

注:一个函数是在一个新的激活环境中被调用的,这个环境存储了 本地变量实参。该激活环境的 父环境被设置为该函数的 闭合环境,从而有了 词法作用域的语义。

Funarg问题的第二种子类型被称为向上funarg问题。这里唯一的区别是捕获的环境比创建它的上下文存活得更久。

下面我们看一个例子:


function foo() {
  let x = 10;
  // 闭包,捕获`foo`的环境。
  function bar() {
    return x;
  }
  // 向上funarg。
  return bar;
}
let x = 20;
// 调用`foo`来返回`bar`闭包。
let bar = foo();
bar(); // 10,而不是20!
javascript

同样,从技术上讲,它与捕获定义环境的确切机制没有什么不同。就在这种情况下,如果没有闭包,foo的激活环境将被销毁。但我们捕获了它,所以它不能被释放,并保留下来,以支持静态作用域语义。

对闭包的理解经常不完整 - 开发者通常认为闭包只是与向上的funarg问题有关(实际上它确实更有意义)。不过,正如我们所见,向下向上funarg问题的技术机制是完全相同的,就是静态作用域的机制

正如我们上面所提到的,与原型相似,同一个父环境可以在几个闭包之间共享。这样,就可以访问和修改共享的数据:


function createCounter() {
  let count = 0;
  return {
    increment() { count++; return count; },
    decrement() { count--; return count; },
  };
}
let counter = createCounter();
console.log(
  counter.increment(), // 1
  counter.decrement(), // 0
  counter.increment(), // 1
);
swift

因为两个闭包incrementdecrement都是在包含count变量的作用域内创建的,所以它们共享这个父作用域。即,捕获总是通过引用发生的,也就是说对整个父环境引用被存储下来了。

我们可以在下图看到:

855d66616c8f4dec69c304a60dcd75d83a80fc72

图 9. 一个共享的环境

有些语言会通过值捕获,给被捕获的变量做个副本,并且不允许在父作用域中修改它。不过在JS中,再说一遍,它总是对父作用域的引用

注:JS引擎实现可能会优化这个步骤,并且不会捕获整个环境,只捕获要用的自由变量,然后依然在父作用域中维护可变数据的不变量。

有关闭包和Funarg问题的详细讨论,可以在对应的章节中找到。

所以所有标识符都是静态作用域的。不过,在ECMAScript中有一个值是动态作用域的。就是this的值。

This

this值是一个动态隐式传给一个上下文的代码的特殊对象。我们可以把它当作是一个隐式的额外形参,能够访问,但是不能修改。

this值的用途是为多个对象执行相同的代码。

定义 15:this:this是一个隐式的上下文对象,可以从一个执行上下文的代码中访问,从而可以为多个对象应用相同的代码。

主要的使用案例是基于类的OOP。一个实例方法(在原型中定义的)存在于一个标本中,但是在该类的所有实例共享


class Point {
  constructor(x, y) {
    this._x = x;
    this._y = y;
  }
  getX() {
    return this._x;
  }
  getY() {
    return this._y;
  }
}
let p1 = new Point(1, 2);
let p2 = new Point(3, 4);
// 这两个实例中都可以访问`getX`和`getY`(两个实例都被作为`this`传递)
console.log(
  p1.getX(), // 1
  p2.getX(), // 3
);
javascript

getX方法被激活时,就会创建一个新的环境存储本地变量和形参。此外,函数环境记录得到了传过来的[[ThisValue]],这个this值是根据函数调用的方式动态绑定的。当该函数是用p1调用时,this值就是p1,而第二种情况下就是p2

this的另一种应用就是通用的接口函数,可以用在mixins或者traits中。

 
 

在如下的例子中,Movable接口包含通用函数move,其中_x_y属性留给这个mixin的用户实现:


// 通用的Movable接口(mixin)。
let Movable = {
  /**
   * 这个函数是通用的,可以与提供`_x`和`_y`属性的任何对象一起用,
   * 不管该对象的class是什么。
   */
  move(x, y) {
    this._x = x;
    this._y = y;
  },
};
let p1 = new Point(1, 2);
// 让 `p1` movable.
Object.assign(p1, Movable);
// 可以访问 `move` 方法。
p1.move(100, 200);
console.log(p1.getX()); // 100
pony

作为替代方案,mixin还可以应用在原型级,而不是像上例中那样在每个实例上。

为展示this值的动态性质,考虑下例,我们留给读者作为要解决的一个练习:


function foo() {
  return this;
}
let bar = {
  foo,
  baz() {
    return this;
  },
};
// `foo`
console.log(
  foo(),       // 全局或者undefined
  bar.foo(),   // bar
  (bar.foo)(), // bar
  (bar.foo = bar.foo)(), // 全局
);
// `bar.baz`
console.log(bar.baz()); // bar
let savedBaz = bar.baz;
console.log(savedBaz()); // 全局
gauss

因为当 foo在一个特定调用中时,只通过查看 foo函数的源代码,我们不能没法说出 this的值是什么,所以我们说 this值是 动态作用域

注: 我们可以在 对应的章中,得到关于如何判断 this值,以及为什么上面的代码会按那样的方式工作的详细解释。

 
 

箭头函数this值是特殊的:其this词法(静态)的,而不是动态的。即,它们的函数环境记录不会提供this值,而是来自于父环境


var x = 10;
let foo = {
  x: 20,
  // 动态 `this`.
  bar() {
    return this.x;
  },
  // 词法 `this`.
  baz: () => this.x,
  qux() {
    // 调用内的词法this。
    let arrow = () => this.x;
    return arrow();
  },
};
console.log(
  foo.bar(), // 20, 来自 `foo`
  foo.baz(), // 10, 来自 global
  foo.qux(), // 20, 来自 `foo` 和箭头函数
);
coffeescript

就像我们说过的那样,在全局上下文中,this全局对象全局环境记录绑定对象)。以前只有一个全局对象,而在当前版本的规范中,可能有多个全局对象,这些全局对象都是代码域的一部分。下面我们来讨论一下这种结构。

在求值之前,所有ECMAScript代码必须与一个域关联。从技术上讲,域只是为一个上下文提供全局环境。

定义 16:域(Realm):*代码域是一个封装了单独的全局环境*的对象。

当一个执行上下文被创建时,就与一个特定的代码域关联起来。这个代码域为该上下文提供全局环境。而且这种关联保持不变。

注:域在浏览器环境中的一个直接等价物就是 iframe元素,该元素恰好就是提供一个自定义的全局环境。在Node.js中,接近于 VM模块的沙箱。

当前版本的规范并没有提供显式创建域的能力,不过可以通过实现隐式创建。不过已经有一个提案要暴露这个API给用户代码。

不过从逻辑上讲,从栈中的每个上下文总是与它的域关联:

02abf812a371fd64ced4808bee6c85622781a092

图 10. 上下文和域的关联

现在我们正在接近ECMAScript运行时的较大的蓝图了。不过,我们依然需要看看代码的入口点,以及初始化过程。这是由作业作业队列的机制管理的。

作业

有些操作可以推迟,并在执行上下文栈上有可用点时执行。

定义 17:作业:作业(job)是一种抽象操作,它在没有其它ECMAScript计算正在进行时启动一个ECMAScript计算。

作业在作业队列中排队,在当前版本的规范中,有两种作业队列:ScriptJobsPromiseJobs

ScriptJobs队列上的初始作业是我们程序的主入口点 - 加载和求值的初始脚本:创建域,创建全局上下文并与该域关联在一起,压到栈中,执行全局代码。

注意,ScriptJobs队列管理脚本模块

而且这个上下文可以执行其它上下文,或者排队其它作业。一个可以引发和排队的作业的例子就是promise

当没有正在运行的执行上下文,并且执行上下文栈为空时,ECMAScript实现会从作业队列中移除第一个挂起的作业,创建一个执行上下文,并开始其执行。

注:作业队列通常是由所谓的 事件循环来处理。ECMAScript标准并没有指定事件循环,将它留给引擎实现,不过你可以在 这里找到一个演示示例。

示例:


// 在PromiseJobs队列上入队一个新的promise。
new Promise(resolve => setTimeout(() => resolve(10), 0))
  .then(value => console.log(value));
// 这个输出执行得早一些,因为它仍然是一个正在执行的上下文,
// 而作业不能先开始执行
console.log(20);
// Output: 20, 10
javascript

注: 更多有关promise的知识请阅读 这个文档

 
 

async函数可以等待promise,所以它们也可以排队promise作业:


async function later() {
  return await Promise.resolve(10);
}
(async () => {
  let data = await later();
  console.log(data); // 10
})();
// 也会发生得早一些,因为async执行是在PromiseJobs队列中排队的。
console.log(20);
// Output: 20, 10
javascript

注: 请在这里阅读更多有关async函数的知识。

现在我们已经很接近当前JS领域的最终蓝图了。我们将看到我们所讨论过的所有这些组件的主要负责人代理

代理

ECMScript中并发并行是用代理模式(Agent Pattern)实现的。代理模式很接近于参与者模式(Actor Patter) — 一个带有消息传递风格通讯的轻量级进程

定义 18:代理(Agent):代理是封装了执行上下文栈、一组作业队列,以及代码域的一个概念。

依赖代理的实现可以在同一个线程上运行,也可以在单独的线程上运行。浏览器环境中的Worker代理就是代理概念的一个例子。

代理之间是状态相互隔离的,而且可以通过发送消息进行通讯。有些数据可以在代理之间共享,比如SharedArrayBuffer。代理还可以组合成代理集群

在下例中,index.html调用agent-smith.js worker,传递共享的内存块:


// 在`index.html`中:
// 这个代理和其它worker之间共享的数据。
let sharedHeap = new SharedArrayBuffer(16);
// 我们角度的数据。
let heapArray = new Int32Array(sharedHeap);
// 创建一个新代理(worker)。
let agentSmith = new Worker('agent-smith.js');
agentSmith.onmessage = (message) => {
  // 代理发送它修改的数据的索引。
  let modifiedIndex = message.data;
  // 检查被修改的数据
  console.log(heapArray[modifiedIndex]); // 100
};
// 发送共享数据给代理
agentSmith.postMessage(sharedHeap);
javascript

如下是worker的代码:


// agent-smith.js
/**
 * 在这个worker中接受共享的 array buffer。
 */
onmessage = function(message) {
  // worker角度的共享数据。
  let heapArray = new Int32Array(message.data);
  let indexToModify = 1;
  heapArray[indexToModify] = 100;
  // 将索引作为消息发送回来。
  postMessage(indexToModify);
};
javascript

上例的完整代码可以在这个gist中找到。

所以下面是ECMAScript运行时的概述图:

9b06e743404384a8846e8358549b31196b55a872

图 11. ECMAScript 运行时

而这就是ECMAScript引擎背后发生的事情!

到这里我们就结束了。这是我们可以在一篇综述文章中讲解有关JS核心的所有信息了。正如我们所提到的,js代码可以被分组到模块中,对象的属性可以通过Proxy对象进行跟踪,等等。- 我们可以在JavaScript语言的各种文档中找到很多用户级的信息。

这里我们试着表示一个ECMAScript程序本身的逻辑结构,希望它澄清了这些细节。如果你有任何疑问、建议或者反馈 - 我很乐意像以前一样在评论中讨论。

感谢TC-39的代表和规范的编辑帮助澄清本文。有关讨论可以在这个推特跟帖中找到。

祝学习ECMAScript顺利!

原文发布时间为:2018年01月29日
原文作者:Dmitry Soshnikov 
本文来源:掘金 如需转载请联系原作者

目录
相关文章
|
3月前
|
JavaScript 算法 前端开发
采招网JS逆向:基于AES解密网络数据
采招网JS逆向:基于AES解密网络数据
62 0
|
5月前
|
JSON 前端开发 JavaScript
在JavaScript中,异步编程是一种处理非阻塞操作(如网络请求、文件读写等)的重要技术
【6月更文挑战第12天】JavaScript中的异步编程通过Promise和async/await处理非阻塞操作。Promise管理异步操作的三种状态,防止回调地狱,支持链式调用和并行处理。async/await是ES8引入的语法糖,使异步代码更像同步代码,提高可读性。两者结合使用能更高效地处理复杂异步场景。
38 3
|
20天前
|
数据采集 JavaScript 前端开发
JavaScript重定向对网络爬虫的影响及处理
JavaScript重定向对网络爬虫的影响及处理
|
1月前
|
存储 资源调度 JavaScript
vue.js【网络请求和状态管理】
vue.js【网络请求和状态管理】
|
3月前
|
数据采集 资源调度 JavaScript
Node.js 适合做高并发、I/O密集型项目、轻量级实时应用、前端构建工具、命令行工具以及网络爬虫和数据处理等项目
【8月更文挑战第4天】Node.js 适合做高并发、I/O密集型项目、轻量级实时应用、前端构建工具、命令行工具以及网络爬虫和数据处理等项目
58 5
|
3月前
|
JavaScript 前端开发 UED
探索JavaScript的历史:网络需求初现、语言创立与标准化的旅程
探索JavaScript的历史:网络需求初现、语言创立与标准化的旅程
|
3月前
|
JavaScript 前端开发 应用服务中间件
【qkl】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
【区块链】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的网络在线考试系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的网络在线考试系统附带文章源码部署视频讲解等
51 0
基于springboot+vue.js+uniapp的网络在线考试系统附带文章源码部署视频讲解等
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的网络办公系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的网络办公系统附带文章和源代码部署视频讲解等
50 8
|
4月前
|
测试技术 API Android开发
autox.js如何监听异常情况,比如网络中断、内存慢、应用死机或者页面无响应
autox.js如何监听异常情况,比如网络中断、内存慢、应用死机或者页面无响应