摘要:技術上來說這個機制被稱為動態分配或代理。定義類一個類是一個正式的抽象集,它規定了對象的初始狀態和行為。技術上來說一個類表示構造函數原型的組合。因此構造函數創建對象并自動設置新創建實例的原型。第二次調用時,相同的上下文再次被壓入棧并恢復。
原文:JavaScript. The Core: 2nd Edition
作者:Dmitry Soshnikov
文章其他語言版本:俄語
這篇文章是 JavaScript. The Core 演講的第二版,文章內容專注于 ECMAScript 編程語言和其運行時系統的核心組件。
面向讀者:有經驗的開發者、專家
文章第一版 涵蓋了 JS 語言通用的方面,該文章描述的抽象大多來自古老的 ES3 規范,也引用了一些 ES5 和 ES6( ES2015 )的變更。
從 ES2015 開始,規范更改了一些核心組件的描述和結構,引入了新的模型等等。所以這篇文章我將聚焦新的抽象,更新的術語和在規范版本更替中仍然維護并保持一致的非常基本的 JS 結構。
文章涵蓋 ES2017+ 運行時系統的內容。
注釋:最新 ECMAScript 規范 版本可以在 TC-39 網站上查看。
我將從對象的概念開始講起,它是 ECMAScript 的根本。
對象ECMAScript 是一門面向對象、基于原型進行組織的編程語言,且它的核心抽象為對象的概念。
定義1:對象:對象是屬性的集合并且有一個原型(prototype)對象。原型的值為一個對象或 null 。
我們來看一個基本的對象示例。對象的原型可通過內部的 [[Prototype]] 屬性引用,在用戶代碼層面則是暴露在 __proto__ 屬性上。
代碼如下:
let point = { x: 10, y: 20, };
上面的對象有兩個顯式的屬性和一個隱藏的 __proto__ 屬性,它是 point 對象的原型引用:
注:對象也可能存儲 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:原型鏈:原型鏈是對象的有限鏈接,用來實現繼承和共享屬性。
規則非常簡單:如果對象自身沒有一個屬性,就會試圖在原型上解析屬性,然后原型的原型,直到查找完整個原型鏈。
技術上來說這個機制被稱為動態分配或代理。
定義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.create、Object.getPrototypeOf、Object.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 );
我們可以從下圖看到這些關系:
然而這明顯很繁瑣。類抽象正是服務這個目的 - 作為一個語法糖(和構造器在語義上所做的一樣,但是是更友好的語法形式),它讓我們使用更方便的模式創建那些對象:
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 一樣通過動態分配(代理)來實現。
技術上來說一個“類”表示“構造函數 + 原型”的組合。因此構造函數創建對象并自動設置新創建實例的原型。這個原型存儲在
定義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 類時在底層所發生的。
注:構造函數就是類繼承的實現細節。
我們看一下對象和它們的類的關系:
上圖顯示了每個對象都有一個關聯的原型。就連構造函數(類)也有原型也就是 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:執行上下文棧:執行上下文棧是一個后進先出的結構,它用來維護控制流和執行順序。
在上面的例子中,我們對棧有“壓入-推出”的修改:
我們可以看到,全局上下文一直都在棧的底部,它是在執行任何其他上下文之前創建的。
你可以在這篇文章中找到更多關于執行上下文的詳細內容。
一般情況下,一個上下文中的代碼會運行到結束,然而正如我們上面所提到的,一些對象 - 如生成器,可能會違反棧后進先出的順序。一個生成器函數可能會掛起其運行上下文并在完成之前將其從棧中移除。當生成器再次激活時,其上下文恢復并再次被壓入棧:
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 函數的上下文的環境結構如下圖所示:
從邏輯上講,這使我們想起上面討論過的原型鏈。并且標識符解析的規則也非常相似:如果在自己的環境中找不到變量,則嘗試在父級環境中、在父級父級中查找它,以此類推 - 直到整個環境鏈都查找完成。
定義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 );
上述代碼可以表示為下圖:
需要注意的是,綁定對象的存在是為了兼容遺留的結構,例如 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 函數捕獲的環境是全局環境:
我們可以看到一個環境引用了一個函數,而這個函數又回引了環境。
定義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(實際上是更合理)。但是,正如我們所看到的,downward 和 upward 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 變量的作用域內創建了兩個閉包:increment 和 decrement ,所以它們共享這個父作用域。也就是說,捕獲總是“通過引用” 發生 - 意味著對整個父環境的引用被存儲。
有些語言可能捕獲的是值,制作捕獲的變量的副本,并且不允許在父作用域中更改它。但是,重復一遍,在 JS 中,它始終是對父范圍的引用。
注:引擎的實現可能會優化這一步,而不會捕獲整個環境。只捕獲使用的自由變量,但它們仍然在父作用域中保持不變的可變數據。
你可以在鏈接文章中找到有關閉包和函參問題的詳細討論。
所有的標識符都是靜態的作用域。然而,在 ECMAScript 中有一個值是動態作用域的。那就是 this 的值。
thisthis 值是一個特殊的對象,它是動態地、隱式地傳遞給上下文中的代碼。我們可以把它看作是一個隱含的額外參數,我們可以訪問,但是不能修改。
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 的另一個應用是泛型接口函數,它可以用在 mixin 或 traits 中。
在下面的例子中,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暴露給用戶代碼的提案。
從邏輯上來說,堆棧中的每個上下文總是與其領域相關聯:
現在我們正在接近 ECMAScript 運行時的全貌了。然而,我們仍然需要看到代碼的入口點和初始化過程。這是由 jobs(作業) 和 job queues(作業隊列) 機制管理的。
Job有一些操作可以被推遲的,并在執行上下文堆棧上有可用點時立即執行。
定義17:Job: Job 是一個抽象操作,當沒有其他 ECMAScript 計算正在進行時,該操作啟動 ECMAScript 計算。
Job 在 作業隊列 中排隊,在當前的規范版本中有兩個作業隊列 ScriptJobs 和 PromiseJobs。
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)。
AgentECMAScript中的并發(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 運行時:
如圖所示,那就是在 ECMAScript 引擎下發生的事情!
現在文章到了結尾的時候。這是我們可以在概述文章中涵蓋的 JS 核心的信息量。就像我們提到的,JS 代碼可以被分組成模塊,對象的屬性可以被 Proxy 對象追蹤等等。 - 許多用戶級別的細節可以在 JavaScript 語言的不同文檔中找到。
盡管我們試圖表示一個 ECMAScript 程序本身的邏輯結構,希望能夠澄清這些細節。如果你有任何問題,建議或反饋意見,我將一如既往地樂于在評論中討論這些問題。
我要感謝 TC-39 的代表和規范編輯幫助澄清本文。該討論可以在這個 Twitter 主題中找到。
祝學習 ECMAScript 好運!
Written by: Dmitry Soshnikov
Published on: November 14th, 2017
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/92432.html
摘要:在線挑戰,還沒用過,貌似現在對英文資料心里還有種抵觸,必須克服實驗樓研發工程師包含了等學習課程。書的作者就是開發了用于數據分析的著名開源庫的作者英文資料,對數據分析中要用到的一些庫,等等做了簡要介紹。形式的資料,示例代碼都很全。 showImg(https://segmentfault.com/img/remote/1460000004852849); 一、說明 面對網絡上紛繁復雜的資...
摘要:一直都挺喜歡這個社區的,給人的第一感覺就是比較的專業正式,社區內氛圍不錯,各種文章的質量也很好,并且幫助了我很多。很開心能夠來到這里,記錄自己的成長,希望自己能夠多活躍一下,無論是在問答上面還是寫作上面。 一直都挺喜歡 Segmentfault 這個社區的,給人的第一感覺就是比較的專業正式,社區內氛圍不錯,各種文章的質量也很好,并且幫助了我很多。很開心能夠來到這里,記錄自己的成長,希望...
摘要:這是我收集的一些資源,分享給大家,全部放在百度網盤,有需要的請轉存到自己的網盤或者下載,以免網盤鏈接失效,另外還有幾百的視頻文件存在網盤,需要的加全部分享在空間,自己可以去下載與權威指南配套源碼禪意花園高清源碼基礎教程權威指南參考手冊鋒利 這是我收集的一些資源,分享給大家,全部放在百度網盤,有需要的請轉存到自己的網盤或者下載,以免網盤鏈接失效,另外還有幾百G的視頻文件存在網盤,需要的加...
摘要:軟件的復雜性命名的藝術在計算機科學中只有兩件困難的事情緩存失效和命名規范。到目前為止,我們依然將看做為開發人員找不到合適命名的一種替代方式。 軟件的復雜性:命名的藝術 在計算機科學中只有兩件困難的事情:緩存失效和命名規范。—— Phil Karlton 前言 編寫優質代碼本身是一件很困難的事情,為什么這么說?因為良好的編碼風格是為了能更好的理解與閱讀。通常我們會只注重前者,而忽略了后者...
閱讀 2136·2021-09-27 14:04
閱讀 1880·2019-08-30 15:55
閱讀 1705·2019-08-30 13:13
閱讀 1072·2019-08-30 13:07
閱讀 2749·2019-08-29 15:20
閱讀 3246·2019-08-29 12:42
閱讀 3342·2019-08-28 17:58
閱讀 3599·2019-08-28 17:56