深入理解JavaScript系列(4)

2018-06-09 15:56 更新

本文是深入理解JavaScript系列的第篇讀文筆記,博客原文在這里。

內(nèi)容簡要

本文闡述的內(nèi)容是JavaScript中經(jīng)常遇到的兩個知識點:自執(zhí)行函數(shù)函數(shù)閉包。如果你之前稍微接觸過過JavaScript,你應(yīng)該能夠明白我所指的意思,這里我就不像大叔原文中那么較真這個行為的具體叫法了。

在JavaScript的世界中,如果你能夠?qū)ψ詧?zhí)行函數(shù)和函數(shù)閉包了若指掌,在實際編碼中能夠信手拈來,那么,一般來說你JavaScript的功力至少有中級以上了,呵呵,這可能還是一種保守的估計。

如果你有讀過流行JavaScript類庫源碼的話,你可能會發(fā)現(xiàn),源碼的作者對自執(zhí)行函數(shù)和函數(shù)閉包的使用是比較頻繁的,再結(jié)合一些具體的業(yè)務(wù)場景,往往會得到一些非常美妙的設(shè)計。如果你去悉心品讀,可能會發(fā)現(xiàn)高手寫出來的JavaScript代碼和新手寫出的JavaScript代碼完全是天壤之別。

BACKBONE

大叔的原文中只針對自執(zhí)行函數(shù)作了比較透徹的說明,而對函數(shù)閉包僅僅用了一個示例就一筆帶過。這篇讀文筆記中,我將會針對這兩點分別作一些詳細(xì)說明,盡量用簡明的話將我理解中的這兩個概念闡述清楚。

自執(zhí)行函數(shù)

什么是自執(zhí)行?

首先,什么叫自執(zhí)行?在JavaScript中函數(shù)在執(zhí)行的時候會創(chuàng)建一個叫做執(zhí)行上下文的東西,這里問題又來了,這個執(zhí)行上下文又是什么東西呢?

簡單的說,執(zhí)行上下文就是JavaScript代碼在執(zhí)行時創(chuàng)建的一個容器,這個容器中可以隨時創(chuàng)建只屬于這一塊代碼的變量,函數(shù)聲明等等。

其實,執(zhí)行上下文是ECMA-262規(guī)定的一個非常抽象的概念,我們這里只要對這個概念有個把握就可以了,更多的解釋我就不多作筆墨了,如有興趣,可查閱相關(guān)文檔。

上面說到,執(zhí)行上下文其實代碼執(zhí)行時才會生成的一個東西,如果我就簡單的寫一段JavaScript代碼放在這里,我并沒有在瀏覽器中引入這個JavaScript片段執(zhí)行它,那么它就不會有執(zhí)行上下文了。所以,自執(zhí)行的含義,簡單來說,一段JavaScript代碼中自己執(zhí)行了。這里的一段JavaScript代碼一般都是指一個JavaScript函數(shù),所以這里的自執(zhí)行就是指函數(shù)調(diào)用。

我們來看個例子,

function makeCounter() {
    // 只能在makeCounter內(nèi)部訪問i
    var i = 0;
    return function () {
        console.log(++i);
    };
}
// 注意,counter和counter2是不同的實例,分別有自己范圍內(nèi)的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內(nèi)部)。

這里,我們每次調(diào)用函數(shù)makeCounter()時,其實都會生成一個獨立的執(zhí)行上下文。具體來看,makeCounter生成的執(zhí)行上下文中包含了一個變量i以及一個匿名函數(shù)。

這里需要特別提出的一點是,每個獨立的執(zhí)行上下文,其中的變量都是相互獨立的,即countercounter2其實是不同的實例。

問題的核心

當(dāng)你聲明類似這樣的函數(shù),

function foo() {
    // function body
}
var foo2 = function() {
    // function body
}

我們可以簡單在函數(shù)名foo(或者變量名foo2)的后面加上()即可實現(xiàn)自執(zhí)行。如下,

foo();
foo2();

那是不是意為著我只要在函數(shù)的后面加上一對()就可以達(dá)到自執(zhí)行的目的呢?我們看下面的代碼,

function() {
    return 'test';
}();
function foo() {
    return 'test2';
}();

