深刻理解JavaScript系列4:當即調用的函數表達式

前言

你們學JavaScript的時候,常常遇到自執行匿名函數的代碼,今天咱們主要就來想一想說一下自執行。javascript

在詳細瞭解這個以前,咱們來談了解一下「自執行」這個叫法,本文對這個功能的叫法也不必定徹底對,主要是看我的如何理解,由於有的人說當即調用,有的人說自動執行,因此你徹底能夠按照你本身的理解來取一個名字,不過我聽不少人都叫它爲「自執行」,但做者後面說了不少,來講服你們稱呼爲「當即調用的函數表達式」。html

本文英文原文地址:http://benalman.com/news/2010/11/immediately-invoked-function-expression/java

什麼是自執行?

在JavaScript裏,任何function在執行的時候都會建立一個執行上下文,由於爲function聲明的變量和function有可能只在該function內部,這個上下文,在調用function的時候,提供了一種簡單的方式來建立自由變量或私有子function。git

// 因爲該function裏返回了另一個function,其中這個function能夠訪問自由變量i
// 全部說,這個內部的function其實是有權限能夠調用內部的對象。
function makeCounter() {
    // 只能在makeCounter內部訪問i
    var i = 0;

    return function () {
        console.log(++i);
    };
}
// 注意,counter和counter2是不一樣的實例,分別有本身範圍內的i。
var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2

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

alert(i); // 引用錯誤:i沒有defind(由於i是存在於makeCounter內部)。

不少狀況下,咱們不須要makeCounter多個實例,甚至某些case下,咱們也不須要顯示的返回值,OK,往下看。github

問題的核心

當你聲明相似function foo(){}或var foo = function(){}函數的時候,經過在後面加個括弧就能夠實現自執行,例如foo(),看代碼:express

// 由於想下面第一個聲明的function能夠在後面加一個括弧()就能夠本身執行了,好比foo(),
// 由於foo僅僅是function() { /* code */ }這個表達式的一個引用

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

// ...是否是意味着後面加個括弧均可以自動執行?

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

上述代碼,若是運行,第2個代碼會出錯,由於在解析器解析全局的function或者function內部function關鍵字的時候,默認是認爲function聲明,而不是function表達式,若是你不顯示告訴編譯器,它默認會聲明成一個缺乏名字的function,而且拋出一個語法錯誤信息,由於function聲明須要一個名字。segmentfault

函數,括弧,語法錯誤(SyntaxError)

有趣的是,即使你爲上面那個錯誤的代碼加上一個名字,他也會提示語法錯誤,只不過和上面的緣由不同。在一個表達式後面加上括號(),該表達式會當即執行,可是在一個語句後面加上括號(),是徹底不同的意思,他的只是分組操做符。閉包

// 下面這個function在語法上是沒問題的,可是依然只是一個語句
// 加上括號()之後依然會報錯,由於分組操做符須要包含表達式

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

// 可是若是你在括弧()裏傳入一個表達式,將不會有異常拋出
// 可是foo函數依然不會執行
function foo(){ /* code */ }(1);

// 由於它徹底等價於下面這個代碼,一個function聲明後面,又聲明瞭一個毫無關係的表達式: 
function foo(){ /* code */ }(1);

你能夠訪問ECMA-262-3 in detail. Chapter 5\. Functions 獲取進一步的信息。ecmascript

自執行函數表達式

要解決上述問題,很是簡單,咱們只須要用大括弧將代碼的代碼所有括住就好了,由於JavaScript裏括弧()裏面不能包含語句,因此在這一點上,解析器在解析function關鍵字的時候,會將相應的代碼解析成function表達式,而不是function聲明。不明白的,能夠看深刻理解JavaScript系列2:揭祕命名函數表達式中的函數表達式和函數聲明函數

// 下面2個括弧()都會當即執行

(function () { /* code */ } ()); // 推薦使用這個
(function () { /* code */ })(); // 可是這個也是能夠用的

// 因爲括弧()和JS的&&,異或,逗號等操做符是在函數表達式和函數聲明上消除歧義的
// 因此一旦解析器知道其中一個已是表達式了,其它的也都默認爲表達式了
// 不過,請注意下一章節的內容解釋

var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();

// 若是你不在乎返回值,或者不怕難以閱讀
// 你甚至能夠在function前面加一元操做符號

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 還有一個狀況,使用new關鍵字,也能夠用,但我不肯定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 若是須要傳遞參數,只須要加上括弧()

