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

起點

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

畢竟閉包和高階函數這兩種概念,在開發中是很是有份量的。好處多多,妙處多多,那麼咱們就再也不兜圈子了,直接開始今天的主題,閉包&高階函數設計模式

閉包

閉包是前端er離不開的一個話題,並且也是一個難懂又必須明白的概念。提及閉包,它與變量的做用域和變量的生命週期密切相關。 這兩個知識點咱們也沒法繞開,那麼就一塊兒瞭解下吧瀏覽器

變量做用域

首先變量做用域分爲兩類:全局做用域和局部做用域,這個沒話說你們都懂。咱們常說的變量做用域其實也主要是在函數中聲明的做用域bash

  • 在函數中聲明變量時沒有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函數post

function fn() {
    var a = 110;     // a爲局部變量
    return function() {
        console.log(a);
    }
    console.log(a);  // 110
}
var fn2 = fn();
fn2();      // 110
複製代碼

如此這般,這般如此,垂手可得,小case的事,就能夠從外面訪問到局部變量a了ui

那麼到此爲止,咱們已經發現了閉包的其中一個意義:閉包就是可以讀取其餘函數內部變量的函數,嗯,沒毛病,繼續往下看

變量生命週期

在解決了上面如何拿到小樣兒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還請客官移步,小憩片刻以後,來繼續進入下一個主題,高階函數

相關文章
相關標籤/搜索