1. 程式人生 > >深入理解JavaScript系列(4):立即呼叫的函式表示式

深入理解JavaScript系列(4):立即呼叫的函式表示式

javascript 函式function前面的一元操作符, 感嘆號、小括號、一元操作符!()+-||

看下面內容之前可以先看看上面的文章,總結的非常贊

前言

大家學JavaScript的時候,經常遇到自執行匿名函式的程式碼,今天我們主要就來想想說一下自執行
在詳細瞭解這個之前,我們來談了解一下“自執行”這個叫法,本文對這個功能的叫法也不一定完全對,主要是看個人如何理解,因為有的人說立即呼叫,有的人說自動執行,所以你完全可以按照你自己的理解來取一個名字,不過我聽很多人都叫它為“自執行”,但作者後面說了很多,來說服大家稱呼為“立即呼叫的函式表示式”。

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

什麼是自執行?

在JavaScript裡,任何function在執行的時候都會建立一個執行上下文,因為為function宣告的變數和function有可能只在該function內部,這個上下文,在呼叫function的時候,提供了一種簡單的方式來建立自由變數或私有子function。

// 由於該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,往下看。

問題的核心

當你宣告類似function foo(){}或var foo = function(){}函式的時候,通過在後面加個括弧就可以實現自執行,例如foo(),看程式碼:

// 因為想下面第一個宣告的function可以在後面加一個括弧()就可以自己執行了,比如foo(),
// 因為foo僅僅是function() { /* code */ }這個表示式的一個引用
 
var foo = function(){ /* code */ }
 
// ...是不是意味著後面加個括弧都可以自動執行?
 
function(){ /* code */ }(); // SyntaxError: Unexpected token (
//

上述程式碼,如果甚至執行,第2個程式碼會出錯,因為在解析器解析全域性的function或者function內部function關鍵字的時候,預設是認為function宣告,而不是function表示式,如果你不顯示告訴編譯器,它預設會宣告成一個缺少名字的function,並且丟擲一個語法錯誤資訊,因為function宣告需要一個名字。

旁白:函式(function),括弧(paren),語法錯誤(SyntaxError)

有趣的是,即便你為上面那個錯誤的程式碼加上一個名字,他也會提示語法錯誤,只不過和上面的原因不一樣。在一個表示式後面加上括號(),該表示式會立即執行,但是在一個語句後面加上括號(),是完全不一樣的意思,他的只是分組操作符。

// 下面這個function在語法上是沒問題的,但是依然只是一個語句
// 加上括號()以後依然會報錯,因為分組操作符需要包含表示式
 
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
 
// 但是如果你在括弧()裡傳入一個表示式,將不會有異常丟擲
// 但是foo函式依然不會執行
function foo(){ /* code */ }( 1 );
 
// 因為它完全等價於下面這個程式碼,一個function聲明後面,又聲明瞭一個毫無關係的表示式: 
function foo(){ /* code */ }
 
( 1 );

自執行函式表示式

要解決上述問題,非常簡單,我們只需要用大括弧將程式碼的程式碼全部括住就行了,因為JavaScript裡括弧()裡面不能包含語句,所以在這一點上,解析器在解析function關鍵字的時候,會將相應的程式碼解析成function表示式,而不是function宣告。

// 下面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住的傳入引數,自執行函式表示式可以有效地儲存狀態。

// 這個程式碼是錯誤的,因為變數i從來就沒背locked住
// 相反,當迴圈執行以後,我們在點選的時候i才獲得數值
// 因為這個時候i操真正獲得值
// 所以說無論點選那個連線,最終顯示的都是I am link #10(如果有10個a元素的話)

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的值變成了a元素總數(例如10)
// 但閉包內部的lockedInIndex值是沒有改變,因為他已經執行完畢了
// 所以當點選連線的時候,結果是正確的

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

}

// 你也可以像下面這樣應用,在處理函式那裡使用自執行函式表示式
// 而不是在addEventListener外部
// 但是相對來說,上面的程式碼更具可讀性

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

}

其實,上面2個例子裡的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(); } ());

// 另外,下面的程式碼在黑莓5裡執行會出錯,因為在一個命名的函式表示式裡,他的名稱是undefined
// 呵呵,奇怪
(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):全面解析Module模式 。

更多閱讀

希望上面的一些例子,能讓你對立即呼叫的函式表達(也就是我們所說的自執行函式)有所瞭解,如果你想了解更多關於function和Module模式的資訊,請繼續訪問下面列出的網站:

同步與推薦

深入理解JavaScript系列文章,包括了原創,翻譯,轉載等各型別的文章,如果對你有用,請推薦支援一把,給大叔寫作的動力