閉包你真的了解嗎?

開始之前

這是回北京后被疫情困在家的第四周了,在彈盡糧絕和剛剛看完《十二道鋒味》芬蘭之旅,還沉浸在奇幻的極光和誘人的西餐中開始寫這篇網(wǎng)絡(luò)日記。關(guān)于閉包在之前面試的時(shí)候就想寫了,但是因?yàn)槭聝憾啵α私o耽擱下了。而且寫一篇技術(shù)類的網(wǎng)絡(luò)日記真的還是挺費(fèi)時(shí)間的。寫這種技術(shù)類的日記不像小時(shí)候?qū)懙娜沼?,記錄一些流水,這個(gè)真的還是要備課。這片日記還是以阮一峰老師講閉包為主。但是阮老師寫的關(guān)于閉包單刀直入,似乎少了一些前情知識(shí),所以在內(nèi)容上面做了一些調(diào)整,然后加入了幾個(gè)實(shí)戰(zhàn)的案例來(lái)分析JavaScript這個(gè)重要的概念——閉包。

環(huán)境與作用域

閉包之前先了解一些 JavaScript 的環(huán)境和作用域有助于對(duì)閉包的理解,可以說(shuō)是這部分內(nèi)容是閉包的先行知識(shí)。

任何的編程語(yǔ)言其實(shí)都有這個(gè)概念,環(huán)境就是函數(shù)運(yùn)行的環(huán)境,作用域就是函數(shù)作用的范圍。舉個(gè)類似的例子,一座城市。城市里有學(xué)校,醫(yī)院,商場(chǎng)等基礎(chǔ)設(shè)施是市民的生活環(huán)境,城市的存在需要市民的依賴,只有市民依賴城市生活這座城市才會(huì)存在,當(dāng)某一天市民不再依賴這座城市生活了,城市基本上也就破敗了,也就不復(fù)存在了。城市里面的一個(gè)個(gè)基礎(chǔ)設(shè)施單位學(xué)校,超市等也是一樣,沒人用了就倒閉了回收了。城市里面一些基礎(chǔ)設(shè)施會(huì)有自己的輻射范圍,尤其像是學(xué)校,都會(huì)劃學(xué)區(qū),就規(guī)定了這個(gè)學(xué)校就服務(wù)自己所在片區(qū)的孩子?,F(xiàn)在我們做對(duì)比的話我們可以把城市想象成JavaScript中的全局環(huán)境。城市中一個(gè)個(gè)的基礎(chǔ)設(shè)置就相當(dāng)于函數(shù),函數(shù)中也會(huì)有自己的小環(huán)境。每個(gè)環(huán)境都有自己的作用范圍,全局環(huán)景就對(duì)應(yīng)全局作用域,函數(shù)環(huán)境就對(duì)應(yīng)函數(shù)作用域。
javaScript全局環(huán)境有一個(gè)特點(diǎn)就是全局環(huán)境永遠(yuǎn)不會(huì)被回收。舉個(gè)例子

<html>
    .....
  <body>
    <script>
      let title = 'javascript closure'
      console.log(title);
    </script>
  </body>
</html>

上面的代碼不完整啊,就是簡(jiǎn)單的寫一個(gè)html頁(yè)面,里面只有上面的一個(gè)script定義的內(nèi)容,其他什么都沒有。我們會(huì)發(fā)現(xiàn),當(dāng)我們用瀏覽器打開,看控制臺(tái)打印出title了,當(dāng)我們?cè)俅卧诳刂婆_(tái)打印title,title還會(huì)會(huì)打印出來(lái),可見這個(gè)腳本執(zhí)行完,這個(gè)環(huán)境并沒有被回收。


上面是全局環(huán)境,我們?cè)倏匆幌氯汁h(huán)境中的局部環(huán)境,先看一下下面這段代碼:

<script type="text/javascript">
    let title = 'JavaScript Closure'
        function foo(){
            let n = 0;
            function show(){
        console.log(title);
                console.log(++n);
            }
            show();
        }
        foo();
    </script>

上面的代碼中我們?cè)偃汁h(huán)境中創(chuàng)建了一個(gè) foo 函數(shù),它會(huì)形成自己的一個(gè)局部環(huán)境,里面有有數(shù)據(jù) n 和 函數(shù) show,調(diào)用show時(shí),他會(huì)在父級(jí)foo的環(huán)境中創(chuàng)建自己的環(huán)境。環(huán)境是只有在函數(shù)調(diào)用的才會(huì)創(chuàng)建,對(duì)應(yīng)計(jì)算機(jī)語(yǔ)言就是創(chuàng)建一塊內(nèi)存區(qū)域。

