【译】JavaScript 核心(第二版)

原文: JavaScript. The Core: 2nd Edition
做者: Dmitry Soshnikov

文章其余语言版本:俄语javascript

这篇文章是 JavaScript. The Core 演讲的第二版,文章内容专一于 ECMAScript 编程语言和其运行时系统的核心组件。html

面向读者:有经验的开发者、专家java

文章初版 涵盖了 JS 语言通用的方面,该文章描述的抽象大多来自古老的 ES3 规范,也引用了一些 ES5 和 ES6( ES2015 )的变动。node

从 ES2015 开始,规范更改了一些核心组件的描述和结构,引入了新的模型等等。因此这篇文章我将聚焦新的抽象,更新的术语和在规范版本更替中仍然维护并保持一致的很是基本的 JS 结构。git

文章涵盖 ES2017+ 运行时系统的内容。github

注释:最新 ECMAScript 规范 版本能够在 TC-39 网站上查看。

我将从对象的概念开始讲起,它是 ECMAScript 的根本。web

对象

ECMAScript 是一门面向对象、基于原型进行组织的编程语言,且它的核心抽象为对象的概念。编程

定义1:对象:对象是属性的集合而且有一个原型(prototype)对象。原型的值为一个对象或 null

咱们来看一个基本的对象示例。对象的原型可经过内部的 [[Prototype]] 属性引用,在用户代码层面则是暴露在 __proto__ 属性上。api

代码以下:promise

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

上面的对象有两个显式的属性和一个隐藏的 __proto__ 属性,它是 point 对象的原型引用:

A basic object with a prototype

注:对象也可能存储 symbol 。阅读这篇文章了解更多关于 symbol 的内容。

原型对象用于实现动态分配机制的继承。咱们先思考一下原型链概念,以便详细了解这个机制。

原型

全部对象在建立的时候都会获得原型。若是没有显式地设置原型,那么对象接收默认原型做为它们的继承对象。

定义2:原型:原型是一个代理对象,用来实现基于原型的继承。

原型能够经过 __proto__ 属性或 Object.create 方法显式的设置。

// Base object.
let point = {
  x: 10,
  y: 20,
};
 
// Inherit from `point` object.
let point3D = {
  z: 30,
  __proto__: point,
};
 
console.log(
  point3D.x, // 10, inherited
  point3D.y, // 20, inherited
  point3D.z  // 30, own
);
注:默认状况下,对象接收 Object.prototype 做为它们的继承对象。

任何对象均可做为其它对象的原型,且原型自己能够有原型。若是对象的原型不为 null ,原型的原型不为 null ,以此类推,这就叫作原型链。

定义3:原型链:原型链是对象的有限连接,用来实现继承和共享属性。

Figure 2. A prototype chain

规则很是简单:若是对象自身没有一个属性,就会试图在原型上解析属性,而后原型的原型,直到查找完整个原型链。

技术上来讲这个机制被称为动态分配或代理。

定义4:代理:一个在继承链上解析属性的机制。这个过程是在运行时发生的,所以也被叫作 动态分配
注:与此相反的静态分配是在编译的时候解析引用的,动态分配则是在运行时。

若是属性最终都没有在原型链上找到的话,那么返回 undefined 值。

// An "empty" object.
let empty = {};
 
console.log(
 
  // function, from default prototype
  empty.toString,
   
  // undefined
  empty.x,
 
);

从上面的代码能够知道,一个默认的对象实际上永远不为空--它老是从 Object.prototype 继承一些东西。若是想要建立一个无原型的字典(dictionary),咱们必须明确地将原型设为 null

// Doesn't inherit from anything.
let dict = Object.create(null);
 
console.log(dict.toString); // undefined

动态分配机制容许继承链彻底可变,提供修改代理对象的能力:

let protoA = {x: 10};
let protoB = {x: 20};
 
// Same as `let objectC = {__proto__: protoA};`:
let objectC = Object.create(protoA);
console.log(objectC.x); // 10
 
