圖 1. 一個含有原型的基本對象
為什麼需要原型呢,讓我們考慮 原型鏈 的概念來回答這個問題。
原型鏈(Prototype chain)
原型對象也是普通的對象,並且也有可能有自己的原型,如果一個原型對象的原型不為null的話,我們就稱之為原型鏈(prototype chain)。
A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties.原型鏈是一個由對象組成的有限對象鏈由於實現繼承和共享屬性。
想象一個這種情況,2個對象,大部分內容都一樣,只有一小部分不一樣,很明顯,在一個好的設計模式中,我們會需要重用那部分相同的,而不是在每個對象中重復定義那些相同的方法或者屬性。在基於類[class-based]的系統中,這些重用部分被稱為類的繼承 – 相同的部分放入class A,然後class B和class C從A繼承,並且可以聲明擁有各自的獨特的東西。
ECMAScript沒有類的概念。但是,重用[reuse]這個理念沒什麼不同(某些方面,甚至比class-更加靈活),可以由prototype chain原型鏈來實現。這種繼承被稱為delegation based inheritance-基於繼承的委托,或者更通俗一些,叫做原型繼承。
類似於類”A”,”B”,”C”,在ECMAScript中尼創建對象類”a”,”b”,”c”,相應地, 對象“a” 擁有對象“b”和”c”的共同部分。同時對象“b”和”c”只包含它們自己的附加屬性或方法。
復制代碼 代碼如下:
var a = { x: 10, calculate: function (z) { return this.x + this.y + z }}; var b = { y: 20, __proto__: a}; var c = { y: 30, __proto__: a}; // 調用繼承過來的方法b.calculate(30); // 60c.calculate(40); // 80
這樣看上去是不是很簡單啦。b和c可以使用a中定義的calculate方法,這就是有原型鏈來[prototype chain]實現的。
原理很簡單:如果在對象b中找不到calculate方法(也就是對象b中沒有這個calculate屬性), 那麼就會沿著原型鏈開始找。如果這個calculate方法在b的prototype中沒有找到,那麼就會沿著原型鏈找到a的prototype,一直遍歷完整個原型鏈。記住,一旦找到,就返回第一個找到的屬性或者方法。因此,第一個找到的屬性成為繼承屬性。如果遍歷完整個原型鏈,仍然沒有找到,那麼就會返回undefined。
注意一點,this這個值在一個繼承機制中,仍然是指向它原本屬於的對象,而不是從原型鏈上找到它時,它所屬於的對象。例如,以上的例子,this.y是從b和c中獲取的,而不是a。當然,你也發現了this.x是從a取的,因為是通過原型鏈機制找到的。
如果一個對象的prototype沒有顯示的聲明過或定義過,那麼__prototype__的默認值就是object.prototype, 而object.prototype也會有一個__prototype__, 這個就是原型鏈的終點了,被設置為null。
下面的圖示就是表示了上述a,b,c的繼承關系
圖 2. 原型鏈
原型鏈通常將會在這樣的情況下使用:對象擁有 相同或相似的狀態結構(same or similar state structure) (即相同的屬性集合)與 不同的狀態值(different state values)。在這種情況下,我們可以使用 構造函數(Constructor) 在 特定模式(specified pattern) 下創建對象。
構造函數(Constructor)
除了創建對象,構造函數(constructor) 還做了另一件有用的事情—自動為創建的新對象設置了原型對象(prototype object) 。原型對象存放於 ConstructorFunction.prototype 屬性中。
例如,我們重寫之前例子,使用構造函數創建對象“b”和“c”,那麼對象”a”則扮演了“Foo.prototype”這個角色:
復制代碼 代碼如下:
// 構造函數
function Foo(y) {
// 構造函數將會以特定模式創建對象:被創建的對象都會有"y"屬性
this.y = y;
}
// "Foo.prototype"存放了新建對象的原型引用
// 所以我們可以將之用於定義繼承和共享屬性或方法
// 所以,和上例一樣,我們有了如下代碼:
// 繼承屬性"x"
Foo.prototype.x = 10;
// 繼承方法"calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// 使用foo模式創建 "b" and "c"
var b = new Foo(20);
var c = new Foo(30);
// 調用繼承的方法
b.calculate(30); // 60
c.calculate(40); // 80
// 讓我們看看是否使用了預期的屬性
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// "Foo.prototype"自動創建了一個特殊的屬性"constructor"
// 指向a的構造函數本身
// 實例"b"和"c"可以通過授權找到它並用以檢測自己的構造函數
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
);
上述代碼可表示為如下的關系:
圖 3. 構造函數與對象之間的關系
上述圖示可以看出,每一個object都有一個prototype. 構造函數Foo也擁有自己的__proto__, 也就是Function.prototype, 而Function.prototype的__proto__指向了Object.prototype. 重申一遍,Foo.prototype只是一個顯式的屬性,也就是b和c的__proto__屬性。
這個問題完整和詳細的解釋可以在大叔即將翻譯的第18、19兩章找到。有兩個部分:面向對象編程.一般理論(OOP. The general theory),描述了不同的面向對象的范式與風格(OOP paradigms and stylistics),以及與ECMAScript的比較, 面向對象編程.ECMAScript實現(OOP. ECMAScript implementation), 專門講述了ECMAScript中的面向對象編程。
現在,我們已經了解了基本的object原理,那麼我們接下去來看看ECMAScript裡面的程序執行環境[runtime program execution]. 這就是通常稱為的“執行上下文堆棧”[execution context stack]。每一個元素都可以抽象的理解為object。你也許發現了,沒錯,在ECMAScript中,幾乎處處都能看到object的身影。
執行上下文棧(Execution Context Stack)
在ECMASscript中的代碼有三種類型:global, function和eval。
每一種代碼的執行都需要依賴自身的上下文。當然global的上下文可能涵蓋了很多的function和eval的實例。函數的每一次調用,都會進入函數執行中的上下文,並且來計算函數中變量等的值。eval函數的每一次執行,也會進入eval執行中的上下文,判斷應該從何處獲取變量的值。
注意,一個function可能產生無限的上下文環境,因為一個函數的調用(甚至遞歸)都產生了一個新的上下文環境。
復制代碼 代碼如下:
function foo(bar) {}
// 調用相同的function,每次都會產生3個不同的上下文
//(包含不同的狀態,例如參數bar的值)
foo(10);
foo(20);
foo(30);
一個執行上下文可以激活另一個上下文,就好比一個函數調用了另一個函數(或者全局的上下文調用了一個全局函數),然後一層一層調用下去。邏輯上來說,這種實現方式是棧,我們可以稱之為上下文堆棧。
激活其它上下文的某個上下文被稱為 調用者(caller) 。被激活的上下文被稱為被調用者(callee) 。被調用者同時也可能是調用者(比如一個在全局上下文中被調用的函數調用某些自身的內部方法)。
當一個caller激活了一個callee,那麼這個caller就會暫停它自身的執行,然後將控制權交給這個callee. 於是這個callee被放入堆棧,稱為進行中的上下文[running/active execution context]. 當這個callee的上下文結束之後,會把控制權再次交給它的caller,然後caller會在剛才暫停的地方繼續執行。在這個caller結束之後,會繼續觸發其他的上下文。一個callee可以用返回(return)或者拋出異常(exception)來結束自身的上下文。
如下圖,所有的ECMAScript的程序執行都可以看做是一個執行上下文堆棧[execution context (EC) stack]。堆棧的頂部就是處於激活狀態的上下文。
圖 4. 執行上下文棧
當一段程序開始時,會先進入全局執行上下文環境[global execution context], 這個也是堆棧中最底部的元素。此全局程序會開始初始化,初始化生成必要的對象[objects]和函數[functions]. 在此全局上下文執行的過程中,它可能會激活一些方法(當然是已經初始化過的),然後進入他們的上下文環境,然後將新的元素壓入堆棧。在這些初始化都結束之後,這個系統會等待一些事件(例如用戶的鼠標點擊等),會觸發一些方法,然後進入一個新的上下文環境。
見圖5,有一個函數上下文“EC1″和一個全局上下文“Global EC”,下圖展現了從“Global EC”進入和退出“EC1″時棧的變化:
圖 5. 執行上下文棧的變化
ECMAScript運行時系統就是這樣管理代碼的執行。
關於ECMAScript執行上下文棧的內容請查閱本系列教程的第11章執行上下文(Execution context)。
如上所述,棧中每一個執行上下文可以表示為一個對象。讓我們看看上下文對象的結構以及執行其代碼所需的 狀態(state) 。
執行上下文(Execution Context)
一個執行的上下文可以抽象的理解為object。每一個執行的上下文都有一系列的屬性(我們稱為上下文狀態),他們用來追蹤關聯代碼的執行進度。這個圖示就是一個context的結構。
圖 6. 上下文結構
除了這3個所需要的屬性(變量對象(variable object),this指針(this value),作用域鏈(scope chain) ),執行上下文根據具體實現還可以具有任意額外屬性。接著,讓我們仔細來看看這三個屬性。
變量對象(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.
變量對象(variable object) 是與執行上下文相關的 數據作用域(scope of data) 。
它是與上下文關聯的特殊對象,用於存儲被定義在上下文中的 變量(variables) 和 函數聲明(function declarations) 。
復制代碼
注意:函數表達式[function expression](而不是函數聲明[function declarations,區別請參考本系列第2章])是不包含在VO[variable object]裡面的。
變量對象(Variable Object)是一個抽象的概念,不同的上下文中,它表示使用不同的object。例如,在global全局上下文中,變量對象也是全局對象自身[global object]。(這就是我們可以通過全局對象的屬性來指向全局變量)。
讓我們看看下面例子中的全局執行上下文情況:
復制代碼 代碼如下:
var foo = 10;
function bar() {} // // 函數聲明
(function baz() {}); // 函數表達式
console.log(
this.foo == foo, // true
window.bar == bar // true
);
console.log(baz); // 引用錯誤,baz沒有被定義
全局上下文中的變量對象(VO)會有如下屬性:
圖 7. 全局變量對象
如上所示,函數“baz”如果作為函數表達式則不被不被包含於變量對象。這就是在函數外部嘗試訪問產生引用錯誤(ReferenceError) 的原因。請注意,ECMAScript和其他語言相比(比如C/C++),僅有函數能夠創建新的作用域。在函數內部定義的變量與內部函數,在外部非直接可見並且不污染全局對象。使用 eval 的時候,我們同樣會使用一個新的(eval創建)執行上下文。eval會使用全局變量對象或調用者的變量對象(eval的調用來源)。
那函數以及自身的變量對象又是怎樣的呢?在一個函數上下文中,變量對象被表示為活動對象(activation object)。
活動對象(activation object)
當函數被調用者激活,這個特殊的活動對象(activation object) 就被創建了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具有索引屬性的參數映射表)。活動對象在函數上下文中作為變量對象使用。
即:函數的變量對象保持不變,但除去存儲變量與函數聲明之外,還包含以及特殊對象arguments 。
考慮下面的情況:
復制代碼 代碼如下:
function foo(x, y) {
var z = 30;
function bar() {} // 函數聲明
(function baz() {}); // 函數表達式
}
foo(10, 20);
“foo”函數上下文的下一個激活對象(AO)如下圖所示:
圖 8. 激活對象
同樣道理,function expression不在AO的行列。
對於這個AO的詳細內容可以通過本系列教程第9章找到。
我們接下去要講到的是第三個主要對象。眾所周知,在ECMAScript中,我們會用到內部函數[inner functions],在這些內部函數中,我們可能會引用它的父函數變量,或者全局的變量。我們把這些變量對象成為上下文作用域對象[scope object of the context]. 類似於上面討論的原型鏈[prototype chain],我們在這裡稱為作用域鏈[scope chain]。
作用域鏈(Scope Chains)
A scope chain is a list of objects that are searched for identifiers appear in the code of the context.
作用域鏈是一個 對象列表(list of objects) ,用以檢索上下文代碼中出現的 標識符(identifiers) 。
復制代碼
作用域鏈的原理和原型鏈很類似,如果這個變量在自己的作用域中沒有,那麼它會尋找父級的,直到最頂層。
標示符[Identifiers]可以理解為變量名稱、函數聲明和普通參數。例如,當一個函數在自身函數體內需要引用一個變量,但是這個變量並沒有在函數內部聲明(或者也不是某個參數名),那麼這個變量就可以稱為自由變量[free variable]。那麼我們搜尋這些自由變量就需要用到作用域鏈。
在一般情況下,一個作用域鏈包括父級變量對象(variable object)(作用域鏈的頂部)、函數自身變量VO和活動對象(activation object)。不過,有些情況下也會包含其它的對象,例如在執行期間,動態加入作用域鏈中的—例如with或者catch語句。[譯注:with-objects指的是with語句,產生的臨時作用域對象;catch-clauses指的是catch從句,如catch(e),這會產生異常對象,導致作用域變更]。
當查找標識符的時候,會從作用域鏈的活動對象部分開始查找,然後(如果標識符沒有在活動對象中找到)查找作用域鏈的頂部,循環往復,就像作用域鏈那樣。
復制代碼 代碼如下:
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x"和"y"是自由變量
// 會在作用域鏈的下一個對象中找到(函數”bar”的互動對象之後)
console.log(x + y + z);
})();
})();
我們假設作用域鏈的對象聯動是通過一個叫做__parent__的屬性,它是指向作用域鏈的下一個對象。這可以在Rhino Code中測試一下這種流程,這種技術也確實在ES5環境中實現了(有一個稱為outer鏈接).當然也可以用一個簡單的數據來模擬這個模型。使用__parent__的概念,我們可以把上面的代碼演示成如下的情況。(因此,父級變量是被存在函數的[[Scope]]屬性中的)。
圖 9. 作用域鏈
在代碼執行過程中,如果使用with或者catch語句就會改變作用域鏈。而這些對象都是一些簡單對象,他們也會有原型鏈。這樣的話,作用域鏈會從兩個維度來搜尋。
我們再看下面這個例子:
復制代碼 代碼如下:
Object.prototype.x = 10;
var w = 20;
var y = 30;
// 在SpiderMonkey全局對象裡
// 例如,全局上下文的變量對象是從"Object.prototype"繼承到的
// 所以我們可以得到“沒有聲明的全局變量”
// 因為可以從原型鏈中獲取
console.log(x); // 10
(function foo() {
// "foo" 是局部變量
var w = 40;
var x = 100;
// "x" 可以從"Object.prototype"得到,注意值是10哦
// 因為{z: 50}是從它那裡繼承的
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// 在"with"對象從作用域鏈刪除之後
// x又可以從foo的上下文中得到了,注意這次值又回到了100哦
// "w" 也是局部變量
console.log(x, w); // 100, 40
// 在浏覽器裡
// 我們可以通過如下語句來得到全局的w值
console.log(window.w); // 20
})();
我們就會有如下結構圖示。這表示,在我們去搜尋__parent__之前,首先會去__proto__的鏈接中。
圖 10. with增大的作用域鏈
注意,不是所有的全局對象都是由Object.prototype繼承而來的。上述圖示的情況可以在SpiderMonkey中測試。
只要所有外部函數的變量對象都存在,那麼從內部函數引用外部數據則沒有特別之處——我們只要遍歷作用域鏈表,查找所需變量。然而,如上文所提及,當一個上下文終止之後,其狀態與自身將會被 銷毀(destroyed) ,同時內部函數將會從外部函數中返回。此外,這個返回的函數之後可能會在其他的上下文中被激活,那麼如果一個之前被終止的含有一些自由變量的上下文又被激活將會怎樣?通常來說,解決這個問題的概念在ECMAScript中與作用域鏈直接相關,被稱為 (詞法)閉包((lexical) closure)。
閉包(Closures)
在ECMAScript中,函數是“第一類”對象。這個名詞意味著函數可以作為參數被傳遞給其他函數使用 (在這種情況下,函數被稱為“funargs”——“functional arguments”的縮寫[譯注:這裡不知翻譯為泛函參數是否恰當])。接收“funargs”的函數被稱之為 高階函數(higher-order functions) ,或者更接近數學概念的話,被稱為 運算符(operators) 。其他函數的運行時也會返回函數,這些返回的函數被稱為 function valued 函數 (有 functional value 的函數)。
“funargs”與“functional values”有兩個概念上的問題,這兩個子問題被稱為“Funarg problem” (“泛函參數問題”)。要准確解決泛函參數問題,需要引入 閉包(closures) 到的概念。讓我們仔細描述這兩個問題(我們可以見到,在ECMAScript中使用了函數的[[Scope]]屬性來解決這個問題)。
“funarg problem”的一個子問題是“upward funarg problem”[譯注:或許可以翻譯為:向上查找的函數參數問題]。當一個函數從其他函數返回到外部的時候,這個問題將會出現。要能夠在外部上下文結束時,進入外部上下文的變量,內部函數 在創建的時候(at creation moment) 需要將之存儲進[[Scope]]屬性的父元素的作用域中。然後當函數被激活時,上下文的作用域鏈表現為激活對象與[[Scope]]屬性的組合(事實上,可以在上圖見到):
Scope chain = Activation object + [[Scope]]
作用域鏈 = 活動對象 + [[Scope]]
請注意,最主要的事情是——函數在被創建時保存外部作用域,是因為這個 被保存的作用域鏈(saved scope chain) 將會在未來的函數調用中用於變量查找。
復制代碼 代碼如下:
function foo() {
var x = 10;
return function bar() {
console.log(x);
};
}
// "foo"返回的也是一個function
// 並且這個返回的function可以隨意使用內部的變量x
var returnedFunction = foo();
// 全局變量 "x"
var x = 20;
// 支持返回的function
returnedFunction(); // 結果是10而不是20
這種形式的作用域稱為靜態作用域[static/lexical scope]。上面的x變量就是在函數bar的[[Scope]]中搜尋到的。理論上來說,也會有動態作用域[dynamic scope], 也就是上述的x被解釋為20,而不是10. 但是EMCAScript不使用動態作用域。
“funarg problem”的另一個類型就是自上而下[”downward funarg problem”].在這種情況下,父級的上下會存在,但是在判斷一個變量值的時候會有多義性。也就是,這個變量究竟應該使用哪個作用域。是在函數創建時的作用域呢,還是在執行時的作用域呢?為了避免這種多義性,可以采用閉包,也就是使用靜態作用域。
請看下面的例子:
復制代碼 代碼如下:
// 全局變量 "x"
var x = 10;
// 全局function
function foo() {
console.log(x);
}
(function (funArg) {
// 局部變量 "x"
var x = 20;
// 這不會有歧義
// 因為我們使用"foo"函數的[[Scope]]裡保存的全局變量"x",
// 並不是caller作用域的"x"
funArg(); // 10, 而不是20
})(foo); // 將foo作為一個"funarg"傳遞下去
從上述的情況,我們似乎可以斷定,在語言中,使用靜態作用域是閉包的一個強制性要求。不過,在某些語言中,會提供動態和靜態作用域的結合,可以允許開發員選擇哪一種作用域。但是在ECMAScript中,只采用了靜態作用域。所以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中是函數),並且靜態保存所有父級的作用域。通過這些保存的作用域來搜尋到函數中的自由變量。
復制代碼
請注意,因為每一個普通函數在創建時保存了[[Scope]],理論上,ECMAScript中所有函數都是閉包。
還有一個很重要的點,幾個函數可能含有相同的父級作用域(這是一個很普遍的情況,例如有好幾個內部或者全局的函數)。在這種情況下,在[[Scope]]中存在的變量是會共享的。一個閉包中變量的變化,也會影響另一個閉包的。
復制代碼 代碼如下:
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
);
上述代碼可以用這張圖來表示:
圖 11. 共享的[[Scope]]
在某個循環中創建多個函數時,上圖會引發一個困惑。如果在創建的函數中使用循環變量(如”k”),那麼所有的函數都使用同樣的循環變量,導致一些程序員經常會得不到預期值。現在清楚為什麼會產生如此問題了——因為所有函數共享同一個[[Scope]],其中循環變量為最後一次復賦值。
復制代碼 代碼如下:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
有一些用以解決這類問題的技術。其中一種技巧是在作用域鏈中提供一個額外的對象,比如增加一個函數:
復制代碼 代碼如下:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
alert(x);
};
})(k); // 將k當做參數傳遞進去
}
// 結果正確
data[0](); // 0
data[1](); // 1
data[2](); // 2
閉包理論的深入研究與具體實踐可以在本系列教程第16章閉包(Closures)中找到。如果想得到關於作用域鏈的更多信息,可以參照本系列教程第14章作用域鏈(Scope chain)。
下一章節將會討論一個執行上下文的最後一個屬性——this指針的概念。
This指針
A this value is a special object which is related with the execution context.
Therefore, it may be named as a context object (i.e. an object in which context the execution context is activated).
this適合執行的上下文環境息息相關的一個特殊對象。因此,它也可以稱為上下文對象[context object](激活執行上下文的上下文)。
復制代碼
任何對象都可以作為上下文的this值。我想再次澄清對與ECMAScript中,與執行上下文相關的一些描述——特別是this的誤解。通常,this 被錯誤地,描述為變量對象的屬性。最近比如在這本書中就發現了(盡管書中提及this的那一章還不錯)。 請牢記:
a this value is a property of the execution context, but not a property of the variable object.
this是執行上下文環境的一個屬性,而不是某個變量對象的屬性
復制代碼
這個特點很重要,因為和變量不同,this是沒有一個類似搜尋變量的過程。當你在代碼中使用了this,這個 this的值就直接從執行的上下文中獲取了,而不會從作用域鏈中搜尋。this的值只取決中進入上下文時的情況。
順便說一句,和ECMAScript不同,Python有一個self的參數,和this的情況差不多,但是可以在執行過程中被改變。在ECMAScript中,是不可以給this賦值的,因為,還是那句話,this不是變量。
在global context(全局上下文)中,this的值就是指全局這個對象,這就意味著,this值就是這個變量本身。
復制代碼 代碼如下:
var x = 10;
console.log(
x, // 10
this.x, // 10
window.x // 10
);
在函數上下文[function context]中,this會可能會根據每次的函數調用而成為不同的值.this會由每一次caller提供,caller是通過調用表達式[call expression]產生的(也就是這個函數如何被激活調用的)。例如,下面的例子中foo就是一個callee,在全局上下文中被激活。下面的例子就表明了不同的caller引起this的不同。
復制代碼 代碼如下:
// "foo"函數裡的alert沒有改變
// 但每次激活調用的時候this是不同的
function foo() {
alert(this);
}
// caller 激活 "foo"這個callee,
// 並且提供"this"給這個 callee
foo(); // 全局對象
foo.prototype.constructor(); // foo.prototype
var bar = {
baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // 這是一個全局對象
(bar.baz, bar.baz)(); // 也是全局對象
(false || bar.baz)(); // 也是全局對象
var otherFoo = bar.baz;
otherFoo(); // 還是全局對象
如果要深入思考每一次函數調用中,this值的變化(更重要的是怎樣變化),你可以閱讀本系列教程第10章This。上文所提及的情況都會在此章內詳細討論。
總結(Conclusion)
在此我們完成了一個簡短的概述。盡管看來不是那麼簡短,但是這些話題若要完整表述完畢,則需要一整本書。.我們沒有提及兩個重要話題:函數(functions) (以及不同類型的函數之間的不同,比如函數聲明與函數表達式)與ECMAScript的 求值策略(evaluation strategy) 。這兩個話題可以分別查閱本系列教程第15章函數(Functions) 與第19章求值策略(Evaluation strategy)。
如果你有任何評論,問題或者補充,我很歡迎在文章評論中討論。
祝大家學習ECMAScript順利。