(10)JavaScript核心(晉級(jí)高手必讀篇)

2018-02-24 15:25 更新

本篇是ECMA-262-3 in detail系列的一個(gè)概述(本人后續(xù)會(huì)翻譯整理這些文章到本系列(第11-19章)。每個(gè)章節(jié)都有一個(gè)更詳細(xì)的內(nèi)容鏈接,你可以繼續(xù)讀一下每個(gè)章節(jié)對(duì)應(yīng)的詳細(xì)內(nèi)容鏈接進(jìn)行更深入的了解。

適合的讀者:有經(jīng)驗(yàn)的開發(fā)員,專業(yè)前端人員。

原作者: Dmitry A. Soshnikov
發(fā)布時(shí)間: 2010-09-02
原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
參考1:http://ued.ctrip.com/blog/?p=2795
參考2:http://www.cnblogs.com/ifishing/archive/2010/12/08/1900594.html
主要是綜合了上面2位高手的中文翻譯,將兩篇文章的精華部分都結(jié)合在一起了。

我們首先來看一下對(duì)象[Object]的概念,這也是ECMASript中最基本的概念。

對(duì)象Object

ECMAScript是一門高度抽象的面向?qū)ο?object-oriented)語言,用以處理Objects對(duì)象. 當(dāng)然,也有基本類型,但是必要時(shí),也需要轉(zhuǎn)換成object對(duì)象來用。

An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value.

Object是一個(gè)屬性的集合,并且都擁有一個(gè)單獨(dú)的原型對(duì)象[prototype object]. 這個(gè)原型對(duì)象[prototype object]可以是一個(gè)object或者null值。

讓我們來舉一個(gè)基本Object的例子,首先我們要清楚,一個(gè)Object的prototype是一個(gè)內(nèi)部的[[prototype]]屬性的引用。

不過一般來說,我們會(huì)使用__ 下劃線來代替雙括號(hào),例如proto__(這是某些腳本引擎比如SpiderMonkey的對(duì)于原型概念的具體實(shí)現(xiàn),盡管并非標(biāo)準(zhǔn))。

var foo = {
  x: 10,
  y: 20
};

上述代碼foo對(duì)象有兩個(gè)顯式的屬性[explicit own properties]和一個(gè)自帶隱式的 proto 屬性[implicit proto property],指向foo的原型。

圖 2. 原型鏈

原型鏈通常將會(huì)在這樣的情況下使用:對(duì)象擁有 相同或相似的狀態(tài)結(jié)構(gòu)(same or similar state structure) (即相同的屬性集合)與 不同的狀態(tài)值(different state values)。在這種情況下,我們可以使用 構(gòu)造函數(shù)(Constructor) 在 特定模式(specified pattern) 下創(chuàng)建對(duì)象。

構(gòu)造函數(shù)(Constructor)

除了創(chuàng)建對(duì)象,構(gòu)造函數(shù)(constructor) 還做了另一件有用的事情—自動(dòng)為創(chuàng)建的新對(duì)象設(shè)置了原型對(duì)象(prototype object) 。原型對(duì)象存放于 ConstructorFunction.prototype 屬性中。

例如,我們重寫之前例子,使用構(gòu)造函數(shù)創(chuàng)建對(duì)象“b”和“c”,那么對(duì)象”a”則扮演了“Foo.prototype”這個(gè)角色:

// 構(gòu)造函數(shù)
function Foo(y) {
  // 構(gòu)造函數(shù)將會(huì)以特定模式創(chuàng)建對(duì)象:被創(chuàng)建的對(duì)象都會(huì)有"y"屬性
  this.y = y;
}

// "Foo.prototype"存放了新建對(duì)象的原型引用
// 所以我們可以將之用于定義繼承和共享屬性或方法
// 所以,和上例一樣,我們有了如下代碼:

// 繼承屬性"x"
Foo.prototype.x = 10;

// 繼承方法"calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// 使用foo模式創(chuàng)建 "b" and "c"
var b = new Foo(20);
var c = new Foo(30);

// 調(diào)用繼承的方法
b.calculate(30); // 60
c.calculate(40); // 80

// 讓我們看看是否使用了預(yù)期的屬性

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // "Foo.prototype"自動(dòng)創(chuàng)建了一個(gè)特殊的屬性"constructor"
  // 指向a的構(gòu)造函數(shù)本身
  // 實(shí)例"b"和"c"可以通過授權(quán)找到它并用以檢測(cè)自己的構(gòu)造函數(shù)

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

上述代碼可表示為如下的關(guān)系:

圖 4. 執(zhí)行上下文棧