image

我們運(yùn)行這個(gè)文件會(huì)看到控制臺(tái)打印出總是會(huì)打印出 1 來(lái),即使我們多次調(diào)用 foo(), 每次的結(jié)果都還是一樣的,這說(shuō)明 foo 這個(gè)換環(huán)境每次都是新的,也就是我們每調(diào)用一次就會(huì)開辟一塊新的內(nèi)存空間。
image

關(guān)于作用域,全局作用域只有一個(gè),每個(gè)函數(shù)又都有作用域(環(huán)境)。

  • 編譯器運(yùn)行時(shí)會(huì)將變量定義在所在作用域
  • 使用變量時(shí)會(huì)從當(dāng)前作用域開始向上查找變量
  • 作用域就像攀親戚一樣,晚輩總是可以向上輩要些東西


    image

    作用域鏈只向上查找,找到全局window即終止,所以我們會(huì)看到 show 函數(shù)是可以訪問到 title 這個(gè)變量。但是我們盡量不要在全局作用域上定義變量

但是有的時(shí)候我們想保留一個(gè)函數(shù)環(huán)境或者說(shuō)成是作用域。比如我們說(shuō)上面函數(shù)中的 n, 我們想實(shí)現(xiàn)的效果就是我們每次調(diào)用一次 show 函數(shù) n 就要累加一次。也就說(shuō)但我們執(zhí)行完 foo函數(shù)的時(shí)候他的那塊內(nèi)存區(qū)域不會(huì)被清空回收。

<script type="text/javascript">
        let title = 'JavaScript Closure'
        function foo(){
            let n = 0;
            return function show(){
                console.log(title);
                console.log(++n);
            }
        }
        let a = foo();
        a(); // JavaScript Closure   1
        a(); // JavaScript Closure   2
        a(); // JavaScript Closure   3
    </script>

我們可以看到當(dāng)我們把 show 函數(shù)的引用返回出去,foo的環(huán)境就會(huì)被保留。那么也就是說(shuō)子函數(shù)被外部使用時(shí),它的父級(jí)環(huán)境就是會(huì)保留的。
其實(shí)我們經(jīng)常使用的構(gòu)造函數(shù)就是一個(gè)很好的環(huán)境例子,子函數(shù)被外部使用時(shí),父級(jí)環(huán)境將會(huì)保留。

function User() {
  let a = 1;
  this.show = function() {
    console.log(a++);
  };
}
let a = new User();
a.show(); //1
a.show(); //2
let b = new User();
b.show(); //1

其實(shí)構(gòu)造函數(shù)我們可以看作是下面代碼的一種變形(構(gòu)造函數(shù)實(shí)際上會(huì)復(fù)雜一些)

function User(){
  let a = 1;
  function show(){
    console.log(a++)
  }
  return {
    show:show
  }
}

ES6中 letconst 可以變量的聲明放在塊級(jí)作用域中(放在新環(huán)境,而不是全局中)。

{
    let a = 9;
}
console.log(a); //ReferenceError: a is not defined
if (true) {
    var i = 1;
}
console.log(i);//1

對(duì)于這一點(diǎn)我們之前寫 for 循環(huán)肯定最有感觸,比如下面的例子:

let arr = [];
for (var i = 0; i < 10; i++) {
    arr.push((() => i));
}
console.log(arr[3]()); //10 
=================================
let arr = [];
for (let i = 0; i < 10; i++) {
    arr.push((() => i));
}
console.log(arr[3]()); //3 

下面這個(gè)圖可以表示一下著兩個(gè)區(qū)別:

for 循環(huán)的時(shí)候會(huì)開辟很多會(huì)塊級(jí)作用于,當(dāng)使用 var 的時(shí)候,i 是定義在了全局環(huán)境中,而使用 let 就會(huì)在新開辟的塊級(jí)作用域聲明一個(gè) i 變量,這樣在函數(shù)執(zhí)行的時(shí)候在本塊級(jí)作用域就找到了,就不用去全局找了。全局中如果沒有其他地方定義 i,其實(shí)全局中 i 也是不存在的。
當(dāng)然了如果我們既要保留現(xiàn)在的效果,又要保留全局中的 i ,我們使用我們的老方法

//自行構(gòu)建閉包
var arr = [];
for (var i = 0; i < 10; i++) {
  (function (a) {
      arr.push(()=>a);
  })(i);
}
console.log(arr[3]()); //3
console.log(i); //10

閉包

