[譯] JavaScript:當即執行函數表達式(IIFE)

原文:http://benalman.com/news/2010/11/immediately-invoked-function-expression/#iife
by Murphywuwugit

博客同步地址 當即執行函數github


可能你並無注意到,我是一個對於專業術語有一點堅持細節人。全部,當我聽到流行的可是還存在誤解的術語「自執行匿名函數」屢次時,我最終決定將個人想法寫進這篇文章裏。express

除了提供關於這種模式事實上是如何工做的一些全面的信息,更進一步的,實際上我建議咱們應該知道咱們應該叫它什麼。並且,若是你想跳過這裏,你能夠直接跳到當即調用函數表達式進行閱讀,可是我建議你讀完整篇文章。閉包

它是什麼

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

function makeCounter() {
    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這樣的函數返回屢次累加值,而且能夠只調用一次獲得一個單一的值,在其餘一些狀況裏,你甚至不須要明確的知道返回值。函數

它的核心

如今,不管你定義一個函數像這樣function foo(){}或者var foo = function(){},調用時,你都須要在後面加上一對圓括號,像這樣foo()this

//向下面這樣定義的函數能夠經過在函數名後加一對括號進行調用,像這樣`foo()`,
//由於foo相對於函數表達式`function(){/* code */}`只是一個引用變量

var foo = function(){/* code */}

//那這能夠說明函數表達式能夠經過在其後加上一對括號本身調用本身嗎?

function(){ /* code */}(); //SyntaxError: Unexpected token (

正如你所看到的,這裏捕獲了一個錯誤。當圓括號爲了調用函數出如今函數後面時,不管在全局環境或者局部環境裏遇到了這樣的function關鍵字,默認的,它會將它看成是一個函數聲明,而不是函數表達式,若是你不明確的告訴圓括號它是一個表達式,它會將其看成沒有名字的函數聲明而且拋出一個錯誤,由於函數聲明須要一個名字。調試

問題1:這裏我麼能夠思考一個問題,咱們是否是也能夠像這樣直接調用函數 var foo = function(){console.log(1)}(),答案是能夠的。
問題2:一樣的,咱們還能夠思考一個問題,像這樣的函數聲明在後面加上圓括號被直接調用,又會出現什麼狀況呢?請看下面的解答。code

函數,圓括號,錯誤

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

//然而函數聲明語法上是無效的,它仍然是一個聲明,緊跟着的圓括號是無效的,由於圓括號裏須要包含表達式

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/18209252090847232

new 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 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();}())

// 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)

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

相關文章
相關標籤/搜索