前言
在ECMAScript中,有兩個最常用的創建函數對象的方法,即使用函數表達式或者使用函數聲明。對此,ECMAScript規范明確了一點,即是,即函數聲明必須始終帶有一個標識符(Identifier),也就是我們所說的函數名,而函數表達式則可以省略。下面看看這兩者的詳細區別介紹。
什麼是Function Declaration(函數聲明)?
Function Declaration 可以定義命名的函數變量,而無需給變量賦值。 Function Declaration 是一種獨立的結構,不能嵌套在非功能模塊中。可以將它類比為Variable Declaration(變量聲明)。就像Variable Declaration 必須以“var”開頭一樣,Function Declaration 必須以“function”開頭。
舉例來說
function bar() { return 3;}ECMA 5(13.0)定義語法:
function Identifier ( FormalParameterList[opt] ) { FunctionBody }
函數名在自身作用域和父作用域內是可獲取的(否則就取不到函數了)。
function bar() { return 3;} bar() //3bar //function什麼是Function Expression(函數表達式)?
Function Expression 將函數定義為表達式語句(通常是變量賦值)的一部分。通過Function Expression 定義的函數可以是命名的,也可以是匿名的。 Function Expression 不能以“function”開頭(下面自調用的例子要用括號將其括起來)。
舉例來說
//anonymous function expressionvar a = function() { return 3;} //named function expressionvar 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 現在還不是標準,所以不建議應用在產品開發中。
下面我們從一些小測試開始。猜猜以下情況都會彈出什麼結果?
題1:
function foo(){ function bar() { return 3; } return bar(); function bar() { return 8; }}alert(foo());題2:
function foo(){ var bar = function() { return 3; }; return bar(); var bar = function() { return 8; };}alert(foo());題3:
alert(foo());function foo(){ var bar = function() { return 3; }; return bar(); var bar = function() { return 8; };}題4:
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] 的話,就繼續往下讀吧……(即使答對了也要繼續讀哦)
現在來解釋下前面的測試。
Question 1 用了function declaration,也就是說它們get hoisted(被提升)了……
等一下,什麼是Hoisting?
這裡引用Ben Cherry的話:“Function declaration和function variable(函數變量)通常會被JavaScript 解釋器移('hoisted')到當前作用域頂部”。
function declaration 被提升時,整個函數體都會隨之提升,所以Question 1 的代碼經過解釋器解釋後是像這樣運行的:
//**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 中的第一個表達式:
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 的代碼會按以下順序運行:
//**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 在開始就被聲明了,但是它的值並沒有定義。其它代碼都是按順序執行的。
//**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 一樣)。
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 看著像創建了對象。
//Function Declarationfunction add(a,b) {return a + b};//Function Expressionvar 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(函數式編程)分不開。
//Function Expressionvar sayHello = alert.curry("hello!");Function Expression 有缺點嗎?
Function Expression 創建的函數大多是匿名的。比如下面的函數是匿名的,today 只是一個匿名函數的引用:
var today = function() {return new Date()}這會有問題嗎?多數情況下不會,但是就像Nick Fitzgerald 指出的,調試匿名函數會很煩。他建議使用Named Function Expressions (NFEs)作為工作區:
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 還是很有用的。這沒什麼。死記教條是危險的,並且通常會造成代碼拐彎抹角。更重要的是你理解了概念,這樣就可以根據自身情況決定用哪種方式創建函數。以上就是本文的全部內容了,希望此文對大家在這方面有幫助。