你必須瞭解的JavaScript閉包

2022-01-21 19:01:03
本篇文章給大家帶來了關於JavaScript閉包的學習筆記,其中包括了閉包與方法棧以及閉包的作用,希望對大家有幫助。

從定義上來講,它是一個指令碼語言,而且是一個相對容易學習的指令碼語言。不需要太多的專業知識,你也能夠在一定程度上使用js(JavaScript的簡寫)程式碼。

當然如果你已經學習了一前端知識,你應該能理解這個工具的作用,這是一個非常方便的顯示頁面元素之間間距的工具。你看,你僅僅是進行了一些簡單的瀏覽器操作,甚至你無法理解上述程式碼的內容,但你剛剛確確實實的嵌入了一段js程式碼在你所在的頁面中(顯然它是無害的,請放心使用)感謝up主CodingStartup起碼課的視訊【有了它,把網頁做到跟設計圖一樣】以及up主ArcRain在視訊下方的回覆

這篇學習筆記的目的是記錄我自己對於js學習路程中的一些感悟和體會,以及一些我自己認為的小技巧,而不是為了教學,所以其中的部分內容的原理我並不會給出答案,有可能是我沒法準確的描述,有可能是我還沒弄懂,本人水平相當有限,如果文字中有錯誤的部分歡迎大家指摘。

1. 學習JavaScript的契機

正式學習JavaScript是在培訓班,沒錯我是從培訓班出來的,並不是科班出身,可以說是非常的草根了。我學習的時候ES6標準還並未普及,變數命名還在用非常傳統的var,學習的第一段程式碼是經典的console.log('Hello,world!'),當然它是在控制檯上列印出來的。

當然,在培訓機構中的JavaScript內容講的是非常的淺顯,只有最為基礎的變數定義與命名,function宣告,回撥函數,ajax以及最為基礎的dom操作。顯然這些內容對於工作完全不夠用的。

對於js學習的‘進修’機會來源於我的工作,在工作中我第一次知道了node這個東西,也瞭解到即便是js也是可以做後臺的(我是做的JAVA培訓),也開始逐漸接觸到了一些ES6的標準。當然這些都是後話,最開始我接觸到最大的障礙是這貨。

2.‘噁心’的閉包

啊,對我只有那麼一丁丁點基礎的我,完全無法理解我們公司自己封裝的jsonp程式碼,它是長這個樣子的。

  var jsonp = (function(){
        var JSONP;
       return function(url){
           if (JSONP) {
             document.getElementsByTagName("head")[0].removeChild(JSONP);
          }
         JSONP = document.createElement("script");
          JSONP.type = "text/javascript";
          JSONP.src = url;
          document.getElementsByTagName("head")[0].appendChild(JSONP);
       }
     }())

當然,現在瀏覽器上已經無法通過控制檯直接使用這個方法了,為了防止XSS攻擊瀏覽器已經禁止這樣注入程式碼了,但是在伺服器上還是可以用的,當然,這些都不是重點。

重點是這裡

    if (JSONP) {
       //dosome
 }

如果你和我當初一樣,不知道什麼叫閉包或者對閉包一知半解,那麼,對於這裡你應該也會產生疑問,思路大約是這樣的

第2行定義了JSONP但是沒有賦值,現在JSONP值為null,第三行返回了一個方法,第四行檢測JSONP值是否為空,如果不為空則做了一些事情,好了,後面可以不用看了,這個if白寫了,它百分百進不去!

你看嘛,前面也沒有賦值,然後直接判斷,那它明明就是null。但是實際使用的時候你會發現,這個地方第一次呼叫確實不會進入這個分支,但只要你呼叫了第二次,,它就百分百會進入這個分支。

// 這個是一個可以在控制檯輸出的閉包版本,你可以自己試一下
var closedhull = (function() {
    let name = null; // 這裡直接賦值為null
    return function(msg){
        if(name) {
            console.log('name:', name)
            return name += msg;
        }
        return name = msg;
    }
}())
closedhull('我是第一句。') //我是第一句。
closedhull('我是第二句。') //我是第一句。我是第二句。

上面這個例子執行後,無論是從console.log()亦或是返回值上都不難看出,確實進入了if(name)的分支,這個就是閉包的表現。這裡給出一下閉包的定義

