摘要:技術(shù)上來(lái)說(shuō)這個(gè)機(jī)制被稱(chēng)為動(dòng)態(tài)分配或代理。定義類(lèi)一個(gè)類(lèi)是一個(gè)正式的抽象集,它規(guī)定了對(duì)象的初始狀態(tài)和行為。技術(shù)上來(lái)說(shuō)一個(gè)類(lèi)表示構(gòu)造函數(shù)原型的組合。因此構(gòu)造函數(shù)創(chuàng)建對(duì)象并自動(dòng)設(shè)置新創(chuàng)建實(shí)例的原型。第二次調(diào)用時(shí),相同的上下文再次被壓入棧并恢復(fù)。
原文:JavaScript. The Core: 2nd Edition
作者:Dmitry Soshnikov
文章其他語(yǔ)言版本:俄語(yǔ)
這篇文章是 JavaScript. The Core 演講的第二版,文章內(nèi)容專(zhuān)注于 ECMAScript 編程語(yǔ)言和其運(yùn)行時(shí)系統(tǒng)的核心組件。
面向讀者:有經(jīng)驗(yàn)的開(kāi)發(fā)者、專(zhuān)家
文章第一版 涵蓋了 JS 語(yǔ)言通用的方面,該文章描述的抽象大多來(lái)自古老的 ES3 規(guī)范,也引用了一些 ES5 和 ES6( ES2015 )的變更。
從 ES2015 開(kāi)始,規(guī)范更改了一些核心組件的描述和結(jié)構(gòu),引入了新的模型等等。所以這篇文章我將聚焦新的抽象,更新的術(shù)語(yǔ)和在規(guī)范版本更替中仍然維護(hù)并保持一致的非常基本的 JS 結(jié)構(gòu)。
文章涵蓋 ES2017+ 運(yùn)行時(shí)系統(tǒng)的內(nèi)容。
注釋?zhuān)?/strong>最新 ECMAScript 規(guī)范 版本可以在 TC-39 網(wǎng)站上查看。
我將從對(duì)象的概念開(kāi)始講起,它是 ECMAScript 的根本。
對(duì)象ECMAScript 是一門(mén)面向?qū)ο蟆⒒谠瓦M(jìn)行組織的編程語(yǔ)言,且它的核心抽象為對(duì)象的概念。
定義1:對(duì)象:對(duì)象是屬性的集合并且有一個(gè)原型(prototype)對(duì)象。原型的值為一個(gè)對(duì)象或 null 。
我們來(lái)看一個(gè)基本的對(duì)象示例。對(duì)象的原型可通過(guò)內(nèi)部的 [[Prototype]] 屬性引用,在用戶代碼層面則是暴露在 __proto__ 屬性上。
代碼如下:
let point = { x: 10, y: 20, };
上面的對(duì)象有兩個(gè)顯式的屬性和一個(gè)隱藏的 __proto__ 屬性,它是 point 對(duì)象的原型引用:
注:對(duì)象也可能存儲(chǔ) symbol 。閱讀這篇文章了解更多關(guān)于 symbol 的內(nèi)容。
原型對(duì)象用于實(shí)現(xiàn)動(dòng)態(tài)分配機(jī)制的繼承。我們先思考一下原型鏈概念,以便詳細(xì)了解這個(gè)機(jī)制。
原型所有對(duì)象在創(chuàng)建的時(shí)候都會(huì)得到原型。如果沒(méi)有顯式地設(shè)置原型,那么對(duì)象接收默認(rèn)原型作為它們的繼承對(duì)象。
定義2:原型:原型是一個(gè)代理對(duì)象,用來(lái)實(shí)現(xiàn)基于原型的繼承。
原型可以通過(guò) __proto__ 屬性或 Object.create 方法顯式的設(shè)置。
// 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 );
注:默認(rèn)情況下,對(duì)象接收 Object.prototype 作為它們的繼承對(duì)象。
任何對(duì)象都可作為其它對(duì)象的原型,且原型本身可以有原型。如果對(duì)象的原型不為 null ,原型的原型不為 null ,以此類(lèi)推,這就叫做原型鏈。
定義3:原型鏈:原型鏈?zhǔn)菍?duì)象的有限鏈接,用來(lái)實(shí)現(xiàn)繼承和共享屬性。
規(guī)則非常簡(jiǎn)單:如果對(duì)象自身沒(méi)有一個(gè)屬性,就會(huì)試圖在原型上解析屬性,然后原型的原型,直到查找完整個(gè)原型鏈。
技術(shù)上來(lái)說(shuō)這個(gè)機(jī)制被稱(chēng)為動(dòng)態(tài)分配或代理。
定義4:代理:一個(gè)在繼承鏈上解析屬性的機(jī)制。這個(gè)過(guò)程是在運(yùn)行時(shí)發(fā)生的,因此也被叫做動(dòng)態(tài)分配。
注:與此相反的靜態(tài)分配是在編譯的時(shí)候解析引用的,動(dòng)態(tài)分配則是在運(yùn)行時(shí)。
如果屬性最終都沒(méi)有在原型鏈上找到的話,那么返回 undefined 值。
// An "empty" object. let empty = {}; console.log( // function, from default prototype empty.toString, // undefined empty.x, );
從上面的代碼可以知道,一個(gè)默認(rèn)的對(duì)象實(shí)際上永遠(yuǎn)不為空--它總是從 Object.prototype 繼承一些東西。如果想要?jiǎng)?chuàng)建一個(gè)無(wú)原型的字典(dictionary),我們必須明確地將原型設(shè)為 null :
// Doesn"t inherit from anything. let dict = Object.create(null); console.log(dict.toString); // undefined
動(dòng)態(tài)分配機(jī)制允許繼承鏈完全可變,提供修改代理對(duì)象的能力:
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__ 現(xiàn)在是標(biāo)準(zhǔn)屬性,并且在解釋時(shí)使用易于理解,但實(shí)踐時(shí)傾向使用操作原型的 API 方法,如 Object.create、Object.getPrototypeOf、Object.setPrototypeOf ,類(lèi)似于反射(Reflect)模塊。
從上面 Object.prototype 示例我們知道同一個(gè)原型可以給多個(gè)對(duì)象共享。從這個(gè)原理出發(fā),ECMAScript 實(shí)現(xiàn)了基于類(lèi)的繼承。我們看下示例,并且深入了解 JS 的 “類(lèi)(class)” 抽象。
類(lèi)當(dāng)多個(gè)對(duì)象共享同一個(gè)初始的狀態(tài)和行為時(shí),它們就形成了一個(gè)類(lèi)。
定義5:類(lèi):一個(gè)類(lèi)是一個(gè)正式的抽象集,它規(guī)定了對(duì)象的初始狀態(tài)和行為。
假如我們需要多個(gè)對(duì)象繼承同一個(gè)原型,我們當(dāng)然可以創(chuàng)建這個(gè)原型并顯式的繼承它:
// 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 );
我們可以從下圖看到這些關(guān)系:
然而這明顯很繁瑣。類(lèi)抽象正是服務(wù)這個(gè)目的 - 作為一個(gè)語(yǔ)法糖(和構(gòu)造器在語(yǔ)義上所做的一樣,但是是更友好的語(yǔ)法形式),它讓我們使用更方便的模式創(chuàng)建那些對(duì)象:
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 中基于類(lèi)的繼承是在基于原型的代理之上實(shí)現(xiàn)的。注:一個(gè)“類(lèi)”只是理論上的抽象。技術(shù)上來(lái)說(shuō),它可以像 Java 或 C++ 一樣通過(guò)靜態(tài)分配來(lái)實(shí)現(xiàn),也可以像 JavaScript、Python、Ruby 一樣通過(guò)動(dòng)態(tài)分配(代理)來(lái)實(shí)現(xiàn)。
技術(shù)上來(lái)說(shuō)一個(gè)“類(lèi)”表示“構(gòu)造函數(shù) + 原型”的組合。因此構(gòu)造函數(shù)創(chuàng)建對(duì)象并自動(dòng)設(shè)置新創(chuàng)建實(shí)例的原型。這個(gè)原型存儲(chǔ)在
定義6:構(gòu)造器:構(gòu)造器是一個(gè)函數(shù),它用來(lái)創(chuàng)建實(shí)例并自動(dòng)設(shè)置它們的原型。
我們可以顯式的使用構(gòu)造函數(shù)。此外,在類(lèi)抽象引入之前,JS 開(kāi)發(fā)者過(guò)去因?yàn)闆](méi)有更好的選擇而這樣做(我們依然可以在互聯(lián)網(wǎng)上找到大量這樣的遺留代碼):
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 );
創(chuàng)建單級(jí)的構(gòu)造函數(shù)非常簡(jiǎn)單,而從父類(lèi)繼承的模式則要求更多的模板代碼。目前這些模板代碼作為實(shí)現(xiàn)細(xì)節(jié)被隱藏,而這正是在我們創(chuàng)建 JavaScript 類(lèi)時(shí)在底層所發(fā)生的。
注:構(gòu)造函數(shù)就是類(lèi)繼承的實(shí)現(xiàn)細(xì)節(jié)。
我們看一下對(duì)象和它們的類(lèi)的關(guān)系:
上圖顯示了每個(gè)對(duì)象都有一個(gè)關(guān)聯(lián)的原型。就連構(gòu)造函數(shù)(類(lèi))也有原型也就是 Function.prototype 。我們看到 a、b 和 c 是 Letter 的實(shí)例,它們的原型是 Letter.prototype 。
注:所有對(duì)象的實(shí)際原型總是 __proto__ 引用。構(gòu)造函數(shù)顯式聲明的 prototype 屬性只是一個(gè)指向它實(shí)例的原型的引用;實(shí)例的原型仍然是通過(guò) __proto__ 引用得到。點(diǎn)此鏈接詳細(xì)了解。
你可以在文章 ES3. 7.1 OOP: The general theory 中找到關(guān)于 OPP 通用概念(包括基于類(lèi)、基于原型等的詳細(xì)介紹)的詳細(xì)討論。
現(xiàn)在我們已經(jīng)了解了 ECMAScript 對(duì)象間的基本關(guān)系,讓我們更深入的了解 JS 運(yùn)行時(shí)系統(tǒng)。我們將會(huì)看到,幾乎所有的東西都可以用對(duì)象表示。
執(zhí)行上下文為了執(zhí)行 JS 代碼并追蹤其運(yùn)行時(shí)的計(jì)算,ECMAScript 規(guī)范定義了執(zhí)行上下文(execution context)的概念。邏輯上執(zhí)行上下文是用棧來(lái)保持的(執(zhí)行上下文棧我們一會(huì)就會(huì)看到),它與調(diào)用棧(call stack)的通用概念相對(duì)應(yīng)。
定義7:執(zhí)行上下文:執(zhí)行上下文是一個(gè)規(guī)范策略,用于追蹤代碼的運(yùn)行時(shí)計(jì)算。
ECMAScript 代碼有幾種類(lèi)型:全局代碼、函數(shù)和 eval ;它們都在各自的執(zhí)行上下文中運(yùn)行。不同的代碼類(lèi)型及其適當(dāng)?shù)膶?duì)象可能會(huì)影響執(zhí)行上下文的結(jié)構(gòu):例如,生成器函數(shù)(generator functions)會(huì)將其生成器對(duì)象(generator object)保存在上下文中。
我們看一個(gè)遞歸函數(shù)調(diào)用:
function recursive(flag) { // Exit condition. if (flag === 2) { return; } // Call recursively. recursive(++flag); } // Go. recursive(0);
當(dāng)一個(gè)函數(shù)調(diào)用時(shí),就創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入棧 - 這時(shí)它就成了活躍的執(zhí)行上下文。當(dāng)函數(shù)返回時(shí),其上下文就從棧中推出。
我們將調(diào)用另一個(gè)上下文的上下文稱(chēng)為調(diào)用者(caller)。被調(diào)用的上下文因此就叫做被調(diào)用者(callee)。在上面的例子中,recursive 函數(shù)同時(shí)承擔(dān)著上述兩者角色:調(diào)用者和被調(diào)用者 - 當(dāng)遞歸地調(diào)用自身。
定義8:執(zhí)行上下文棧:執(zhí)行上下文棧是一個(gè)后進(jìn)先出的結(jié)構(gòu),它用來(lái)維護(hù)控制流和執(zhí)行順序。
在上面的例子中,我們對(duì)棧有“壓入-推出”的修改:
我們可以看到,全局上下文一直都在棧的底部,它是在執(zhí)行任何其他上下文之前創(chuàng)建的。
你可以在這篇文章中找到更多關(guān)于執(zhí)行上下文的詳細(xì)內(nèi)容。
一般情況下,一個(gè)上下文中的代碼會(huì)運(yùn)行到結(jié)束,然而正如我們上面所提到的,一些對(duì)象 - 如生成器,可能會(huì)違反棧后進(jìn)先出的順序。一個(gè)生成器函數(shù)可能會(huì)掛起其運(yùn)行上下文并在完成之前將其從棧中移除。當(dāng)生成器再次激活時(shí),其上下文恢復(fù)并再次被壓入棧:
function *gen() { yield 1; return 2; } let g = gen(); console.log( g.next().value, // 1 g.next().value, // 2 );
上面代碼中的 yield 語(yǔ)句返回值給調(diào)用者并將上下文推出。第二次調(diào)用 next 時(shí),相同的上下文再次被壓入棧并恢復(fù)。這樣的上下文會(huì)比創(chuàng)建它的調(diào)用者生命周期更長(zhǎng),因此違反了后進(jìn)先出的結(jié)構(gòu)。
注:你可以閱讀這篇文檔了解關(guān)于生成器和迭代器的更多內(nèi)容。
現(xiàn)在我們將討論執(zhí)行上下文的重要組成部分;特別是 ECMAScript 運(yùn)行時(shí)如何管理變量的存儲(chǔ)和代碼中嵌套塊創(chuàng)建的作用域(scope)。這是詞法環(huán)境(lexical environments)的通用概念,它在 JS 中用來(lái)存儲(chǔ)數(shù)據(jù)和解決“函數(shù)參數(shù)問(wèn)題(Funarg problem)” - 和閉包(closure)的機(jī)制一起。
環(huán)境每個(gè)執(zhí)行上下文都有一個(gè)相關(guān)的詞法環(huán)境。
定義9:詞法環(huán)境:詞法環(huán)境是用于定義上下文中出現(xiàn)的標(biāo)識(shí)符與其值之間的關(guān)聯(lián)的結(jié)構(gòu)。每個(gè)環(huán)境都可以有一個(gè)指向其可選父環(huán)境的引用。
所以,一個(gè)環(huán)境是在某個(gè)范圍內(nèi)定義的變量,函數(shù)和類(lèi)的存儲(chǔ)。
從技術(shù)上來(lái)說(shuō),一個(gè)環(huán)境是由一個(gè)環(huán)境記錄(一個(gè)將標(biāo)識(shí)符映射到值的實(shí)際存儲(chǔ)表)和一個(gè)對(duì)父項(xiàng)(可能是 null)的引用這一對(duì)組成。
看代碼:
let x = 10; let y = 20; function foo(z) { let x = 100; return x + y + z; } foo(30); // 150
上面代碼的全局上下文和 foo 函數(shù)的上下文的環(huán)境結(jié)構(gòu)如下圖所示:
從邏輯上講,這使我們想起上面討論過(guò)的原型鏈。并且標(biāo)識(shí)符解析的規(guī)則也非常相似:如果在自己的環(huán)境中找不到變量,則嘗試在父級(jí)環(huán)境中、在父級(jí)父級(jí)中查找它,以此類(lèi)推 - 直到整個(gè)環(huán)境鏈都查找完成。
定義10:標(biāo)識(shí)符解析:在環(huán)境鏈中解析變量(綁定)的過(guò)程。 無(wú)法解析的綁定會(huì)導(dǎo)致 ReferenceError 。
這就解釋了:為什么變量 x 被解析為 100,而不是 10 - 它是直接在 foo 自己的環(huán)境中找到;為什么我們可以訪問(wèn)參數(shù) z - 它也只是存儲(chǔ)在激活環(huán)境中;也是為什么我們可以訪問(wèn)變量 y - 它是在父級(jí)環(huán)境中找到的。
與原型類(lèi)似,相同的父級(jí)環(huán)境可以由多個(gè)子環(huán)境共享:例如,兩個(gè)全局函數(shù)共享相同的全局環(huán)境。
注:您可以在這篇文章中獲得有關(guān)詞法環(huán)境的詳細(xì)信息。
環(huán)境記錄因類(lèi)型而異。有對(duì)象環(huán)境記錄和聲明式環(huán)境記錄。在聲明式記錄之上還有函數(shù)環(huán)境記錄和模塊環(huán)境記錄。每種類(lèi)型的記錄都有它的特性。但是,標(biāo)識(shí)符解析的通用機(jī)制在所有環(huán)境中都是通用的,并且不依賴(lài)于記錄的類(lèi)型。
一個(gè)對(duì)象環(huán)境記錄的例子就是全局環(huán)境記錄。這種記錄也有相關(guān)聯(lián)的綁定對(duì)象,它可以存儲(chǔ)記錄中的一些屬性,而不是全部,反之亦然(譯者注:不同的可以看下面的示例代碼)。綁定對(duì)象也可以通過(guò) 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 );
上述代碼可以表示為下圖:
需要注意的是,綁定對(duì)象的存在是為了兼容遺留的結(jié)構(gòu),例如 var 聲明和with 語(yǔ)句,它們也將它們的對(duì)象作為綁定對(duì)象提供。這就是環(huán)境被表示為簡(jiǎn)單對(duì)象的歷史原因。現(xiàn)在,環(huán)境模型更加優(yōu)化,但其結(jié)果是,我們無(wú)法再將綁定作為屬性訪問(wèn)(譯者注:如上面的代碼中我們不能通過(guò) this.y 訪問(wèn) y 的值)。
我們已經(jīng)看到環(huán)境是如何通過(guò)父鏈接相關(guān)聯(lián)的。現(xiàn)在我們將看到一個(gè)環(huán)境的生命周期如何比創(chuàng)造它的上下文環(huán)境的更久。這是我們即將討論的閉包機(jī)制的基礎(chǔ)。
閉包ECMAScript中的函數(shù)是頭等的(first-class)。這個(gè)概念是函數(shù)式編程的基礎(chǔ),這些方面也被 JavaScript 所支持。
定義11:頭等函數(shù):它是一種函數(shù),其可以作為正常數(shù)據(jù)參與:存儲(chǔ)在變量中,作為參數(shù)傳遞,或作為另一個(gè)函數(shù)的值返回。
與頭等函數(shù)概念相關(guān)的是所謂的“函參問(wèn)題(Funarg problem)”(或“一個(gè)函數(shù)參數(shù)的問(wèn)題”)。當(dāng)一個(gè)函數(shù)需要處理自由變量時(shí),這個(gè)問(wèn)題就會(huì)出現(xiàn)。
定義12:自由變量:一個(gè)既不是參數(shù)也不是自身函數(shù)的局部變量的變量。
我們來(lái)看看函參問(wèn)題,并看它在 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);
對(duì)于函數(shù) foo 來(lái)說(shuō),x 是自由變量。當(dāng) foo 函數(shù)被激活時(shí)(通過(guò)
funArg 參數(shù)) - 應(yīng)該在哪里解析 x 的綁定?是創(chuàng)建函數(shù)的外部作用域還是調(diào)用函數(shù)的調(diào)用者作用域?正如我們所見(jiàn),調(diào)用者即 bar 函數(shù),也提供了 x 的綁定 - 值為 20 。
上面描述的用例被稱(chēng)為 downward funarg problem,即在確定綁定的正確環(huán)境時(shí)的模糊性:它應(yīng)該是創(chuàng)建時(shí)的環(huán)境,還是調(diào)用時(shí)的環(huán)境?
這是通過(guò)使用靜態(tài)作用域的協(xié)議來(lái)解決的,也就是創(chuàng)建時(shí)的作用域。
定義13:靜態(tài)作用域:一種實(shí)現(xiàn)靜態(tài)作用域的語(yǔ)言,僅僅通過(guò)查看源碼就可以確定在哪個(gè)環(huán)境中解析綁定。
靜態(tài)作用域有時(shí)也被稱(chēng)為詞法作用域,因此也就是詞法環(huán)境的命名由來(lái)。
從技術(shù)上來(lái)說(shuō),靜態(tài)作用域是通過(guò)捕獲創(chuàng)建函數(shù)的環(huán)境來(lái)實(shí)現(xiàn)的。
注:您可以閱讀鏈接文章的了解靜態(tài)和動(dòng)態(tài)作用域。
在我們的例子中,foo 函數(shù)捕獲的環(huán)境是全局環(huán)境:
我們可以看到一個(gè)環(huán)境引用了一個(gè)函數(shù),而這個(gè)函數(shù)又回引了環(huán)境。
定義14:閉包:閉包是捕獲定義環(huán)境的函數(shù)。在將來(lái)此環(huán)境用于標(biāo)識(shí)符解析。注:一個(gè)函數(shù)調(diào)用時(shí)是在全新的環(huán)境中激活,該環(huán)境存儲(chǔ)局部變量和參數(shù)。激活環(huán)境的父環(huán)境被設(shè)置為函數(shù)的閉包環(huán)境,從而產(chǎn)生詞法作用域語(yǔ)義。
函參問(wèn)題的第二個(gè)子類(lèi)型被稱(chēng)為upward funarg problem。它們之間唯一的區(qū)別是捕捉環(huán)境的生命周期比創(chuàng)建它的環(huán)境更長(zhǎng)。
我們看例子:
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!
同樣,從技術(shù)上來(lái)說(shuō),它與捕獲定義環(huán)境的確切機(jī)制沒(méi)有區(qū)別。只是這種情況下,如果沒(méi)有閉包,foo 的激活環(huán)境就會(huì)被銷(xiāo)毀。但是我們捕獲了它,所以它不能被釋放,并被保留 - 以支持靜態(tài)作用域語(yǔ)義。
人們對(duì)閉包的理解通常是不完整的 - 開(kāi)發(fā)人員通常考慮閉包僅僅依據(jù) upward funarg problem(實(shí)際上是更合理)。但是,正如我們所看到的,downward 和 upward funarg problem 的技術(shù)機(jī)制是完全一樣的 - 就是靜態(tài)作用域的機(jī)制。
正如我們上面提到的,與原型類(lèi)似,幾個(gè)閉包可以共享相同的父環(huán)境。這允許它們?cè)L問(wèn)和修改共享數(shù)據(jù):
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 變量的作用域內(nèi)創(chuàng)建了兩個(gè)閉包:increment 和 decrement ,所以它們共享這個(gè)父作用域。也就是說(shuō),捕獲總是“通過(guò)引用” 發(fā)生 - 意味著對(duì)整個(gè)父環(huán)境的引用被存儲(chǔ)。
有些語(yǔ)言可能捕獲的是值,制作捕獲的變量的副本,并且不允許在父作用域中更改它。但是,重復(fù)一遍,在 JS 中,它始終是對(duì)父范圍的引用。
注:引擎的實(shí)現(xiàn)可能會(huì)優(yōu)化這一步,而不會(huì)捕獲整個(gè)環(huán)境。只捕獲使用的自由變量,但它們?nèi)匀辉诟缸饔糜蛑斜3植蛔兊目勺償?shù)據(jù)。
你可以在鏈接文章中找到有關(guān)閉包和函參問(wèn)題的詳細(xì)討論。
所有的標(biāo)識(shí)符都是靜態(tài)的作用域。然而,在 ECMAScript 中有一個(gè)值是動(dòng)態(tài)作用域的。那就是 this 的值。
thisthis 值是一個(gè)特殊的對(duì)象,它是動(dòng)態(tài)地、隱式地傳遞給上下文中的代碼。我們可以把它看作是一個(gè)隱含的額外參數(shù),我們可以訪問(wèn),但是不能修改。
this 值的目的是為多個(gè)對(duì)象執(zhí)行相同的代碼。
定義15:this:一個(gè)隱式的上下文對(duì)象,可以從一個(gè)執(zhí)行上下文的代碼中訪問(wèn) - 以便為多個(gè)對(duì)象執(zhí)行相同的代碼。
this 主要的用例是基于類(lèi)的 OOP。一個(gè)實(shí)例方法(在原型上定義)存在于一個(gè)范例中,但在該類(lèi)的所有實(shí)例中共享。
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 );
當(dāng) getX 方法被激活時(shí),會(huì)創(chuàng)建一個(gè)新的環(huán)境來(lái)存儲(chǔ)局部變量和參數(shù)。另外,函數(shù)環(huán)境記錄得到傳遞來(lái)的 [[ThisValue]] ,它是根據(jù)函數(shù)的調(diào)用方式動(dòng)態(tài)綁定的。當(dāng)用 p1 調(diào)用時(shí),this 值恰好是 p1 ,第二種情況下是 p2 。
this 的另一個(gè)應(yīng)用是泛型接口函數(shù),它可以用在 mixin 或 traits 中。
在下面的例子中,Movable 接口包含泛型函數(shù) move ,它期望這個(gè) mixin 的用戶實(shí)現(xiàn) _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 也可以應(yīng)用于原型級(jí)別,而不是像上例中每個(gè)實(shí)例做的那樣。
為了展示 this 值的動(dòng)態(tài)性,考慮下面例子,我們把這個(gè)例子留給讀者來(lái)解決:
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
因?yàn)橹煌ㄟ^(guò)查看 foo 函數(shù)的源代碼,我們不能知道它在特定的調(diào)用中 this 的值是什么,所以我們說(shuō) this 值是動(dòng)態(tài)作用域。
注:您可以在這篇文章中得到關(guān)于如何確定 this 值的詳細(xì)解釋?zhuān)约盀槭裁瓷厦娴拇a是那樣的結(jié)果。
箭頭函數(shù)中 this 值比較特殊:其 this 是詞法的(靜態(tài)的),而不是動(dòng)態(tài)的。即他們的函數(shù)環(huán)境記錄不提供 this 值,它是從父環(huán)境中獲取的。
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 );
就像我們所說(shuō)的,在全局上下文,this 值是全局對(duì)象(全局環(huán)境記錄的綁定對(duì)象)。以前只有一個(gè)全局對(duì)象。在當(dāng)前版本的規(guī)范中,可能有多個(gè)全局對(duì)象,這是代碼領(lǐng)域(code realms)的一部分。我們來(lái)討論一下這個(gè)結(jié)構(gòu)。
領(lǐng)域在求值之前,所有 ECMAScript 代碼都必須與一個(gè)領(lǐng)域相關(guān)聯(lián)。從技術(shù)上來(lái)說(shuō),一個(gè)領(lǐng)域只是為一個(gè)上下文提供全局環(huán)境。
定義16:領(lǐng)域:代碼領(lǐng)域是封裝獨(dú)立的全局環(huán)境的對(duì)象。
當(dāng)一個(gè)執(zhí)行上下文被創(chuàng)建時(shí),它與一個(gè)特定的代碼領(lǐng)域相關(guān)聯(lián),這個(gè)代碼領(lǐng)域?yàn)檫@個(gè)上下文提供了全局環(huán)境。該關(guān)聯(lián)在未來(lái)將保持不變。
注:瀏覽器環(huán)境中的直接領(lǐng)域是 iframe 元素,正是它提供了一個(gè)自定義的全局環(huán)境。在 Node.js 中,它和 vm 模塊的沙箱類(lèi)似。
規(guī)范的當(dāng)前版本沒(méi)有提供顯式創(chuàng)建領(lǐng)域的能力,但是它們可以由實(shí)現(xiàn)隱含地創(chuàng)建。不過(guò)有一個(gè)將這個(gè)API暴露給用戶代碼的提案。
從邏輯上來(lái)說(shuō),堆棧中的每個(gè)上下文總是與其領(lǐng)域相關(guān)聯(lián):
現(xiàn)在我們正在接近 ECMAScript 運(yùn)行時(shí)的全貌了。然而,我們?nèi)匀恍枰吹酱a的入口點(diǎn)和初始化過(guò)程。這是由 jobs(作業(yè)) 和 job queues(作業(yè)隊(duì)列) 機(jī)制管理的。
Job有一些操作可以被推遲的,并在執(zhí)行上下文堆棧上有可用點(diǎn)時(shí)立即執(zhí)行。
定義17:Job: Job 是一個(gè)抽象操作,當(dāng)沒(méi)有其他 ECMAScript 計(jì)算正在進(jìn)行時(shí),該操作啟動(dòng) ECMAScript 計(jì)算。
Job 在 作業(yè)隊(duì)列 中排隊(duì),在當(dāng)前的規(guī)范版本中有兩個(gè)作業(yè)隊(duì)列 ScriptJobs 和 PromiseJobs。
ScriptJobs 隊(duì)列中的初始 job 是我們程序的主要入口 - 初始化已加載且求值的腳本:創(chuàng)建一個(gè)領(lǐng)域,創(chuàng)建一個(gè)全局上下文,并且與這個(gè)領(lǐng)域相關(guān)聯(lián),它被推入堆棧,全局代碼被執(zhí)行。
需要注意的是,ScriptJobs 隊(duì)列管理著腳本和模塊兩者。
此外,這個(gè)上下文可以執(zhí)行其他上下文,或使其他 jobs 到隊(duì)列中排隊(duì)。一個(gè)可以產(chǎn)生排隊(duì)的 job 就是 promise。
如果沒(méi)有正在運(yùn)行的執(zhí)行上下文,并且執(zhí)行上下文堆棧為空,則 ECMAScript 實(shí)現(xiàn)會(huì)從作業(yè)隊(duì)列中移除第一個(gè) job,創(chuàng)建執(zhí)行上下文并開(kāi)始執(zhí)行。
注:作業(yè)隊(duì)列通常由被稱(chēng)為“事件循環(huán)”的抽象來(lái)處理。
ECMAScript 標(biāo)準(zhǔn)沒(méi)有指定事件循環(huán),而是將其留給實(shí)現(xiàn)決定,但是你可以在鏈接頁(yè)面找到一個(gè)教學(xué)示例。
示例:
// 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
注:你可以在鏈接文檔中閱讀有關(guān) promise 的更多信息。
async 函數(shù)可以等待(await) promise 執(zhí)行,所以它們也使 promise 作業(yè)排隊(duì):
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 函數(shù)內(nèi)容請(qǐng)點(diǎn)擊鏈接。
現(xiàn)在我們已經(jīng)非常接近當(dāng)前 JS 宇宙的最終畫(huà)面。馬上我們將看到我們討論的所有組件的主要所有者 - 代理商(Agents)。
AgentECMAScript中的并發(fā)(concurrency)和并行(parallelism)是使用代理模式(Agent pattern)的實(shí)現(xiàn)的。代理模式非常接近參與者模式(Actor pattern) - 一個(gè)具有消息傳遞風(fēng)格的輕量級(jí)進(jìn)程。
定義18:Agent:代理是封裝執(zhí)行上下文堆棧、作業(yè)隊(duì)列集和代碼領(lǐng)域的抽象概念。
依賴(lài)代理的實(shí)現(xiàn)可以在同一個(gè)線程上運(yùn)行,也可以在多帶帶的線程上運(yùn)行。瀏覽器環(huán)境中的 Worker 代理是代理概念的一個(gè)例子。
代理的狀態(tài)是相互隔離的,可以通過(guò)發(fā)送消息進(jìn)行通信。一些數(shù)據(jù)可以在代理之間共享,例如 SharedArrayBuffer 。代理也可以組合成代理集群。
在下面的例子中,index.html 調(diào)用 agent-smith.js worker ,傳遞共享的內(nèi)存塊:
// 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); };
你可以在鏈接頁(yè)面得到示例的完整代碼。
(需要注意的是,如果你在本地運(yùn)行這個(gè)例子,請(qǐng)?jiān)?Firefox 中運(yùn)行它,因?yàn)橛捎诎踩颍珻hrome 不允許從本地文件加載 web worker)
下圖展示了 ECMAScript 運(yùn)行時(shí):
如圖所示,那就是在 ECMAScript 引擎下發(fā)生的事情!
現(xiàn)在文章到了結(jié)尾的時(shí)候。這是我們可以在概述文章中涵蓋的 JS 核心的信息量。就像我們提到的,JS 代碼可以被分組成模塊,對(duì)象的屬性可以被 Proxy 對(duì)象追蹤等等。 - 許多用戶級(jí)別的細(xì)節(jié)可以在 JavaScript 語(yǔ)言的不同文檔中找到。
盡管我們?cè)噲D表示一個(gè) ECMAScript 程序本身的邏輯結(jié)構(gòu),希望能夠澄清這些細(xì)節(jié)。如果你有任何問(wèn)題,建議或反饋意見(jiàn),我將一如既往地樂(lè)于在評(píng)論中討論這些問(wèn)題。
我要感謝 TC-39 的代表和規(guī)范編輯幫助澄清本文。該討論可以在這個(gè) Twitter 主題中找到。
祝學(xué)習(xí) ECMAScript 好運(yùn)!
Written by: Dmitry Soshnikov
Published on: November 14th, 2017
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/92432.html
摘要:在線挑戰(zhàn),還沒(méi)用過(guò),貌似現(xiàn)在對(duì)英文資料心里還有種抵觸,必須克服實(shí)驗(yàn)樓研發(fā)工程師包含了等學(xué)習(xí)課程。書(shū)的作者就是開(kāi)發(fā)了用于數(shù)據(jù)分析的著名開(kāi)源庫(kù)的作者英文資料,對(duì)數(shù)據(jù)分析中要用到的一些庫(kù),等等做了簡(jiǎn)要介紹。形式的資料,示例代碼都很全。 showImg(https://segmentfault.com/img/remote/1460000004852849); 一、說(shuō)明 面對(duì)網(wǎng)絡(luò)上紛繁復(fù)雜的資...
摘要:一直都挺喜歡這個(gè)社區(qū)的,給人的第一感覺(jué)就是比較的專(zhuān)業(yè)正式,社區(qū)內(nèi)氛圍不錯(cuò),各種文章的質(zhì)量也很好,并且?guī)椭宋液芏唷:荛_(kāi)心能夠來(lái)到這里,記錄自己的成長(zhǎng),希望自己能夠多活躍一下,無(wú)論是在問(wèn)答上面還是寫(xiě)作上面。 一直都挺喜歡 Segmentfault 這個(gè)社區(qū)的,給人的第一感覺(jué)就是比較的專(zhuān)業(yè)正式,社區(qū)內(nèi)氛圍不錯(cuò),各種文章的質(zhì)量也很好,并且?guī)椭宋液芏唷:荛_(kāi)心能夠來(lái)到這里,記錄自己的成長(zhǎng),希望...
摘要:這是我收集的一些資源,分享給大家,全部放在百度網(wǎng)盤(pán),有需要的請(qǐng)轉(zhuǎn)存到自己的網(wǎng)盤(pán)或者下載,以免網(wǎng)盤(pán)鏈接失效,另外還有幾百的視頻文件存在網(wǎng)盤(pán),需要的加全部分享在空間,自己可以去下載與權(quán)威指南配套源碼禪意花園高清源碼基礎(chǔ)教程權(quán)威指南參考手冊(cè)鋒利 這是我收集的一些資源,分享給大家,全部放在百度網(wǎng)盤(pán),有需要的請(qǐng)轉(zhuǎn)存到自己的網(wǎng)盤(pán)或者下載,以免網(wǎng)盤(pán)鏈接失效,另外還有幾百G的視頻文件存在網(wǎng)盤(pán),需要的加...
摘要:軟件的復(fù)雜性命名的藝術(shù)在計(jì)算機(jī)科學(xué)中只有兩件困難的事情緩存失效和命名規(guī)范。到目前為止,我們依然將看做為開(kāi)發(fā)人員找不到合適命名的一種替代方式。 軟件的復(fù)雜性:命名的藝術(shù) 在計(jì)算機(jī)科學(xué)中只有兩件困難的事情:緩存失效和命名規(guī)范。—— Phil Karlton 前言 編寫(xiě)優(yōu)質(zhì)代碼本身是一件很困難的事情,為什么這么說(shuō)?因?yàn)榱己玫木幋a風(fēng)格是為了能更好的理解與閱讀。通常我們會(huì)只注重前者,而忽略了后者...
閱讀 2128·2021-09-27 14:04
閱讀 1873·2019-08-30 15:55
閱讀 1698·2019-08-30 13:13
閱讀 1065·2019-08-30 13:07
閱讀 2742·2019-08-29 15:20
閱讀 3240·2019-08-29 12:42
閱讀 3324·2019-08-28 17:58
閱讀 3593·2019-08-28 17:56