當(dāng)一段程序開始時(shí),會(huì)先進(jìn)入全局執(zhí)行上下文環(huán)境[global execution context], 這個(gè)也是堆棧中最底部的元素。此全局程序會(huì)開始初始化,初始化生成必要的對(duì)象[objects]和函數(shù)[functions]. 在此全局上下文執(zhí)行的過程中,它可能會(huì)激活一些方法(當(dāng)然是已經(jīng)初始化過的),然后進(jìn)入他們的上下文環(huán)境,然后將新的元素壓入堆棧。在這些初始化都結(jié)束之后,這個(gè)系統(tǒng)會(huì)等待一些事件(例如用戶的鼠標(biāo)點(diǎn)擊等),會(huì)觸發(fā)一些方法,然后進(jìn)入一個(gè)新的上下文環(huán)境。

見圖5,有一個(gè)函數(shù)上下文“EC1″和一個(gè)全局上下文“Global EC”,下圖展現(xiàn)了從“Global EC”進(jìn)入和退出“EC1″時(shí)棧的變化:

?圖 6. 上下文結(jié)構(gòu)

除了這3個(gè)所需要的屬性(變量對(duì)象(variable object),this指針(this value),作用域鏈(scope chain) ),執(zhí)行上下文根據(jù)具體實(shí)現(xiàn)還可以具有任意額外屬性。接著,讓我們仔細(xì)來看看這三個(gè)屬性。

變量對(duì)象(Variable Object)

A variable object is a scope of data related with the execution context.
It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

變量對(duì)象(variable object) 是與執(zhí)行上下文相關(guān)的 數(shù)據(jù)作用域(scope of data) 。
它是與上下文關(guān)聯(lián)的特殊對(duì)象,用于存儲(chǔ)被定義在上下文中的 變量(variables) 和 函數(shù)聲明(function declarations) 。

注意:函數(shù)表達(dá)式[function expression](而不是函數(shù)聲明[function declarations,區(qū)別請(qǐng)參考本系列第2章])是不包含在VO[variable object]里面的。

變量對(duì)象(Variable Object)是一個(gè)抽象的概念,不同的上下文中,它表示使用不同的object。例如,在global全局上下文中,變量對(duì)象也是全局對(duì)象自身[global object]。(這就是我們可以通過全局對(duì)象的屬性來指向全局變量)。

讓我們看看下面例子中的全局執(zhí)行上下文情況:

var foo = 10;

function bar() {} // // 函數(shù)聲明
(function baz() {}); // 函數(shù)表達(dá)式

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // 引用錯(cuò)誤,baz沒有被定義

全局上下文中的變量對(duì)象(VO)會(huì)有如下屬性:

圖 8. 激活對(duì)象

同樣道理,function expression不在AO的行列。

對(duì)于這個(gè)AO的詳細(xì)內(nèi)容可以通過本系列教程第9章找到。

我們接下去要講到的是第三個(gè)主要對(duì)象。眾所周知,在ECMAScript中,我們會(huì)用到內(nèi)部函數(shù)[inner functions],在這些內(nèi)部函數(shù)中,我們可能會(huì)引用它的父函數(shù)變量,或者全局的變量。我們把這些變量對(duì)象成為上下文作用域?qū)ο骩scope object of the context]. 類似于上面討論的原型鏈[prototype chain],我們?cè)谶@里稱為作用域鏈[scope chain]。

作用域鏈(Scope Chains)

A scope chain is a list of objects that are searched for identifiers appear in the code of the context.
作用域鏈?zhǔn)且粋€(gè) 對(duì)象列表(list of objects) ,用以檢索上下文代碼中出現(xiàn)的 標(biāo)識(shí)符(identifiers) 。

作用域鏈的原理和原型鏈很類似,如果這個(gè)變量在自己的作用域中沒有,那么它會(huì)尋找父級(jí)的,直到最頂層。

標(biāo)示符[Identifiers]可以理解為變量名稱、函數(shù)聲明和普通參數(shù)。例如,當(dāng)一個(gè)函數(shù)在自身函數(shù)體內(nèi)需要引用一個(gè)變量,但是這個(gè)變量并沒有在函數(shù)內(nèi)部聲明(或者也不是某個(gè)參數(shù)名),那么這個(gè)變量就可以稱為自由變量[free variable]。那么我們搜尋這些自由變量就需要用到作用域鏈。