閉包就是能夠讀取其他函數內部變數的函數。

3.閉包的樣子到底是什麼樣的

好了,看過閉包是個啥了,先不說會不會用,至少,算是見過了,閉包有個顯著的特徵return function(){}

不是!

它的顯著特徵是在function內的function!

觀察以下方法

/*第一個案例*/
function test1(){
    // a應該在方法執行結束後銷燬
    let a = 1;
    return {
        add: function(){
            return ++a;
        }
    }
}
let a = test1();
a.add()//2
a.add()//3
/*第二個案例*/
(function(){
    // b應該在方法執行結束後銷燬
    let b = 1,
        timer = setInterval(()=>{
        console.log(++b)
    }, 2000)
    setTimeout(()=>{
        clearInterval(timer)
    }, 10000)
})()// 2 3 4 5 6
/*第三個案例*/
function showMaker(obj){
    // obj應該在方法執行結束後銷燬
    return function(){
        console.log(JSON.stringify(obj))
    }
}
let shower = showMaker({a:1})
// 顯然這裡你還能看到他
shower(); // {"a":1}
/*第四個案例*/
let outObj = (function(){
    let c = 'hello',
        obj = {};
    Object.defineProperty(obj, 'out', {
        get(){
            return c;
        },
        set(v){
            c = v;
        }
    });
    return obj
})()
outObj.out // 可以讀取並設定c的值

這四個都是閉包,他們都具備方法中的方法這一特性。

4.閉包與方法棧(對原理不感興趣可以略過)

閉包的定義,1. 可以在變數的作用域外存取該變數。2. 通過某種手段延長一個區域性變數的生命週期。3. 讓一個區域性變數的存活時間超過它的時間迴圈執行時間

3中由於涉及到了事件迴圈概念,之後涉及到時會去講的,這裡主要討論前兩種方式的定義。

一下內容如果你知道方法棧是個啥了就可以跳過了

區域性作用域:在ES6之前,一般指一個方法內部(從參數列開始,到方法體的括號結束為止),ES6中增加let關鍵字後,在使用let的情況下是指在一個{}中的範圍內(顯然,你不能在隱式的{}中使用let,編譯器會禁止你做出這種行為的,因為沒有{}就沒有塊級作用域),咱們這裡為了簡化討論內容,暫且不把let的塊級作用域算作閉包的範疇(其實應該算,不過意義不大,畢竟,你可以在外層塊宣告它。天啊,JS的命名還沒擁擠到需要在一個方法內再去防止汙染的程度。)

區域性變數:區別於全域性變數,全域性變數會在某些時候被意外額創造和使用,這令人非常的...惱火和無助。區域性變數就是在區域性作用域下使用變數宣告關鍵字宣告出來的變數,應該很好理解。

區域性變數的生命週期:好了,你在一個區域性作用域中通過關鍵字(var const let等)宣告了一個變數,然後給它賦值,這個區域性變數在這個區域性作用域中冒險就開始了,它會被使用,被重新賦值(除了傲嬌的const小姐外),被呼叫(如果它是個方法),這個區域性變數的本質是一個真實的值,區別在於如果它是個物件(物件,陣列,方法都是物件)那麼,它其實本質是一個地址的指標。如果它一個基礎型別,那麼它就是那個真實的值。它之所以存活是因為它有個住所。記憶體。

區域性作用域與記憶體:每當出現一個區域性作用域,一個方法棧就被申請了出來,在這個方法棧大概長這樣子

|  data5 |
|  data4 |
|  data3 |
|  data2 |
|__data1_|

當然,它是能夠套娃的,長這個樣子

|  | d2 |  |
|  |_d1_|  |
|  data3   |
|  data2   |
|__data1___|

如果上面的東西是在太過於抽象,那麼,我可以用實際案例展示一下

function stack1(){
    var data1,
        data2,
        data3,
        data4,
        data5
}
function stack2(){
    var data1,
        data2,
        data3;
    function stackInner(){
        var d1,
            d2;
    }
}

如果方法棧能夠直觀的感受的話,大約就是這個樣子,咱們重點來分析stack2的這種情況,同時寫一點實際內容進去