遺憾的是,這兩種方式,不管是在匿名函數(shù)后加()還是在普通的函數(shù)聲明后加()都達(dá)不到讓函數(shù)自執(zhí)行的目的。這兩種情況下,你都會得到一個報錯。

上面提到的兩種錯誤方式,其實出錯的原理還不太一樣,

  • 前者是JavaScript在解析function關(guān)鍵字時,默認(rèn)其是函數(shù)聲明,函數(shù)聲明要求必須有一個函數(shù)名。
  • 后者是一個函數(shù)聲明,函數(shù)聲明后直接跟一個(),這個()其實是一個分組操作符,這里報錯的原因是因為分組操作符需要一個表達(dá)式語句而不是一個聲明語句。

自執(zhí)行函數(shù)表達(dá)式

經(jīng)過上面的說明,我們知道,不管是匿名函數(shù)(雖然這個匿名的聲明也有問題)還是函數(shù)foo其實都只是函數(shù)聲明,而這里的()是一個運算符,它要求前面的東西必須為(函數(shù))表達(dá)式!

所以,我們只需要將()前面的內(nèi)容變成函數(shù)表達(dá)式就行了。我們看下面的代碼,

// 下面2個括弧()都會立即執(zhí)行
(function(){ /* code */ }());
(function(){ /* code */ })();
// 由于括弧()和JS的&&,異或,逗號等操作符是在函數(shù)表達(dá)式和函數(shù)聲明上消除歧義的
// 所以一旦解析器知道其中一個已經(jīng)是表達(dá)式了,其它的也都默認(rèn)為表達(dá)式了
var i = function(){ /* code */ }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 如果你不在意返回值,或者不怕難以閱讀
// 你甚至可以在function前面加一元操作符號
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
// 上面這種使用一元表達(dá)式這種方式其實是不太常見的
// 而且有時候肯定在一些場景下存在一些弊端,因為一元表達(dá)式會有一個不為undefined的返回值
// 要想返回值為undefined,那么最保險的就是使用void關(guān)鍵字
void function(){/* code */}();

一般常用的兩種形式就是(function(){}());(function(){})();,大叔的原文中說第一種是推薦的寫法,但是不知道為什么現(xiàn)在很多人都是用的第二種~~

區(qū)別

原文中還提到了這個話題。額,其實是英文原文的作者提到的。其實在我看來,自執(zhí)行匿名函數(shù)立即執(zhí)行函數(shù)表達(dá)式的區(qū)別基本上可以忽略,在實際的使用其實都是一回事,只不過兩種形式的函數(shù)主體不太一致。如下代碼,

(function() {
    return '我是自執(zhí)行匿名函數(shù)';
})();
(function foo() {
    foo();
})();

好吧,我承認(rèn)第二種其實是不太常見的。

Module模式

想想前篇文章說的Module模式,我們常常使用Module模式配合自執(zhí)行函數(shù)來封裝一個工具。

下面是一個例子,

// 創(chuàng)建一個立即調(diào)用的匿名函數(shù)表達(dá)式
// return一個變量,其中這個變量里包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的function自身
var counter = (function () {
    var i = 0;
    return {
        get: function () {
            return i;
        },
        set: function (val) {
            i = val;
        },
        increment: function () {
            return ++i;
        }
    };
} ());
// counter是一個帶有多個屬性的對象,上面的代碼對于屬性的體現(xiàn)其實是方法
counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5
counter.i; // undefined 因為i不是返回對象的屬性
i; // 引用錯誤: i 沒有定義(因為i只存在于閉包)

當(dāng)然這里還用到了閉包的概念。我自己就經(jīng)常使用這種技巧來封裝一些配置類或者工具類的東西。封裝后,只要暴露一個對象就可以了,從而達(dá)到了對內(nèi)部變量的隱藏。

函數(shù)閉包

什么叫閉包?

什么叫(函數(shù))閉包呢?各種專業(yè)文獻(xiàn)上對這個詞的解釋比較抽象,不是太好理解。我個人對閉包的理解就是:閉包就是一個帶有了父作用域相關(guān)變量的函數(shù)。或者更加通俗一點就是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。

我想先談?wù)凧avaScript中為什么會有閉包這個東西。

我們知道在JavaScript中,函數(shù)第一等公民,函數(shù)的用途非常廣泛,函數(shù)可以參數(shù)傳入另一個函數(shù),還可以返回值從一個函數(shù)中返回。我們看下面的代碼,

