封閉了內心卻包容了天下,閉包你並不孤獨

chenhongdong發表於2018-04-18

起點

本文之所以會寫這種老生常談的文章,是為了接下來的設計模式做鋪墊。既然已經提筆了,就打算不改了,繼續寫下去,相信也一定有很多人對閉包這樣的概念有些模糊,那就瞧一瞧、看一看

畢竟閉包和高階函式這兩種概念,在開發中是非常有分量的。好處多多,妙處多多,那麼我們就不再兜圈子了,直接開始今天的主題,閉包&高階函式

閉包

閉包是前端er離不開的一個話題,而且也是一個難懂又必須明白的概念。說起閉包,它與變數的作用域和變數的生命週期密切相關。 這兩個知識點我們也無法繞開,那麼就一起了解下吧

變數作用域

首先變數作用域分為兩類:全域性作用域和區域性作用域,這個沒話說大家都懂。我們常說的變數作用域其實也主要是在函式中宣告的作用域

  • 在函式中宣告變數時沒有var關鍵字,就代表是全域性變數
  • 在函式中宣告變數帶有var關鍵字的即是區域性變數,區域性變數只能在函式內才能訪問到
function fn() {
    var a = 110;     // a為區域性變數
    console.log(a);  // 110
}
fn();
console.log(a);     // a is not defined  外部訪問不到內部的變數
複製程式碼

上面程式碼展示了在函式中宣告的區域性變數a在函式外部確實無法拿到。小樣兒的還挺囂張,對於迎難而上的coder來說,還不信拿不下a了

客官,莫急,且聽風吟。大家是否還記得在js中,函式可是“一等公民”啊,大大滴厲害

函式可以創造函式作用域,在函式作用域中如果要查詢一個變數的時候,如果在該函式內沒有宣告這個變數,就會向該函式的外層繼續查詢,一直查到全域性變數為止

所以變數的查詢是由內而外的,這也形成了所謂的作用域鏈

var a = 7;
function outer() {
    var b = 9;
    function inner() {
        var c = 8;
        alert(b);
        alert(a);
    }
    inner();
    alert(c);   // c is not defined
}
outer();    // 呼叫函式
複製程式碼

利用作用域鏈,我們試著去拿到a,改造一下fn函式

function fn() {
    var a = 110;     // a為區域性變數
    return function() {
        console.log(a);
    }
    console.log(a);  // 110
}
var fn2 = fn();
fn2();      // 110
複製程式碼

如此這般,這般如此,輕而易舉,小case的事,就可以從外面訪問到區域性變數a了

那麼到此為止,我們已經發現了閉包的其中一個意義:閉包就是能夠讀取其他函式內部變數的函式,嗯,沒毛病,繼續往下看

變數生命週期

在解決了上面如何拿到小樣兒a的問題,我們不妨再把變數生命週期這個概念先簡單地過一遍。

  • 對於全域性變數來說,它的生命週期自然是永久的(forever),除非我們不高興,主動幹掉它,報銷它。
  • 而對於在函式中通過var宣告的區域性變數來說,就沒那麼幸運了,當函式執行完畢,區域性變數們就失去了價值,就被垃圾回收機制給當成垃圾處理掉了
  • 比如像下面這樣的程式碼就很可憐
function fn() {
    var a = 123;    // fn執行完畢後,變數a就將被銷燬了
    console.log(a);
}
fn();
複製程式碼

雖然以上垃圾回收的過程我們無法親眼看見,但是聽者傷心聞者流淚啊。可不可以不要如此殘忍,我願傾其所有,換你三生三世。

悲傷的到來,我們無法拒絕,那就讓我們想辦法去改變這一切。現在再讓我們來看下這段程式碼:

function add() {
    var a = 1;
    return function() {
        a++;
        console.log(a);
    }
}
var fn = add();
fn();   // 2
fn();   // 3
fn();   // 4
複製程式碼

這段程式碼最神奇的地方就是,當add函式執行完後,區域性變數a並沒有被銷燬,而是依然存在,這其中到底發生了什麼?讓我們慢慢分析一下:

  • 當fn = add()時,fn返回了一個函式的引用,這個函式裡有區域性變數a
  • 既然這個區域性變數還能被外部訪問fn(),就沒有必要把它給銷燬了,於是就保留了下來

閉包是個好東西,可以完成很多工作,其中就包括一道網上常考的經典題目

    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <script>
        var aLi = document.getElementsByTagName('li');
        for (var i = 0; i < aLi.length; i++) {
            aLi[i].onclick = function() {
                console.log(i);     // ?
            };
        }
    </script>