在 JavaScript 中,函數(shù)內(nèi)部可以直接讀取全局變量,在函數(shù)外部無(wú)法讀取函數(shù)內(nèi)的局部變量。但是出于種種原因,我們有時(shí)候需要得到函數(shù)內(nèi)的局部變量,那就是在函數(shù)內(nèi)部再定義一個(gè)函數(shù),他可以讀取函數(shù)內(nèi)部的變量,然后我們把這個(gè)函數(shù)返回,就到了這個(gè)效果。上面 show 那個(gè)例子就可以看出,我們利用它成功讀取了 foo 函數(shù)內(nèi)部變量 n。這個(gè)就是閉包。

那么到底什么是閉包,用阮老師的話是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。

由于在Javascript語(yǔ)言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,因此可以把閉包簡(jiǎn)單理解成"定義在一個(gè)函數(shù)內(nèi)部的函數(shù)"。

所以,在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)的一座橋梁。他是函數(shù)作用域的一種延伸。

閉包的用途

閉包可以用在許多地方。它的最大用處有兩個(gè),一個(gè)是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個(gè)就是讓這些變量的值始終保持在內(nèi)存中。

怎么來(lái)理解這句話呢?請(qǐng)看下面的代碼。

function f1(){
  var n=999;
  nAdd=function(){n+=1}
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000

在這段代碼中,result實(shí)際上就是閉包f2函數(shù)。它一共運(yùn)行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數(shù)f1中的局部變量n一直保存在內(nèi)存中,并沒有在f1調(diào)用后被自動(dòng)清除。

為什么會(huì)這樣呢?原因就在于f1是f2的父函數(shù),而f2被賦給了一個(gè)全局變量,這導(dǎo)致f2始終在內(nèi)存中,而f2的存在依賴于f1,因此f1也始終在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后,被垃圾回收機(jī)制(garbage collection)回收。

這段代碼中另一個(gè)值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關(guān)鍵字,因此nAdd是一個(gè)全局變量,而不是局部變量。其次,nAdd的值是一個(gè)匿名函數(shù)(anonymous function),而這個(gè)匿名函數(shù)本身也是一個(gè)閉包,所以nAdd相當(dāng)于是一個(gè)setter,可以在函數(shù)外部對(duì)函數(shù)內(nèi)部的局部變量進(jìn)行操作。

實(shí)戰(zhàn)應(yīng)用

假設(shè)我們現(xiàn)在有一個(gè)按鈕,我們想要實(shí)現(xiàn)的效果是當(dāng)我們點(diǎn)擊按鈕的時(shí)候,按鈕會(huì)向右邊移動(dòng)。下面來(lái)實(shí)現(xiàn)一下這個(gè)效果:
Version 1

<!DOCTYPE html>
<html>
<head>
    <title>JavaScript閉包</title>
</head>
<body>
    <button id='button' style="position: absolute;">JavaScript</button>
    <script type="text/javascript">
        window.onload = function(){
            let button = document.getElementById('button');
            button.addEventListener('click',function(){
                let left = 1;
                setInterval(function(){
                    button.style.left  = left++ + 'px';
                },100)
            })
        }
    </script>
</body>
</html>
image

我們會(huì)看到這個(gè)按鈕是一直抖動(dòng)的,因?yàn)槲覀兠看吸c(diǎn)擊的時(shí)候都會(huì)開辟一塊作用域,如下圖:

如果我們要改進(jìn)它,需要我們將 left 這個(gè)值保留下來(lái),也就是放到父級(jí)中去:
Version 2

<script type="text/javascript">
        window.onload = function(){
            let button = document.getElementById('button');
            let left = 1;
            button.addEventListener('click',function(){
                setInterval(function(){
                    button.style.left  = left++ + 'px';
                },100)
            })
        }
    </script>

我們將 left 放到父級(jí)作用域來(lái),我們會(huì)看到這個(gè)按鈕就不會(huì)再抖動(dòng)了。當(dāng)然了目前還是不完善的,因?yàn)楫?dāng)我們多點(diǎn)擊按鈕幾次,這個(gè)按鈕會(huì)越來(lái)越快,原因跟V1是相同的,left是成了父級(jí)了,但是setinterval還在,我們點(diǎn)擊 n次,相當(dāng)于interval形成了n個(gè)事件隊(duì)列,執(zhí)行的事件間隔就相當(dāng)于 100 / n。所以我們要判定,如果事件綁定了我們就不要再次綁定了。

Version 3

<script type="text/javascript">
        window.onload = function(){
            let button = document.getElementById('button');
            let left = 1;
            let interval = false;
            button.addEventListener('click',function(){
                if(!interval){
                    interval = true;
                    setInterval(function(){
                        button.style.left  = left++ + 'px';
                    },100)
                }
            })
        }
    </script>