上面所說的括弧是消除歧義的,其實壓根就不必,由於括弧原本內部原本指望的就是函數表達式,可是咱們依然用它,主要是爲了方便開發人員閱讀,當你讓這些已經自動執行的表達式賦值給一個變量的時候,咱們看到開頭有括弧(,很快就能明白,而不須要將代碼拉到最後看看到底有沒有加括弧。

用閉包保存狀態

和普通function執行的時候傳參數同樣,自執行的函數表達式也能夠這麼傳參,由於閉包直接能夠引用傳入的這些參數,利用這些被lock住的傳入參數,自執行函數表達式能夠有效地保存狀態。

下面是錯誤的使用:

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');

}

因爲變量i歷來就沒背locked住。相反,當循環執行之後,咱們在點擊的時候i得到數值,因此說不管點擊哪一個鏈接,最終顯示的都是I am link #10(若是有10個a元素的話)

下面是正確的使用:

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

}

因爲在自執行函數表達式閉包內部i的值做爲locked的索引存在,在循環執行結束之後,儘管最後i的值變成了a元素總數(例如10)但閉包內部的lockedInIndex值是沒有改變,由於他已經執行完畢了因此當點擊鏈接的時候,結果是正確的。

或者你也能夠像這樣使用:

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');

}

上面的代碼在處理函數那裏使用自執行函數表達式,而不是在addEventListener外部,這樣也能夠達到locked的效果,可是前面的代碼更具備可讀性。

其實,前面兩個例子裏的lockedInIndex變量,也能夠換成i,由於和外面的i不在一個做用於,因此不會出現問題,這也是匿名函數+閉包的威力。

自執行匿名函數和當即執行的函數表達式區別

在這篇文章中,咱們一直叫自執行函數,確切的說是自執行匿名函數(Self-executing anonymous function),但英文原文做者一直倡議使用當即調用的函數表達式(Immediately-Invoked Function Expression)這一名稱,做者又舉了一堆例子來解釋,好吧,咱們來看看:

// 這是一個自執行的函數,函數內部執行自身,遞歸
function foo() { foo(); }

// 這是一個自執行的匿名函數,由於沒有函數名稱
// 必須使用arguments.callee屬性來執行本身
var foo = function () { arguments.callee(); };

// 這可能也是一個自執行的匿名函數,僅僅是foo函數名引用它自身
// 若是你將foo改變成其它的,你將獲得一個自執行(used-to-self-execute)的匿名函數
var foo = function () { foo(); };

// 有些人叫這個是自執行的匿名函數(即使它不是),由於它沒有調用自身,它只是當即執行而已。
(function () { /* code */ } ());

// 爲函數表達式添加一個函數名稱,能夠方便Debug
// 注意:一旦命名爲函數添加了函數名,這個函數就再也不是匿名的了
(function foo() { /* code */ } ());

// 當即調用的函數表達式(IIFE)也能夠自執行,不過可能不經常使用罷了
(function () { arguments.callee();} ());
(function foo() { foo(); } ());

但願這裏的一些例子,可讓你們明白,什麼叫自執行,什麼叫當即調用。

注:arguments.callee在ECMAScript 5 strict mode裏被廢棄了,因此在這個模式下,實際上是不能用的。

Module模式

在講到這個當即調用的函數表達式的時候,我又想起來了Module模式,若是你還不熟悉這個模式,咱們先來看看代碼:

// 建立一個當即調用的匿名函數表達式
// return一個變量,其中這個變量裏包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的function自身

var counter = (function () {
    var i = 0;

    return {
        get: function () {
            return i;
        },
        set: function (val) {
            i = val;
        },
        increment: function () {
            return ++i;
        }
    };
} ());

// counter是一個帶有多個屬性的對象,上面的代碼對於屬性的體現實際上是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 由於i不是返回對象的屬性
i; // 引用錯誤: i 沒有定義(由於i只存在於閉包)

關於更多Module模式的介紹,請訪問個人上一篇文章:深刻理解JavaScript系列2:揭祕命名函數表達式

更多閱讀

但願上面的一些例子,能讓你對當即調用的函數表達(也就是咱們所說的自執行函數)有所瞭解,若是你想了解更多關於function和Module模式的信息,請繼續訪問下面列出的網站:

  1. ECMA-262-3 in detail. Chapter 5\. Functions. - Dmitry A. Soshnikov

  2. Functions and function scope - Mozilla Developer Network

  3. Named function expressions - Juriy 「kangax」 Zaytsev

  4. 深刻理解JavaScript系列3:全面解析Module模式 - hiyangguo

  5. Closures explained with JavaScript - Nick Morgan

關於本文

本文轉自TOM大叔深刻理解JavaScript系列

【深刻理解JavaScript系列】文章,包括了原創,翻譯,轉載,整理等各種型文章,原文是TOM大叔的一個很是不錯的專題,現將其從新整理髮布。謝謝大叔。若是你以爲本文不錯,請幫忙點個推薦,支持一把,感激涕零。

更多優秀文章歡迎關注個人專欄

相關文章
相關標籤/搜索