複製程式碼

見過這道題的觀眾請舉手,確實這道題的目的就是為了考對閉包的理解。上面的答案無論怎麼點結果都是4。

這是因為li節點的onclick事件屬於非同步的,在click被觸發的時候,for迴圈以迅雷不及掩耳盜鈴的速度就執行完畢了,此時變數i的值已經是4了

因此在li的click事件函式順著作用域鏈從內向外開始找i的時候,發現i的值已經全是4了

解決方法就需要通過閉包,把每次迴圈的i值都存下來。然後當click事件繼續順著作用域鏈查詢的時候,會先找到被存下來的i,這樣每一個li點選都可以找到對應的i值了

    <script>
        var aLi = document.getElementsByTagName('li');
        for (var i = 0; i < aLi.length; i++) {
            (function(n) {    // n為對應的索引值
                aLi[i].onclick = function() {  
                    console.log(n);     // 0, 1, 2, 3
                };
            })(i);  // 這裡i每迴圈一次都存一下,然後把0,1,2,3傳給上面的形參n
        }
    </script>
複製程式碼

其他作用

閉包應用非常廣泛,我們這裡就說一下大家熟知的,比如可以封裝私有變數,可以把一些不需要暴露在全域性的變數封裝成私有變數,這樣可以防止造成變數的全域性汙染

var sum = (function() {
    var cache = {};     // 將cache放入函式內部,避免被其他地方修改
    return function() {
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        var a = 0;
        for (var i = 0; i < arguments.length; i++) {
            a += arguments[i];
        }
        return cache[args] = a;
    }
})();
複製程式碼

除此之外相信很多人都見過一些庫如jQuery,underscore他們的最外層都是類似如下樣子的程式碼

(function(win, undefined) {
    var a = 1;
    var obj = {};
    obj.fn = function() {};
    
    // 最後把想要暴露出去的內容可以掛載到window上
    win.obj = obj;
})(window);
複製程式碼

是的,沒錯,利用閉包也可以做到模組化。另外還可以將變數的使用延長,再來看一個例子

var monitor = (function() {
    var imgs = [];
    return function(src){
        var img = new Image();
        imgs.push(img);
        img.src = src;
    }
})();

monitor('http://dd.com/srp.gif');
複製程式碼

上面的程式碼是用於打點進行統計資料的情形,在之前的一些瀏覽器中,會出現打點丟失的情況,因為img是函式內的區域性變數,當函式執行完後img就被銷燬了,而此時可能http請求還沒有發出。

所以遇到這種情況的時候,把img變數用閉包封裝起來,就可以解決了

記憶體管理

很多人都聽過一個版本,就是閉包會造成記憶體洩漏,所以要儘量減少閉包的使用

Just now就來為閉包來正名,不是你想象那樣的:

  • 區域性變數本來應該隨著函式的執行完畢被銷燬,但如果區域性變數被封裝在閉包形成的環境中,那這個區域性變數就一直能存在。從我們上面實踐得出的結果來看,這話說的沒毛病
  • But之所以使用閉包是因為我們想要把一些變數存起來方便以後使用,這和放到全域性下,對記憶體的影響是一致的,並不算是記憶體洩漏。如果在將來想回收這些變數,直接把變數設為null即可了
  • 還有就是在使用閉包的同時比較容易形成迴圈引用,如果閉包的作用域鏈中儲存著一些DOM節點,此時就有可能造成記憶體洩漏。但這本身並非閉包的問題,也並非js的問題
  • 要怪就怪老版本的IE同志吧,它內部實現的垃圾回收機制採用的是引用計數策略。在老同志IE中,如果兩個物件之間形成了迴圈引用,那麼這兩個物件都不能被回收,但迴圈引用造成的記憶體洩漏其本質也不是閉包的錯
  • 同樣要解決迴圈引用代理的記憶體洩漏問題,只需把迴圈引用中的變數設為null就好

上面就是我們替閉包的正名,閉包也不容易,被人用還不討好。它明白,不是它的鍋,它是不需要背的!

這不是終點

雖然不是終點,但還是要搞個總結性發炎的,不然怎麼對得起扁桃體兄

閉包

  • 是一個能夠讀取其他函式內部變數的函式,實質上是變數的解析過程(由內而外)
  • 可以用來封裝私有變數,實現模組化
  • 可以儲存變數到記憶體中

說完了閉包,我們先休息個一分鐘,稍微理理思路。

然而真正的原因是由於文章內容過長,拆分成了姊妹篇

So還請客官移步,小憩片刻之後,來繼續進入下一個主題,高階函式

相關文章