【譯】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

相關文章
相關標籤/搜索