使用閉包注意的點(diǎn)

1)由于閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會(huì)造成網(wǎng)頁(yè)的性能問題,在IE中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。

2)閉包會(huì)在父函數(shù)外部,改變父函數(shù)內(nèi)部變量的值。所以,如果你把父函數(shù)當(dāng)作對(duì)象(object)使用,把閉包當(dāng)作它的公用方法(Public Method),把內(nèi)部變量當(dāng)作它的私有屬性(private value),這時(shí)一定要小心,不要隨便改變父函數(shù)內(nèi)部變量的值。

對(duì)于第一個(gè)舉個(gè)例子解釋一下:

對(duì)于下面點(diǎn)擊事件我們要輸出元素的desc信息,因?yàn)橐粋€(gè)個(gè)點(diǎn)擊事件一直駐留在內(nèi)存當(dāng)中的,所以其父級(jí)中的 item 就會(huì)一直存在,有幾個(gè)存在幾份。

<body>
  <div desc="JavaScript">JavaScript</div>
  <div desc="Closure">Closure</div>
</body>
<script>
  let divs = document.querySelectorAll("div");
  divs.forEach(function(item) {
    item.addEventListener("click", function() {
      console.log(item.getAttribute("desc"));
    });
  });
</script>

如果有很多按鈕,就有很多個(gè) item,而我們只需要一個(gè)描述信息的文本而已,所以我們只要一個(gè)他的屬性就可以了,不需要它一直在,免得太多了造成了內(nèi)存泄漏,所以可以這樣改進(jìn):

<script>
  let divs = document.querySelectorAll("div");
  divs.forEach(function(item) {
    let desc = item.getAttribute("desc")
    item.addEventListener("click", function() {
      console.log(desc);
    });
    item = null;
  });
</script>

我們將其屬性賦個(gè)一個(gè)變量,讓他來(lái)代替 item 駐留內(nèi)存總,在最后我們把item設(shè)置為 null,釋放空間。一個(gè)對(duì)象的屬性總是比這個(gè)完整的對(duì)象消耗的空間小,所以我們就到達(dá)了內(nèi)存優(yōu)化的目的。

阮老師的思考題

如果你能理解下面兩段代碼的運(yùn)行結(jié)果,應(yīng)該就算理解閉包的運(yùn)行機(jī)制了。

代碼片段一:

var name = "The Window";

var object = {
  name : "My Object",

  getNameFunc : function(){
    return function(){
      return this.name;
    };

  }

};

alert(object.getNameFunc()());

代碼片段二:

var name = "The Window";

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

寫在最后

這篇日記總共寫了三天,這種基礎(chǔ)性的知識(shí)說(shuō)起來(lái)感覺比應(yīng)用型的難好多。應(yīng)用性的直接介紹一下屬性,放幾個(gè)實(shí)例,說(shuō)一下就可以,放一下效果圖就可以,這種語(yǔ)言知識(shí)說(shuō)的時(shí)候還真的不好說(shuō)起,當(dāng)然了也是我水平不行,自己對(duì)這一塊并沒有理解透徹。所以如果大家看到了這篇日記,發(fā)現(xiàn)上面講的不好不對(duì),還請(qǐng)?jiān)谙旅媪粞?。希望能和大家在前端學(xué)習(xí)的道路上一起進(jìn)步。另外今天是女神節(jié),祝愿所有女生都能成為女神,所有男生都能成為自己女神心中的“閉包”。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 作用域和閉包是 JavaScript 最重要的概念之一,想要進(jìn)一步學(xué)習(xí) JavaScript,就必須理解 Java...
    劼哥stone閱讀 1,249評(píng)論 1 13
  • ??函數(shù)表達(dá)式是 JavaScript 中的一個(gè)既強(qiáng)大有容易令人困惑的特性。定義函數(shù)的的方式有兩種: 函數(shù)聲明; ...
    霜天曉閱讀 894評(píng)論 0 1
  • 閉包(closure)是Javascript語(yǔ)言的一個(gè)難點(diǎn),也是它的特色,很多高級(jí)應(yīng)用都要依靠閉包實(shí)現(xiàn)。 一、變量...
    zouCode閱讀 1,365評(píng)論 0 13
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 5,722評(píng)論 16 88
  • 玄霜又低嘆道:“人為刀俎,我為魚肉。我就是最乖的魚肉,你說(shuō)呢?”他音量控制得極低,四周只他二人能聽得。上官耀華心下...
    _____以歿炎涼閱讀 236評(píng)論 0 1

友情鏈接更多精彩內(nèi)容