function fn1() {
    var a = 1;
    return function fn2() {
        return 1 + a;
    };
}
var foo = fn1(); // typeof foo === 'function'
foo(); // 2

這里foo = fn1()后,foo其實是一個函數(shù)引用,通俗點說,foo就是一個函數(shù)表達(dá)式。那么這個foo在執(zhí)行的時候,它需要訪問變量a,但是這個a并沒有在fn2中定義,它是定義在fn1中的。所以foo(也就是fn2)在執(zhí)行的過程中,會向其父作用域(即fn1所在的作用域)查找變量a。此時,fn2中就保持了一個對父作用域的引用。

類似這樣的場景就是我們所說的(函數(shù))閉包。其實閉包從某種意義上來說,就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。

閉包的作用

閉包最大的作用有兩個,

  • 讀取函數(shù)內(nèi)部的變量
  • 保持對變量的持續(xù)引用

我們來看下面的一個例子,

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()()); // The Window

作一點改動后,

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var self = this;
        return function(){
            return self.name;
        };
    }
};
alert(object.getNameFunc()()); // My Object

我們來稍微分析一下。第一種情況中,

object.getNameFunc()的執(zhí)行結(jié)果是其實是一個函數(shù)引用。而且這個getNameFunc函數(shù)在執(zhí)行時,其內(nèi)部的this指針是指向object的。接下來,object.getNameFunc()()其實等價于,

var name = "The Window";
(function() {
    return this.name;
})();

這個代碼片段在執(zhí)行的時候,會檢索this的值。這里,它最終檢索的結(jié)果就是全局對象window,然后返回的結(jié)果就是name = 'The Window'

而第二種情況中,我們使用變量self暫存了匿名函數(shù)(其實就是getNameFunc函數(shù)表達(dá)式)的this指針,而這個this指針在運行時的指向正是object。函數(shù)getNameFunc返回的匿名函數(shù)毫無疑問,它是一個閉包,而且它保持了對父作用域變量self的持續(xù)引用。

更多內(nèi)容,推薦閱讀阮一峰的學(xué)習(xí)Javascript閉包(Closure)。

常見誤區(qū)

在使用閉包的時候,有一個常見的誤區(qū),我們看下面的代碼,

for (var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 1000);
}

這段代碼的運行結(jié)果將會連續(xù)打印10個10。

你可能會問:???怎么會這樣?不是說好的打印從0到9的序列么?

我們來稍微分析一下。

for循環(huán)中連續(xù)創(chuàng)建了10個延時函數(shù),每個延時函數(shù)的函數(shù)體是打印迭代變量i。這里我們先忽略10個延時函數(shù)由于創(chuàng)建先后順序以及CPU時間片造成誤差。當(dāng)10次循環(huán)結(jié)束后,肯定還是沒有經(jīng)過1000ms,不過此時由于迭代的結(jié)果,迭代變量i已經(jīng)變成10了。接下里延時計時器結(jié)束,開始執(zhí)行延時函數(shù),函數(shù)中需要訪問變量i,不幸的是,此時的i已經(jīng)變成10了,所以打印出來的10個數(shù)據(jù)都是10。

那我們?nèi)绾涡薷哪軌蜻_(dá)到我們本來的目的呢?即按照迭代變量的順序,依次打印出0-9呢?

for (var i = 0; i < 10; i++) {
    setTimeout((function(index){
        return function() {
            console.log(index);
        };
    })(i), 1000);
}

代碼中應(yīng)該看的很清楚了,延時函數(shù)中使用了一個閉包,這個閉包保持了對父作用域中參考變量index的持續(xù)引用,而這個index是隨著每次for循環(huán)實時傳遞進(jìn)來的迭代變量。所以它將會打印出0-9。

總結(jié)

這篇對自執(zhí)行函數(shù)(或者叫立即調(diào)用匿名函數(shù)表達(dá)式)以及函數(shù)閉包作了細(xì)致的闡述,基本上涵蓋了這兩個知識點所有的方方面面,更多的內(nèi)容就是需要在實際編碼中進(jìn)行實戰(zhàn)了。

我還是想強(qiáng)調(diào)那句話,只要對JavaScript中的這兩個要點了若指掌,編碼時能夠做到信手拈來,那么假以時日必定能夠成為JavaScript高手。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號