// Change the delegate:
Object.setPrototypeOf(objectC, protoB);
console.log(objectC.x); // 20
注:即便 __proto__ 如今是标准属性,而且在解释时使用易于理解,但实践时倾向使用操做原型的 API 方法,如 Object.createObject.getPrototypeOfObject.setPrototypeOf ,相似于反射(Reflect)模块。

从上面 Object.prototype 示例咱们知道同一个原型能够给多个对象共享。从这个原理出发,ECMAScript 实现了基于类的继承。咱们看下示例,而且深刻了解 JS 的 “类(class)” 抽象。

当多个对象共享同一个初始的状态和行为时,它们就造成了一个

定义5:类:一个类是一个正式的抽象集,它规定了对象的初始状态和行为。

假如咱们须要多个对象继承同一个原型,咱们固然能够建立这个原型并显式的继承它:

// Generic prototype for all letters.
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
);

咱们能够从下图看到这些关系:

Figure 3. A shared prototype

然而这明显很繁琐。类抽象正是服务这个目的 - 做为一个语法糖(和构造器在语义上所作的同样,可是是更友好的语法形式),它让咱们使用更方便的模式建立那些对象:

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
);
注: 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 类时在底层所发生的。

注:构造函数就是类继承的实现细节。

咱们看一下对象和它们的类的关系:

Figure 4. A constructor and objects relationship

上图显示了每一个对象都有一个关联的原型。就连构造函数(类)也有原型也就是 Function.prototype 。咱们看到 a、b 和 c 是 Letter 的实例,它们的原型是 Letter.prototype

注:全部对象的实际原型老是 __proto__ 引用。构造函数显式声明的 prototype 属性只是一个指向它实例的原型的引用;实例的原型仍然是经过 __proto__ 引用获得。 点此连接详细了解

你能够在文章 ES3. 7.1 OOP: The general theory 中找到关于 OPP 通用概念(包括基于类、基于原型等的详细介绍)的详细讨论。

如今咱们已经了解了 ECMAScript 对象间的基本关系,让咱们更深刻的了解 JS 运行时系统。咱们将会看到,几乎全部的东西均可以用对象表示。

执行上下文

为了执行 JS 代码并追踪其运行时的计算,ECMAScript 规范定义了执行上下文(execution context)的概念。逻辑上执行上下文是用来保持的(执行上下文栈咱们一会就会看到),它与调用栈(call stack)的通用概念相对应。

定义7:执行上下文:执行上下文是一个规范策略,用于追踪代码的运行时计算。

ECMAScript 代码有几种类型:全局代码、函数和 eval ;它们都在各自的执行上下文中运行。不一样的代码类型及其适当的对象可能会影响执行上下文的结构:例如,生成器函数(generator functions)会将其生成器对象(generator object)保存在上下文中。

咱们看一个递归函数调用:

function recursive(flag) {
 
  // Exit condition.
  if (flag === 2) {
    return;
  }
 
  // Call recursively.
  recursive(++flag);
}
 
// Go.
recursive(0);

当一个函数调用时,就建立一个新的执行上下文并把它压入栈 - 这时它就成了活跃的执行上下文。当函数返回时,其上下文就从栈中推出。

咱们将调用另外一个上下文的上下文称为调用者(caller)。被调用的上下文所以就叫作被调用者(callee)。在上面的例子中,recursive 函数同时承担着上述二者角色:调用者和被调用者 - 当递归地调用自身。

定义8:执行上下文栈:执行上下文栈是一个后进先出的结构,它用来维护控制流和执行顺序。

在上面的例子中,咱们对栈有“压入-推出”的修改:

Figure 5. An execution context stack

咱们能够看到,全局上下文一直都在栈的底部,它是在执行任何其余上下文以前建立的。

你能够在这篇文章中找到更多关于执行上下文的详细内容。

