我們從一些小測試開始。以下情況都會彈出什麼結果?
題 1:
1 2 3 4 5 6 7 8 9 10 |
function foo(){ function bar() { return 3; } return bar(); function bar() { return 8; } } alert(foo()); |
題 2:
1 2 3 4 5 6 7 8 9 10 |
function foo(){ var bar = function() { return 3; }; return bar(); var bar = function() { return 8; }; } alert(foo()); |
題 3:
1 2 3 4 5 6 7 8 9 10 |
alert(foo()); function foo(){ var bar = function() { return 3; }; return bar(); var bar = function() { return 8; }; } |
題 4:
1 2 3 4 5 6 7 8 9 10 |
function foo(){ return bar(); var bar = function() { return 3; }; var bar = function() { return 8; }; } alert(foo()); |
如果你的答案不是8、3、3和 [Type Error: bar is not a function] 的話,就繼續往下讀吧……(即使答對了也要繼續讀哦)
什麼是 Function Declaration(函式宣告)?
Function Declaration 可以定義命名的函式變數,而無需給變數賦值。Function Declaration 是一種獨立的結構,不能巢狀在非功能模組中。可以將它類比為 Variable Declaration(變數宣告)。就像 Variable Declaration 必須以“var”開頭一樣,Function Declaration 必須以“function”開頭。
e.g.
1 2 3 |
function bar() { return 3; } |
ECMA 5(13.0)定義語法:
function Identifier ( FormalParameterList[opt] ) { FunctionBody }
函式名在自身作用域和父作用域內是可獲取的(否則就取不到函式了)。
1 2 3 4 5 6 |
function bar() { return 3; } bar() //3 bar //function |
什麼是 Function Expression(函式表示式)?
Function Expression 將函式定義為表示式語句(通常是變數賦值)的一部分。通過 Function Expression 定義的函式可以是命名的,也可以是匿名的。Function Expression 不能以“function”開頭(下面自呼叫的例子要用括號將其括起來)。
e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//anonymous function expression var a = function() { return 3; } //named function expression var a = function bar() { return 3; } //self invoking function expression (function sayHello() { alert("hello!"); })(); |
ECMA 5(13.0)定義語法:
function Identifieropt ( FormalParameterList[opt] ) { FunctionBody }
(這個定義感覺並不完整,因為它忽略了一個條件:外圍語句是表示式,並且不以“function”開頭)
函式名(如果有的話)在作用域外是不可獲取的(與 Function Declaration 對比)。
那 Function Statement 是什麼?
Function Statement 有時是 Function Declaration 的另一種說法。但是 kangax 指出,在 mozilla 中,Function Statement 是 Function Declaration 的一種擴充,使得 Function Declaration 語句可以在任何允許使用 statement(語句)的地方使用。但是 Function Statement 現在還不是標準,所以不建議應用在產品開發中。
現在來解釋下前面的測試。
Question 1 用了 function declaration,也就是說它們 get hoisted(被提升)了……
等一下,什麼是 Hoisting?
這裡引用 Ben Cherry的話:“Function declaration和function variable(函式變數)通常會被 JavaScript 直譯器移(‘hoisted’)到當前作用域頂部”。
function declaration 被提升時,整個函式體都會隨之提升,所以 Question 1 的程式碼經過直譯器解釋後是像這樣執行的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//**Simulated processing sequence for Question 1** function foo(){ //define bar once function bar() { return 3; } //redefine it function bar() { return 8; } //return its invocation return bar(); //8 } alert(foo()); |
但是,我們經常被告訴說,return 語句後面的程式碼是執行不到的啊……
執行 JavaScript 過程中,有 Context(ECMA 5 將之分解為 LexicalEnvironment、VariableEnvironment 和 ThisBinding)和 Process(一系列按序呼叫的語句)兩個概念。當程式進入執行域時,Declaration 會造成 VariableEnvironment。它們不同於 Statement(比如 return),也不遵循 Statement 的執行規則。
Function Expression 會被提升嗎?
這取決於表示式。比如 Question 2 中的第一個表示式:
1 2 3 |
var bar = function() { return 3; }; |
等號左邊的(var bar
)是 Variable Declaration。Variable Declaration 會被提升,但是 Assignment Expression(賦值表示式)不會。所以當 bar 提升時,直譯器會這樣初始化:var bar = undefined
。而函式定義本身不會被提升。
(ECMA 5 12.2 帶有 initialzier
(初始化器)的變數是在 VariableStatement
執行時,由 AssignmentExpression
賦值的,而不是在變數被建立時。)
因此 Question 2 的程式碼會按以下順序執行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//**Simulated processing sequence for Question 2** function foo(){ //a declaration for each function expression var bar = undefined; var bar = undefined; //first Function Expression is executed bar = function() { return 3; }; // Function created by first Function Expression is invoked return bar(); // second Function Expression unreachable } alert(foo()); //3 |
你可能會說,這還能解釋的通,但是 Question 3 的答案錯了,我在 Firebug 執行會報錯。
把程式碼儲存在 HTML 檔案中,之後在 Firefox 上執行試試。或者在 IE8、Chrome 或 Safari 控制檯中執行。顯然 Firebug 控制檯在“global(全域性)”作用域(實際並不是全域性的,而是特有的“Firebug”作用域——試著在 Firebug 控制檯中執行“this == window”你就知道了)執行程式碼時,不會將函式提升。
Question 3 和 Question 1 的邏輯相似。這次是 foo 函式被提升了。
Question 4 就很簡單了,根本就沒有函式提升……
可以這麼說,但是如果根本沒有提升的話,TypeError 會是“bar not defined”,而不是“bar not a function”。此例中確實沒有函式提升,但是有變數提升。因此 bar 在開始就被宣告瞭,但是它的值並沒有定義。其它程式碼都是按順序執行的。
1 2 3 4 5 6 7 8 9 |
//**Simulated processing sequence for Question 4** function foo(){ //a declaration for each function expression var bar = undefined; var bar = undefined; return bar(); //TypeError: "bar not defined" //neither Function Expression is reached } alert(foo()); |
還應該注意什麼?
官方是禁止在非功能模組(比如 if)中使用 Function Declaration 的。但是所有瀏覽器都支援,但是各自的解釋方式不同。
例如下面的程式碼段在 Firefox 3.6 中會拋錯,因為它將 Function Declaration 解釋成了 Function Statement(見上文),所以 x 沒有定義。但是在 IE8、Chrome 5 和 Safari 5 中,會返回函式 x(和標準的 Function Declaration 一樣)。
1 2 3 4 5 6 7 |
function foo() { if(false) { function x() {}; } return x; } alert(foo()); |
可以看出使用 Function Declaration 可能會引起混淆,那麼它有什麼優點嗎?
你可能會說 Function Declaration 很寬鬆啊——如果試圖在宣告前使用函式,提升確實可以修正順序,以便函式可以正確呼叫。但是這種寬鬆不利於嚴謹的編碼,從長遠的角度來看,很有可能會促進而不是阻止意外的發生。畢竟,程式設計師按特定的順序排列語句是有原因的。
那麼還有其它理由支援 Function Expression 的嗎?
你猜呢?
1)Function Declaration 感覺像是要模仿 Java 風格的方法宣告,但是 Java 方法和 JavaScript 並不一樣。在 JavaScript 中,函式是含值的 living 物件。Java 方法僅是對後設資料的儲存。下面的兩段程式碼都定義了函式,但是隻有 Function Expression 看著像建立了物件。
1 2 3 4 |
//Function Declaration function add(a,b) {return a + b}; //Function Expression var add = function(a,b) {return a + b}; |
2)Function Expression 用處更多。Function Declaration 只能作為“statement”孤立存在。它所能做的就是建立一個當前作用域下的物件變數。相比之下,Function Expression(根據定義)是大型結構的一部分。如果想要建立匿名函式、給 prototype(原型)新增函式或是將函式用作其它物件的 property(屬性),都可以用 Function Expression。每當用高階應用,比如 curry 或 compose,建立新的函式時都是在用 Function Expression。Function Expression 和 Functional Programming(函數語言程式設計)分不開。
1 2 |
//Function Expression var sayHello = alert.curry("hello!"); |
Function Expression 有缺點嗎?
Function Expression 建立的函式大多是匿名的。比如下面的函式是匿名的,today
只是一個匿名函式的引用:
1 |
var today = function() {return new Date()} |
這會有問題嗎?多數情況下不會,但是就像 Nick Fitzgerald 指出的,除錯匿名函式會很煩。他建議使用 Named Function Expressions (NFEs)作為工作區:
1 |
var today = function today() {return new Date()} |
但是如 Asen Bozhilov 所說(和 Kangax 文件)NFEs 在 IE9 以下無法正確執行。
結論
隨意放置的 Function Declaration 具有誤導性,並且很少有(如果有的話)情況,用 Function Expression 給變數賦值無法替代 Function Declaration。但是如果必須使用 Function Declaration 的話,將其放在所屬作用域頂部可以減少混淆。永遠不要把 Function Declaration 放在 if 語句中。
說了這麼多,可能在你自己的情況下,Function Declaration 還是很有用的。這沒什麼。死記教條是危險的,並且通常會造成程式碼拐彎抹角。更重要的是你理解了概念,這樣就可以根據自身情況決定用哪種方式建立函式。我希望此文在這方面有幫助。
歡迎討論。如果你覺得我哪裡說的不對或是有所補充,請聯絡我。
同見 第5版 ECMA-262 的 10.5、12.2、13.0、13.2 部分
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式