記得在面試騰訊實習生的時候,面試官問了我這樣一道問題。
當初只知道兩種宣告方式一個是函數宣告一個是函數表達式,具體有什麼不同沒能說得很好。最近剛好看到這方面的書籍,就想好好總結一番。
在ECMAScript中,有兩個最常用的建立函數物件的方法,即使用函數表達式或使用函數宣告。對此,ECMAScript規範明確了一點,即是,即函數宣告 必須始終帶有一個標識符(Identifier),也就是我們所說的函數名,而函數表達式則可以省略。
函數宣告:
1. 建立一個new Function對象,FormalParameterList指定參數,FunctionBody指定函數體。將目前正在運行環境中作用域鏈作為它的作用域。
2. 為目前變數物件建立一個名為Identifier的屬性,值為Result(1)。
函數表達式:
(函數表達式分為匿名與具名函數表達式)
具名函數表達式的解析過程如下:
1. 建立一個new Object物件
2. 將Result(1)加入作用域鏈的頂端
3. 建立一個new Function對象,FormalParameterList指定參數,FunctionBody指定函數體。將目前正在運行的執行環境中作用域鏈作為它的作用域。
4. 為Result(1)建立一個名為Identifier 的屬性,其值為Result(3),唯讀,不可刪除
5. 從作用域鏈移除Result(1)
6. 返回Result(3)
官方文件讀起來十分拗口。簡單來說,ECMAScript是透過上下文來區分這兩者的:假如 function foo(){} 是一個賦值表達式的一部分,則認為它是一個函數表達式。而如果 function foo(){} 被包含在一個函數體內,或位於程式(的最上層)中,則將它作為一個函數宣告來解析。顯然,在省略標識符的情況下,「表達式」 也就只能是表達式了。
還有一種情況:
這種情況也是函數表達式,它被包含在一對圓括號中的函數,在其上下文環境中,()構成了一個分組運算符,而分組運算符只能包含表達式,更多的例子:
下面簡單說說函數宣告與函數表達式的異同。聲明和表達式的行為存在著十分微妙而又十分重要的差異。
首先,函數宣告會在任何表達式被解析和求值之前先行被解析和求值。即使宣告位於原始碼中的最後一行,它也會先於同一作用域中位於最前面的表達式被求值。還是看例子比較容易理解。在下面這個例子中,函數 fn 是在 alert 後面宣告的。但是,在alert 執行的時候,fn已經有定義了:
簡單總結,區別在什麼地方呢?
1. 宣告總是在作用域開始時先行解析;
2. 表達式在遇到時候才運算。
函數宣告還有另外一個重要的特點,即透過條件語句控制函數宣告的行為並未標準化,因此不同環境下可能會得到不同的結果。即是:
那麼,使用函數宣告的實際規則到底是什麼?
FunctionDeclaration(函數宣告)只能出現在Program(程式)或FunctionBody(函數體)內。從句法上講,它們 不能出現在Block(區塊)({ ... })中,例如不能出現在 if、while 或 for 語句中。因為 Block(區塊) 中只能包含Statement(語句), 而不能包含FunctionDeclaration(函數宣告)這樣的SourceElement(來源元素)。
另一方面,仔細看產生規則也會發現,唯一可能讓Expression(表達式)出現在Block(區塊)中情形,就是讓它作為ExpressionStatement(表達式語句)的一部分。但是,規範明確規定了ExpressionStatement(表達式語句)不能以關鍵字function開頭。而這其實就是說,FunctionExpression(函數表達式)同樣也不能出現在Statement(語句)或Block(區塊)中(別忘了Block(區塊)就是由Statement(語句)構成的)。
由於存在上述限制,只要函數出現在區塊中(像上面例子中),實際上就應該將其看作一個語法錯誤,而不是什麼函數宣告或表達式。
那麼我們應該在什麼時候使用函數宣告或函數表達式呢?函數宣告只能出現在「程式碼」中,意味著只能在其它函數體中或全域空間;它們的定義不能不能賦值給一個變數或屬性,或作為一個參數傳遞出現在函數呼叫中;下面的例子是函數宣告的允許的用法,foo(),bar()和local()都是透過函數宣告模式宣告:
當你在語法上不能使用函數宣告的時候,你就可以使用函數表達式。例如:傳遞一個函數作為參數或在物件字面量中定義一個函數:
學問有限,如有錯誤,歡迎指正。