通常状况下,一个上下文中的代码会运行到结束,然而正如咱们上面所提到的,一些对象 - 如生成器,可能会违反栈后进先出的顺序。一个生成器函数可能会挂起其运行上下文并在完成以前将其从栈中移除。当生成器再次激活时,其上下文恢复并再次被压入栈:

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

上面代码中的 yield 语句返回值给调用者并将上下文推出。第二次调用 next 时,相同的上下文再次被压入栈并恢复。这样的上下文会比建立它的调用者生命周期更长,所以违反了后进先出的结构。

注:你能够阅读 这篇文档了解关于生成器和迭代器的更多内容。

如今咱们将讨论执行上下文的重要组成部分;特别是 ECMAScript 运行时如何管理变量的存储和代码中嵌套块建立的做用域(scope)。这是词法环境(lexical environments)的通用概念,它在 JS 中用来存储数据和解决“函数参数问题(Funarg problem)” - 和闭包(closure)的机制一块儿。

环境

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

定义9:词法环境:词法环境是用于定义上下文中出现的 标识符与其值之间的关联的结构。每一个环境均可以有一个指向其 可选父环境的引用。

因此,一个环境是在某个范围内定义的变量,函数和类的存储

从技术上来讲,一个环境是由一个环境记录(一个将标识符映射到值的实际存储表)和一个对父项(多是 null)的引用这一对组成。

看代码:

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

上面代码的全局上下文foo 函数的上下文的环境结构以下图所示:

Figure 6. An environment chain

从逻辑上讲,这使咱们想起上面讨论过的原型链。而且标识符解析的规则也很是类似:若是在本身的环境中找不到变量,则尝试在父级环境中、在父级父级中查找它,以此类推 - 直到整个环境链都查找完成。

定义10:标识符解析:在环境链中解析变量(绑定)的过程。 没法解析的绑定会致使 ReferenceError

这就解释了:为何变量 x 被解析为 100,而不是 10 - 它是直接在 foo 本身的环境中找到;为何咱们能够访问参数 z - 它也只是存储在激活环境中;也是为何咱们能够访问变量 y - 它是在父级环境中找到的。

与原型相似,相同的父级环境能够由多个子环境共享:例如,两个全局函数共享相同的全局环境。

注:您能够在 这篇文章中得到有关词法环境的详细信息。

环境记录因类型而异。有对象环境记录和声明式环境记录。在声明式记录之上还有函数环境记录和模块环境记录。每种类型的记录都有它的特性。可是,标识符解析的通用机制在全部环境中都是通用的,而且不依赖于记录的类型。

一个对象环境记录的例子就是全局环境记录。这种记录也有相关联的绑定对象,它能够存储记录中的一些属性,而不是所有,反之亦然(译者注:不一样的能够看下面的示例代码)。绑定对象也能够经过 this 获得。

// Legacy variables using `var`.
var x = 10;
 
// Modern variables using `let`.
let y = 20;
 
// Both are added to the environment record:
console.log(
  x, // 10
  y, // 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, // 10
  this.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
);

上述代码能够表示为下图:

Figure 7. A binding object

须要注意的是,绑定对象的存在是为了兼容遗留的结构,例如 var 声明和with 语句,它们也将它们的对象做为绑定对象提供。这就是环境被表示为简单对象的历史缘由。如今,环境模型更加优化,但其结果是,咱们没法再将绑定做为属性访问(译者注:如上面的代码中咱们不能经过 this.y 访问 y 的值)。

咱们已经看到环境是如何经过父连接相关联的。如今咱们将看到一个环境的生命周期如何比创造它的上下文环境的更久。这是咱们即将讨论的闭包机制的基础。

闭包

ECMAScript中的函数是头等的(first-class)。这个概念是函数式编程的基础,这些方面也被 JavaScript 所支持。

定义11:头等函数:它是一种函数,其能够做为正常数据参与:存储在变量中,做为参数传递,或做为另外一个函数的值返回。

