它是什麼
在 JavaScript 裡,每個函數,當被調用時,都會建立一個新的執行內容。因為在函數裡定義的變數和函數是唯一在內部被訪問的變數,而不是在外部被訪問的變數,當調用函數時,函數提供的上下文提供了一個非常簡單的方法建立私人變數。
function makeCounter() { var i = 0; return function(){ console.log(++i); }; }//記住:`counter`和`counter2`都有他們自己的變數 `i`var counter = makeCounter();counter();//1counter();//2var counter2 = makeCounter();counter2();//1counter2();//2i;//ReferenceError: i is not defined(它只存在於makeCounter裡)
在許多情況下,你可能並不需要makeWhatever這樣的函數返回多次累加值,並且可以只調用一次得到一個單一的值,在其他一些情況裡,你甚至不需要明確的知道傳回值。
它的核心
現在,無論你定義一個函數像這樣function foo(){}或者var foo = function(){},調用時,你都需要在後面加上一對圓括弧,像這樣foo()。
//向下面這樣定義的函數可以通過在函數名後加一對括弧進行調用,像這樣`foo()`,//因為foo相對於函數運算式`function(){/* code */}`只是一個引用變數var foo = function(){/* code */}//那這可以說明函數運算式可以通過在其後加上一對括弧自己調用自己嗎?function(){ /* code */}(); //SyntaxError: Unexpected token (
正如你所看到的,這裡捕獲了一個錯誤。當圓括弧為了調用函數出現在函數後面時,無論在全域環境或者局部環境裡遇到了這樣的function關鍵字,預設的,它會將它當作是一個函式宣告,而不是函數運算式,如果你不明確的告訴圓括弧它是一個運算式,它會將其當作沒有名字的函式宣告並且拋出一個錯誤,因為函式宣告需要一個名字。
問題1:這裡我麼可以思考一個問題,我們是不是也可以像這樣直接調用函數 var foo = function(){console.log(1)}(),答案是可以的。
問題2:同樣的,我們還可以思考一個問題,像這樣的函式宣告在後面加上圓括弧被直接調用,又會出現什麼情況呢?請看下面的解答。
函數,圓括弧,錯誤
有趣的是,如果你為一個函數指定一個名字並在它後面放一對圓括弧,同樣的也會拋出錯誤,但這次是因為另外一個原因。當圓括弧放在一個函數運算式後面指明了這是一個被調用的函數,而圓括弧放在一個聲明後面便意味著完全的和前面的函式宣告分開了,此時圓括弧只是一個簡單的代表一個括弧(用來控制運算優先的括弧)。
//然而函式宣告文法上是無效的,它仍然是一個聲明,緊跟著的圓括弧是無效的,因為圓括弧裡需要包含運算式function foo(){ /* code */ }();//SyntaxError: Unexpected token//現在,你把一個運算式放在圓括弧裡,沒有拋出錯誤...,但是函數也並沒有執行,因為:function foo(){/* code */}(1)//它等同於如下,一個函式宣告跟著一個完全沒有關係的運算式:function foo(){/* code */}(1);
立即執行函數運算式(IIFE)
幸運的是,修正語法錯誤很簡單。最流行的也最被接受的方法是將函式宣告包裹在圓括弧裡來告訴文法分析器去表達一個函數運算式,因為在Javascript裡,圓括弧不能包含聲明。因為這點,當圓括弧為了包裹函數碰上了 function關鍵詞,它便知道將它作為一個函數運算式去解析而不是函式宣告。注意理解這裡的圓括弧和上面的圓括弧遇到函數時的表現是不一樣的,也就是說。
當圓括弧出現在匿名函數的末尾想要調用函數時,它會預設將函數當成是函式宣告。
當圓括弧包裹函數時,它會預設將函數作為運算式去解析,而不是函式宣告。
//這兩種模式都可以被用來立即調用一個函數運算式,利用函數的執行來創造私人變數(function(){/* code */}());//Crockford recommends this one,括弧內的運算式代表函數立即調用運算式(function(){/* code */})();//But this one works just as well,括弧內的運算式代表函數運算式// Because the point of the parens or coercing operators is to disambiguate// between function expressions and function declarations, they can be// omitted when the parser already expects an expression (but please see the// "important note" below).var i = function(){return 10;}();true && function(){/*code*/}();0,function(){}();//如果你並不關心傳回值,或者讓你的代碼儘可能的易讀,你可以通過在你的函數前面帶上一個一元操作符來儲存位元組!function(){/* code */}();~function(){/* code */}();-function(){/* code */}();+function(){/* code */}();// Here's another variation, from @kuvos - I'm not sure of the performance// implications, if any, of using the `new` keyword, but it works.// http://twitter.com/kuvos/status/18209252090847232new function(){ /* code */ }new function(){ /* code */ }() // Only need parens if passing arguments
關於括弧的重要筆記
在一些情況下,當額外的帶著歧義的括弧圍繞在函數運算式周圍是沒有必要的(因為這時候的括弧已經將其作為一個運算式去表達),但當括弧用於調用函數運算式時,這仍然是一個好主意。
這樣的括弧指明函數運算式將會被立即調用,並且變數將會儲存函數的結果,而不是函數本身。當這是一個非常長的函數運算式時,這可以節約比人閱讀你代碼的時間,不用滾到頁面底部去看這個函數是否被調用。
作為規則,當你書寫清楚明晰的代碼時,有必要阻止 JavaScript 拋出錯誤的,同樣也有必要阻止其他開發人員對你拋出錯誤 WTFError!
儲存閉包的狀態
就像當函數通過他們的名字被調用時,參數會被傳遞,而當函數運算式被立即調用時,參數也會被傳遞。一個立即調用的函數運算式可以用來鎖定值並且有效儲存此時的狀態,因為任何定義在一個函數內的函數都可以使用外面函數傳遞進來的參數和變數(這種關係被叫做閉包)。
// 它的運行原理可能並不像你想的那樣,因為`i`的值從來沒有被鎖定。// 相反的,每個連結,當被點擊時(迴圈已經被很好的執行完畢),因此會彈出所有元素的總數,// 因為這是 `i` 此時的真實值。var elems = document.getElementsByTagName('a');for(var i = 0;i < elems.length; i++ ) { elems[i].addEventListener('click',function(e){ e.preventDefault(); alert('I am link #' + i) },false);}// 而像下面這樣改寫,便可以了,因為在IIFE裡,`i`值被鎖定在了`lockedInIndex`裡。// 在迴圈結束執行時,儘管`i`值的數值是所有元素的總和,但每一次函數運算式被調用時,// IIFE 裡的 `lockedInIndex` 值都是`i`傳給它的值,所以當連結被點擊時,正確的值被彈出。var elems = document.getElementsByTagName('a');for(var i = 0;i < elems.length;i++) { (function(lockedInIndex){ elems[i].addEventListener('click',function(e){ e.preventDefault(); alert('I am link #' + lockedInIndex); },false) })(i);}//你同樣可以像下面這樣使用IIFE,僅僅只用括弧包括點擊處理函數,並不包含整個`addEventListener`。//無論用哪種方式,這兩個例子都可以用IIFE將值鎖定,不過我發現前面一個例子更可讀var elems = document.getElementsByTagName( 'a' );for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', (function( lockedInIndex ){ return function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }; })( i ),false); }
記住,在這最後兩個例子裡,lockedInIndex可以沒有任何問題的訪問i,但是作為函數的參數使用一個不同的命名標識符可以使概念更加容易的被解釋。
立即執行函數一個最顯著的優勢是就算它沒有命名或者說是匿名,函數運算式也可以在沒有使用標識符的情況下被立即調用,一個閉包也可以在沒有當前變數汙染的情況下被使用。
自執行匿名函數(“Self-executing anonymous function”)有什麼問題呢?
你看到它已經被提到好幾次了,但是它仍然不是那麼清楚的被解釋,我提議將術語改成"Immediately-Invoked Function Expression",或者,IIFE,如果你喜歡縮寫的話。
什麼是Immediately-Invoked Function Expression呢?它使一個被立即調用的函數運算式。就像引導你去調用的函數運算式。
我想Javascript社區的成員應該可以在他們的文章裡或者陳述裡接受術語,Immediately-Invoked Function Expression和 IIFE,因為我感覺這樣更容易讓這個概念被理解,並且術語"self-executing anonymous function"真的也不夠精確。
//下面是個自執行函數,遞迴的調用自己本身function foo(){foo();};//這是一個自執行匿名函數。因為它沒有標識符,它必須是使用`arguments.callee`屬性來調用它自己var foo = function(){arguments.callee();};//這也許算是一個自執行匿名函數,但是僅僅當`foo`標識符作為它的引用時,如果你將它換成用`foo`來調用同樣可行var foo = function(){foo();};//有些人像這樣叫'self-executing anonymous function'下面的函數,即使它不是自執行的,因為它並沒有調用它自己。然後,它只是被立即調用了而已。(function(){ /*code*/ }());//為函數運算式增加標識符(也就是說創造一個命名函數)對我們的調試會有很大協助。一旦命名,函數將不再匿名。(function foo(){/* code */}());//IIFEs同樣也可以自執行,儘管,也許他不是最有用的模式(function(){arguments.callee();}())(function foo(){foo();}())// One last thing to note: this will cause an error in BlackBerry 5, because// inside a named function expression, that name is undefined. Awesome, huh?(function foo(){ foo(); }());
希望上面的例子可以讓你更加清楚的知道術語'self-executing'是有一些誤導的,因為他並不是執行自己的函數,儘管函數已經被執行。同樣的,匿名函數也沒用必要特別指出,因為,Immediately Invoked Function Expression,既可以是命名函數也可以匿名函數。
最後:模組模式
當我調用函數運算式時,如果我不至少一次的提醒我自己關於模組模式,我便很可能會忽略它。如果你並不屬性 JavaScript 裡的模組模式,它和我下面的例子很像,但是傳回值用對象代替了函數。
var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function(val){ i = val; }, increment: function(){ return ++i; } } }()); counter.get();//0 counter.set(3); counter.increment();//4 counter.increment();//5 conuter.i;//undefined (`i` is not a property of the returned object) i;//ReferenceError: i is not defined (it only exists inside the closure)
模組模式方法不僅相當的厲害而且簡單。非常少的代碼,你可以有效利用與方法和屬性相關的命名,在一個對象裡,組織全部的模組代碼即最小化了全域變數的汙染也創造了使用變數。