function stack2(){
    var data1 = '1',
        data2 = {x: '2'},
        data3 = '3';
    function stackInner(){
        var d1 = '4',
            d2 = {y: '5'};
    }
    stackInner()
}
stack2()

顯然其中data1,data3,d1持有的是基本型別(string),data2,d2持有的是參照型別(object),反應到圖上

執行時的方法棧的樣子

            |------>{y: '5'}
            |    |->{x: '2'}
    |  | d2-|   || |
    |  |_d1='4'_|| |
    |  data3='3' | |
    |  data2 ----| |
    |__data1='1'___|

畫有點抽象...就這樣吧。具體物件在哪呢?他們在一個叫堆的地方,不是這次的重點,還是先看方法棧內的這些變數,執行結束後,按照先進後出的原則,把棧內的區域性變數一個一個的銷燬,同時堆裡的兩個物件,由於參照被銷燬,沒了繼續存在的意義,等待被垃圾回收。

接下來咱們要做兩件事情:

  • d1不再等於4了,而是參照data1

  • return stackInner 而不是直接呼叫

這樣閉包就完成了

function stack2(){
    var data1 = {msg: 'hello'},
        data2 = {x: '2'},
        data3 = '3';
    function stackInner(){
        var d1 = data1,
            d2 = {y: '5'};
    }
    return stackInner
}
var out = stack2()

這裡有一個要點,d2賦值給data1一定是在stackInner中完成的,原因?因為再stackInner方法中d2才被宣告出來,如果你在stack2中d1 = data1那麼恭喜你,你隱式的宣告了一個叫d1的全域性變數,而且在stackInner由於變數遮蔽的原因,你也看不到全域性上的d2,原本計劃的閉包完全泡湯。

變數遮蔽:不同作用域中相同名稱的變數就會觸發變數遮蔽。

看看棧現在的樣子

執行時的方法棧的樣子

               |------>{y: '5'}
out<---|       | |----|
    |  |  | d2-| | |  |  |
    |  |--|_d1---|_|  |  |
    |     data3='3'   |  |
    |     data2(略)   |  |
    |_____data1<------|__|

好了,這個圖可以和我們永別了,如果有可能,我後面會用畫圖工具替代,這麼畫圖實在是太過邪典了。

這裡涉及到了方法棧的一個特性,就是變數的穿透性,外部變數可以在內部的任意位置使用,因為再內部執行結束前,外部變數會一直存在。

由於stackInner被外部的out參照,導致這個物件不會隨著方法棧的結束而銷燬,接下來,最神奇的事情來了,由於stackInner這物件沒有銷燬,它內部d1依然保有data1所對應資料的參照,d1,d2一定會活下來,因為他們的爸爸stackInner活下來了,data1也以某種形式活了下來。

為什麼說是某種形式,因為,本質上來說data1還是被銷燬了。沒錯,只不過,data1所參照的那個物件的地址連結沒有被銷燬,這個才是本質。棧在呼叫結束後一定是會銷燬的。但是呼叫本體(方法物件)只要存在,那麼內部所參照的連結就不會斷。

這個就是閉包的成因和本質。

5.閉包有什麼用

OK,我猜測上一個章節估計很多人都直接跳過了,其實,跳過影響也不多,這個部分描述一下結論性的東西,閉包的作用。

它的最大作用就是給你的變數一個名稱空間,防止命名衝突。要知道,你的框架,你export的東西,你import進來的東西,在編譯的時候都會變成閉包,為的就是減少你變數對全域性變數的汙染,一個不依賴與import export的模組的程式碼大概長這個樣子

(function(Constr, global){
    let xxx = new Constr(env1, env2, env3)
    global.NameSpace = xxx;
})(function(parm1, parm2, parm3) {
    //dosomeing
    reutrn {
        a: 'some1',
        b: 'some2',
        funcC(){
            //dosome
        },
        funcD(){
            //dosome
        }
    }
}, window)

當然這種封裝程式碼的風格有多種多樣的,但是大家都儘量把一套體系的內容都放到一個名稱空間下,避免與其他框架產生衝突

相關推薦:

以上就是你必須瞭解的JavaScript閉包的詳細內容,更多請關注TW511.COM其它相關文章!