与头等函数概念相关的是所谓的“函参问题(Funarg problem)”(或“一个函数参数的问题”)。当一个函数须要处理自由变量时,这个问题就会出现。

定义12:自由变量:一个既不是参数也不是自身函数的局部变量的变量。

咱们来看看函参问题,并看它在 ECMAScript 中是如何解决的。

考虑下面的代码片断:

let x = 10;
 
function foo() {
  console.log(x);
}
 
function bar(funArg) {
  let x = 20;
  funArg(); // 10, not 20!
}
 
// Pass `foo` as an argument to `bar`.
bar(foo);

对于函数 foo 来讲,x 是自由变量。当 foo 函数被激活时(经过
funArg 参数) - 应该在哪里解析 x 的绑定?是建立函数的外部做用域仍是调用函数的调用者做用域?正如咱们所见,调用者即 bar 函数,也提供了 x 的绑定 - 值为 20 。

上面描述的用例被称为 downward funarg problem,即在肯定绑定的正确环境时的模糊性:它应该是建立时的环境,仍是调用时的环境?

这是经过使用静态做用域的协议来解决的,也就是建立时的做用域。

定义13:静态做用域:一种实现静态做用域的语言,仅仅经过查看源码就能够肯定在哪一个环境中解析绑定。

静态做用域有时也被称为词法做用域,所以也就是词法环境的命名由来。

从技术上来讲,静态做用域是经过捕获建立函数的环境来实现的。

注:您能够阅读 连接文章的了解静态和动态做用域。

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

Figure 8. A closure

咱们能够看到一个环境引用了一个函数,而这个函数又回引了环境。

定义14:闭包:闭包是 捕获定义环境的函数。在未来此环境用于标识符解析。

注:一个函数调用时是在全新的环境中激活,该环境存储局部变量参数。激活环境的父环境被设置为函数的闭包环境,从而产生词法做用域语义。

函参问题的第二个子类型被称为upward funarg problem。它们之间惟一的区别是捕捉环境的生命周期比建立它的环境更长。

咱们看例子:

function foo() {
  let x = 10;
   
  // Closure, capturing environment of `foo`.
  function bar() {
    return x;
  }
 
  // Upward funarg.
  return bar;
}
 
let x = 20;
 
// Call to `foo` returns `bar` closure.
let bar = foo();
 
bar(); // 10, not 20!

一样,从技术上来讲,它与捕获定义环境的确切机制没有区别。只是这种状况下,若是没有闭包,foo 的激活环境就会被销毁。可是咱们捕获了它,因此它不能被释放,并被保留 - 以支持静态做用域语义。

人们对闭包的理解一般是不完整的 - 开发人员一般考虑闭包仅仅依据 upward funarg problem(其实是更合理)。可是,正如咱们所看到的,downwardupward funarg problem 的技术机制是彻底同样的 - 就是静态做用域的机制。

正如咱们上面提到的,与原型相似,几个闭包能够共享相同的父环境。这容许它们访问和修改共享数据:

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
);

因为在包含 count 变量的做用域内建立了两个闭包:incrementdecrement ,因此它们共享这个父做用域。也就是说,捕获老是“经过引用” 发生 - 意味着对整个父环境的引用被存储。

Figure 9. A shared environment

有些语言可能捕获的是值,制做捕获的变量的副本,而且不容许在父做用域中更改它。可是,重复一遍,在 JS 中,它始终是对父范围的引用。

注:引擎的实现可能会优化这一步,而不会捕获整个环境。只捕获使用的自由变量,但它们仍然在父做用域中保持不变的可变数据。

你能够在连接文章中找到有关闭包和函参问题的详细讨论。

全部的标识符都是静态的做用域。然而,在 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);
 
// Can access `getX`, and `getY` from
// both instances (they are passed as `this`).
 
console.log(
  p1.getX(), // 1
  p2.getX(), // 3
);