在一般情況下,一個(gè)作用域鏈包括父級(jí)變量對(duì)象(variable object)(作用域鏈的頂部)、函數(shù)自身變量VO和活動(dòng)對(duì)象(activation object)。不過,有些情況下也會(huì)包含其它的對(duì)象,例如在執(zhí)行期間,動(dòng)態(tài)加入作用域鏈中的—例如with或者catch語句。[譯注:with-objects指的是with語句,產(chǎn)生的臨時(shí)作用域?qū)ο?;catch-clauses指的是catch從句,如catch(e),這會(huì)產(chǎn)生異常對(duì)象,導(dǎo)致作用域變更]。

當(dāng)查找標(biāo)識(shí)符的時(shí)候,會(huì)從作用域鏈的活動(dòng)對(duì)象部分開始查找,然后(如果標(biāo)識(shí)符沒有在活動(dòng)對(duì)象中找到)查找作用域鏈的頂部,循環(huán)往復(fù),就像作用域鏈那樣。

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x"和"y"是自由變量
    // 會(huì)在作用域鏈的下一個(gè)對(duì)象中找到(函數(shù)”bar”的互動(dòng)對(duì)象之后)
    console.log(x + y + z);
  })();
})();

我們假設(shè)作用域鏈的對(duì)象聯(lián)動(dòng)是通過一個(gè)叫做parent的屬性,它是指向作用域鏈的下一個(gè)對(duì)象。這可以在Rhino Code中測(cè)試一下這種流程,這種技術(shù)也確實(shí)在ES5環(huán)境中實(shí)現(xiàn)了(有一個(gè)稱為outer鏈接).當(dāng)然也可以用一個(gè)簡單的數(shù)據(jù)來模擬這個(gè)模型。使用parent的概念,我們可以把上面的代碼演示成如下的情況。(因此,父級(jí)變量是被存在函數(shù)的[[Scope]]屬性中的)。

這種形式的作用域稱為靜態(tài)作用域[static/lexical scope]。上面的x變量就是在函數(shù)bar的[[Scope]]中搜尋到的。理論上來說,也會(huì)有動(dòng)態(tài)作用域[dynamic scope], 也就是上述的x被解釋為20,而不是10. 但是EMCAScript不使用動(dòng)態(tài)作用域。

“funarg problem”的另一個(gè)類型就是自上而下[”downward funarg problem”].在這種情況下,父級(jí)的上下會(huì)存在,但是在判斷一個(gè)變量值的時(shí)候會(huì)有多義性。也就是,這個(gè)變量究竟應(yīng)該使用哪個(gè)作用域。是在函數(shù)創(chuàng)建時(shí)的作用域呢,還是在執(zhí)行時(shí)的作用域呢?為了避免這種多義性,可以采用閉包,也就是使用靜態(tài)作用域。

請(qǐng)看下面的例子:

// 全局變量 "x"
var x = 10;

// 全局function
function foo() {
  console.log(x);
}

(function (funArg) {

  // 局部變量 "x"
  var x = 20;

  // 這不會(huì)有歧義
  // 因?yàn)槲覀兪褂?foo"函數(shù)的[[Scope]]里保存的全局變量"x",
  // 并不是caller作用域的"x"

  funArg(); // 10, 而不是20

})(foo); // 將foo作為一個(gè)"funarg"傳遞下去

從上述的情況,我們似乎可以斷定,在語言中,使用靜態(tài)作用域是閉包的一個(gè)強(qiáng)制性要求。不過,在某些語言中,會(huì)提供動(dòng)態(tài)和靜態(tài)作用域的結(jié)合,可以允許開發(fā)員選擇哪一種作用域。但是在ECMAScript中,只采用了靜態(tài)作用域。所以ECMAScript完全支持使用[[Scope]]的屬性。我們可以給閉包得出如下定義:

A closure is a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes.
Thus, via these saved scopes a function may easily refer free variables.
閉包是一系列代碼塊(在ECMAScript中是函數(shù)),并且靜態(tài)保存所有父級(jí)的作用域。通過這些保存的作用域來搜尋到函數(shù)中的自由變量。

請(qǐng)注意,因?yàn)槊恳粋€(gè)普通函數(shù)在創(chuàng)建時(shí)保存了[[Scope]],理論上,ECMAScript中所有函數(shù)都是閉包。

還有一個(gè)很重要的點(diǎn),幾個(gè)函數(shù)可能含有相同的父級(jí)作用域(這是一個(gè)很普遍的情況,例如有好幾個(gè)內(nèi)部或者全局的函數(shù))。在這種情況下,在[[Scope]]中存在的變量是會(huì)共享的。一個(gè)閉包中變量的變化,也會(huì)影響另一個(gè)閉包的。

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

上述代碼可以用這張圖來表示:

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)