當即執行函數表達式(IIFE)

原文:Immediately-Invoked Function Expression (IIFE) by Ben Alman
原譯:當即執行函數 by Murphywuwu
改增內容: by blanujavascript

也許你沒有注意到,我是一個對於專業術語有一點強迫症的人。因此,當我屢次聽到流行卻易產生誤解的術語「自執行匿名函數」,我最終決定將個人想法寫進這篇文章裏。java

更進一步地說,除了提供關於該模式到底是如何工做的全面信息,事實上我還建議了咱們應該怎樣稱呼這種模式。另外,若是你想跳過這裏,你能夠直接跳到當即調用函數表達式進行閱讀,可是我建議你讀完整篇文章。git

請理解這篇文章不是想說「我對了,你錯了」。我發自真心地想幫助人們理解看似複雜的概念,而且我認爲使用先後一致的精確術語是有助於人們理解的最簡單的方式之一。github

它是什麼

在JavaScript裏,每一個函數,當被調用時,都會建立一個新的執行上下文。由於在函數裏定義的變量和函數只能在函數內部被訪問,外部沒法獲取;當調用函數時,函數提供的上下文就提供了一個很是簡單的方法建立私有變量。express

//由於這個函數的返回值是另外一個能訪問私有變量i的函數,所以返回的函數實際上被提權(privileged)了
function makeCounter() {
    //i只能從`makeConuter`內部訪問
    var i = 0;
    return function(){
        console.log(++i);
    };   
}
//記住:`counter`和`counter2`都有他們本身做用域中的變量 `i`
var counter = makeCounter();
counter();//1
counter();//2

var counter2 = makeCounter();
counter2();//1
counter2();//2

i;//ReferenceError: i is not defined(它只存在於makeCounter裏)

在許多狀況下,你可能並不須要makeWhatever這樣的函數返回屢次累加值,而且能夠只調用一次獲得一個單一的值,在其餘一些狀況裏,你甚至不須要明確的知道返回值。segmentfault

它的核心

如今,不管你定義一個函數像這樣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:一樣的,咱們還能夠思考一個問題,像這樣的函數聲明在後面加上圓括號被直接調用,又會出現什麼狀況呢?請看下面的解答。ecmascript

題外話:函數、圓括號和錯誤

有趣的是,若是你爲一個函數指定一個名字並在它後面放一對圓括號,一樣的也會拋出錯誤,但此次是由於另一個緣由。當圓括號放在一個函數表達式後面指明瞭這是一個被調用的函數,而圓括號放在一個聲明後面便意味着徹底的和前面的函數聲明分開了,此時圓括號只是一個簡單的表明一個括號(用來控制運算優先的括號)。ide

//然而函數聲明語法上是無效的,它仍然是一個聲明,緊跟着的圓括號是無效的,由於圓括號裏須要包含表達式
function foo(){ /* code */ }();//SyntaxError: Unexpected token
//如今,你把一個表達式放在圓括號裏,沒有拋出錯誤...可是函數也並無執行,由於:
function foo(){/* code */}(1)
//它等同於以下,一個函數聲明跟着一個徹底沒有關係的表達式:
function foo(){/* code */}
(1);

關於這個細節,你能夠閱讀Dmitry A. Soshnikov的文章:ECMA-262-3 in detail. Chapter 5. Functions 中文版本函數

當即執行函數表達式(IIFE)

幸運的是,修正語法錯誤很簡單。最流行的也最被接受的方法是將函數聲明包裹在圓括號裏來告訴語法分析器去表達一個函數表達式,由於在JavaScript裏,圓括號不能包含聲明。由於這點,當圓括號爲了包裹函數碰上了 function關鍵詞,它便知道將它做爲一個函數表達式去解析而不是函數聲明。注意理解這裏的圓括號和上面的圓括號遇到函數時的表現是不同的,也就是說。

  • 當圓括號出如今匿名函數的末尾想要調用函數時,它會默認將函數當成是函數聲明。

  • 當圓括號包裹函數時,它會默認將函數做爲表達式去解析,而不是函數聲明。