getX 方法被激活时,会建立一个新的环境来存储局部变量和参数。另外,函数环境记录获得传递来的 [[ThisValue]] ,它是根据函数的调用方式动态绑定的。当用 p1 调用时,this 值刚好是 p1 ,第二种状况下是 p2

this 的另外一个应用是泛型接口函数,它能够用在 mixintraits 中。

在下面的例子中,Movable 接口包含泛型函数 move ,它指望这个 mixin 的用户实现 _x_y 属性:

// Generic Movable interface (mixin).
let Movable = {
 
  /**
   * 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;
  },
};
 
let p1 = new Point(1, 2);
 
// Make `p1` movable.
Object.assign(p1, Movable);
 
// Can access `move` method.
p1.move(100, 200);
 
console.log(p1.getX()); // 100

做为替代方案,mixin 也能够应用于原型级别,而不是像上例中每一个实例作的那样。

为了展现 this 值的动态性,考虑下面例子,咱们把这个例子留给读者来解决:

function foo() {
  return this;
}
 
let bar = {
  foo,
 
  baz() {
    return this;
  },
};
 
// `foo`
console.log(
  foo(),       // global or undefined
 
  bar.foo(),   // bar
  (bar.foo)(), // bar
 
  (bar.foo = bar.foo)(), // global
);
 
// `bar.baz`
console.log(bar.baz()); // bar
 
let savedBaz = bar.baz;
console.log(savedBaz()); // global

由于只经过查看 foo 函数的源代码,咱们不能知道它在特定的调用中 this 的值是什么,因此咱们说 this 值是动态做用域。

注:您能够在 这篇文章中获得关于如何肯定 this 值的详细解释,以及为何上面的代码是那样的结果。

箭头函数this 值比较特殊:其 this 是词法的(静态的),而不是动态的。即他们的函数环境记录不提供 this 值,它是从父环境中获取的。

var x = 10;
 
let foo = {
  x: 20,
 
  // Dynamic `this`.
  bar() {
    return this.x;
  },
 
  // Lexical `this`.
  baz: () => this.x,
 
  qux() {
    // Lexical this within the invocation.
    let arrow = () => this.x;
 
    return arrow();
  },
};
 
console.log(
  foo.bar(), // 20, from `foo`
  foo.baz(), // 10, from global
  foo.qux(), // 20, from `foo` and arrow
);

就像咱们所说的,在全局上下文this 值是全局对象(全局环境记录绑定对象)。之前只有一个全局对象。在当前版本的规范中,可能有多个全局对象,这是代码领域(code realms)的一部分。咱们来讨论一下这个结构。

领域

在求值以前,全部 ECMAScript 代码都必须与一个领域相关联。从技术上来讲,一个领域只是为一个上下文提供全局环境。

定义16:领域:代码领域是封装独立的全局环境的对象。

当一个执行上下文被建立时,它与一个特定的代码领域相关联,这个代码领域为这个上下文提供了全局环境。该关联在将来将保持不变。

注:浏览器环境中的直接领域是 iframe 元素,正是它提供了一个自定义的全局环境。在 Node.js 中,它和 vm 模块的沙箱相似。

规范的当前版本没有提供显式建立领域的能力,可是它们能够由实现隐含地建立。不过有一个将这个API暴露给用户代码的提案

从逻辑上来讲,堆栈中的每一个上下文老是与其领域相关联:

Figure 10. A context and realm association

如今咱们正在接近 ECMAScript 运行时的全貌了。然而,咱们仍然须要看到代码的入口点和初始化过程。这是由 jobs(做业)job queues(做业队列) 机制管理的。

Job

有一些操做能够被推迟的,并在执行上下文堆栈上有可用点时当即执行。

定义17:Job: Job 是一个抽象操做,当没有其余 ECMAScript 计算正在进行时,该操做启动 ECMAScript 计算。

Job 在 做业队列 中排队,在当前的规范版本中有两个做业队列 ScriptJobsPromiseJobs

ScriptJobs 队列中的初始 job 是咱们程序的主要入口 - 初始化已加载且求值的脚本:建立一个领域,建立一个全局上下文,而且与这个领域相关联,它被推入堆栈,全局代码被执行。

须要注意的是,ScriptJobs 队列管理着脚本和模块二者。

此外,这个上下文能够执行其余上下文,或使其余 jobs 到队列中排队。一个能够产生排队的 job 就是 promise。

若是没有正在运行的执行上下文,而且执行上下文堆栈为空,则 ECMAScript 实现会从做业队列中移除第一个 job,建立执行上下文并开始执行。

注:做业队列一般由被称为 “事件循环”的抽象来处理。
ECMAScript 标准没有指定事件循环,而是将其留给实现决定,可是你能够在 连接页面找到一个教学示例。

示例:

// Enqueue a new promise on the PromiseJobs queue.
new Promise(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 first
console.log(20);
 
// Output: 20, 10
注:你能够在 连接文档中阅读有关 promise 的更多信息。

async 函数能够等待(await) promise 执行,因此它们也使 promise 做业排队:

async function later() {
  return await Promise.resolve(10);
}
 
(async () => {
  let data = await later();
  console.log(data); // 10
})();
 
// Also happens earlier, since async execution
// is queued on the PromiseJobs queue.
console.log(20);
 
// Output: 20, 10
注:更多 async 函数内容请 点击连接

如今咱们已经很是接近当前 JS 宇宙的最终画面。立刻咱们将看到咱们讨论的全部组件的主要全部者 - 代理商(Agents)。

Agent

ECMAScript中的并发(concurrency)并行(parallelism)是使用代理模式(Agent pattern)的实现的。代理模式很是接近参与者模式(Actor pattern) - 一个具备消息传递风格的轻量级进程。

定义18:Agent:代理是封装执行上下文堆栈、做业队列集和代码领域的抽象概念。

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

代理的状态是相互隔离的,能够经过发送消息进行通讯。一些数据能够在代理之间共享,例如 SharedArrayBuffer 。代理也能够组合成代理集群

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

// In the `index.html`:
 
// Shared data between this agent, and another worker.
let sharedHeap = new SharedArrayBuffer(16);
 
// Our view of the data.
let heapArray = new Int32Array(sharedHeap);
 
// Create a new agent (worker).
let agentSmith = new Worker('agent-smith.js');
 
agentSmith.onmessage = (message) => {
  // Agent sends the index of the data it modified.
  let modifiedIndex = 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.
  let heapArray = new Int32Array(message.data);
 
  let indexToModify = 1;
  heapArray[indexToModify] = 100;
 
  // Send the index as a message back.
  postMessage(indexToModify);
};

你能够在连接页面获得示例的完整代码。

(须要注意的是,若是你在本地运行这个例子,请在 Firefox 中运行它,由于因为安全缘由,Chrome 不容许从本地文件加载 web worker)

下图展现了 ECMAScript 运行时:

Figure 11. ECMAScript runtime

如图所示,那就是在 ECMAScript 引擎下发生的事情!

如今文章到告终尾的时候。这是咱们能够在概述文章中涵盖的 JS 核心的信息量。就像咱们提到的,JS 代码能够被分组成模块,对象的属性能够被 Proxy 对象追踪等等。 - 许多用户级别的细节能够在 JavaScript 语言的不一样文档中找到。

尽管咱们试图表示一个 ECMAScript 程序自己的逻辑结构,但愿可以澄清这些细节。若是你有任何问题,建议或反馈意见,我将一如既往地乐于在评论中讨论这些问题。

我要感谢 TC-39 的表明和规范编辑帮助澄清本文。该讨论能够在这个 Twitter 主题中找到。

祝学习 ECMAScript 好运!

Written by: Dmitry Soshnikov
Published on: November 14th, 2017

相关文章
相关标签/搜索