//這兩種模式均可以被用來當即調用一個函數表達式,利用函數的執行來創造私有變量
(function(){/* code */}());//Crockford recommends this one
(function(){/* code */})();//But this one works just as well

// 由於括號的做用就是爲了消除函數表達式和函數聲明之間的差別
// 若是解釋器能預料到這是一個表達式,括號能夠被省略
// 不過請參見下面的「重要筆記」
var i = function(){return 10;}();
true && function(){/*code*/}();
0,function(){}();

//若是你並不關心返回值,或者讓你的代碼儘量的易讀,你能夠經過在你的函數前面帶上一個一元操做符來存儲字節
!function(){/* code */}();
~function(){/* code */}();
-function(){/* code */}();
+function(){/* code */}();

// 這裏是另一種方法
// 我(原文做者)不清楚new方法是否會影響性能
// 但它倒是奏效,參見http://twitter.com/kuvos/status/18209252090847232

new function(){ /* code */ }
new function(){ /* code */ }() // 只有當傳入參數時才須要加括號

關於括號的重要筆記

在一些狀況下,當額外的帶着歧義的括號圍繞在函數表達式周圍是沒有必要的(由於這時候的括號已經將其做爲一個表達式去表達),但當括號用於調用函數表達式時,這仍然是一個好主意。

這樣的括號指明函數表達式將會被當即調用,而且變量將會儲存函數的結果,而不是函數自己。當這是一個很是長的函數表達式時,這能夠節約其餘人閱讀你代碼的時間,不用滾到頁面底部去看這個函數是否被調用。

做爲規則,當你書寫清楚明晰的代碼時,有必要阻止JavaScript拋出錯誤的,一樣也有必要阻止其餘開發者對你拋出錯誤WTFError!

保存閉包的狀態

就像當函數經過他們的名字被調用時,參數會被傳遞,而當函數表達式被當即調用時,參數也會被傳遞。一個當即調用的函數表達式能夠用來鎖定值而且有效的保存此時的狀態,由於任何定義在一個函數內的函數均可以使用外面函數傳遞進來的參數和變量(這種關係被叫作閉包)。

關於閉包的更多信息,參見 Closures explained with JavaScript

//它的運行原理可能並不像你想的那樣,由於`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,若是你喜歡縮寫的話(發音相似「iffy」)。

什麼是Immediately-Invoked Function Expression呢?顧名思義,它就是一個被當即調用的函數表達式。

我想JavaScript社區的成員應該能夠在他們的文章裏或者陳述裏接受術語Immediately-Invoked Function ExpressionIIFE,由於我感受這樣更容易讓這個概念被理解,而且術語"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();}())
// 另外,下面這個表達式竟會在黑莓5上拋出錯誤,在一個被命名的函數中,該函數名是undefined。很奇妙吧…
(function foo(){ foo(); }());

但願上面的例子可讓你更加清楚的知道術語'self-executing'是有一些誤導的,由於他並非執行本身的函數,儘管函數已經被執行。一樣的,匿名函數也沒用必要特別指出,由於,Immediately Invoked Function Expression,既能夠是命名函數也能夠匿名函數。

有趣的是:由於arguments.callee在ECMAScript 5 strict mode中被deprecated了,因此在ES5的strict mode中實際上不可能建立一個self-executing anonymous function

最後:模塊模式

當我調用函數表達式時,若是我不至少一次的提醒我本身關於模塊模式,我便極可能會忽略它。若是你並不熟悉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)

模塊模式方法不只至關的厲害並且簡單。很是少的代碼,你能夠有效的利用與方法和屬性相關的命名,在一個對象裏,組織所有的模塊代碼即最小化了全局變量的污染也創造了私人變量。

延伸閱讀

但願這篇文章能夠爲你答疑解惑。固然,若是你產生了更多疑惑,你能夠閱讀下面這些關於函數和模塊模式的文章。

相關文章
相關標籤/搜索