前端入門19-JavaScript進階之閉包

請叫我大蘇發表於2018-12-06

宣告

本系列文章內容全部梳理自以下幾個來源:

作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。

PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。

正文-閉包

在作用域鏈那篇中,稍微留了個閉包的念想,那麼這篇就來講講什麼是閉包。

概念

這個閉包的概念蠻不好理解的,我在阮一峰的某篇文章中看過大概這麼句話,閉包是對英文單詞的直譯,在中文裡沒有與之對應的句子解釋,因此很難理解閉包究竟指的是什麼。

看過很多解釋,有說閉包就是函式;也有說閉包就是程式碼塊;還有說函式內的函式就稱閉包;還有說當函式返回內部某個函式時,返回的這個函式叫閉包,也有說閉包就是能夠讀取其他函式內部資料(變數/函式)的函式。

MDN 網站裡不同文章裡出現過多種解釋:

  1. 一個閉包是一個可以自己擁有獨立的環境與變數的表示式(通常是函式)
  2. 閉包是函式和宣告該函式的詞法環境的組合,這個環境包含了這個閉包建立時所能訪問的所有區域性變數

另外,在某篇文章中,看過這麼段話:

2009年釋出了ECMAScript-262-5th第五版,不同的是取消了變數物件和活動物件等概念,引入了詞法環境(Lexical Environments)、環境記錄(EnviromentRecord)等新的概念

所以如果對詞法環境這個詞不理解的,可以將其理解成執行上下文,或者作用域鏈。在開頭宣告給的第四個連結中,是有幾篇很早很早之前大佬們翻譯的國外的文章,裡面對閉包的解釋剛好和 MDN 的解釋也很類似:

閉包是程式碼塊和建立該程式碼塊的上下文中資料的結合

如果這個程式碼塊是函式,那麼利用作用域鏈那篇中介紹的相關原理,從本質上看閉包:

函式程式碼,和函式的內部屬性 [[Scope]] 兩者的結合可稱為閉包。 :

對於這麼多文章中對閉包的這麼多種解釋,先不做評價,先來想想,為什麼會有閉包,理清了後,你會發現,其實理解閉包沒那麼難。

閉包意義

先看個例子:

var num = 0;
function a() {
    var num = 1;
    function b() {
        console.log(num);
    }
    return b;
}
var c = a();
c();

呼叫 c() 輸出的是 1,這點在作用域鏈那節已經講解過了,這裡再稍微說下:

呼叫 c(),會為 c 函式建立一個函式執行上下文,其中作用域鏈為:

c函式EC.VO –> a函式EC.VO -> 全域性EC.VO

VO 是變數物件,表示儲存著當前上下文中所有變數的物件,所以如果以 VO 的實際物件表示作用域鏈:

c函式{} –> a函式{num:1} -> 全域性{num:0}

(忽略 VO 中其他與此例無關變數)

所以,函式 c 內的程式碼輸出 num 時,到作用域鏈上尋找時,發現最後使用的是 a 函式內部的 num 變數,最終輸出 1。

但當時也提了個疑問,當程式碼執行到 c() 時,a 函式已經執行結束,那麼 a 函式的 EC 已經從執行環境棧 ECS 中被移出了,c 函式的 EC 裡的作用域鏈為何還會有 a函式EC.VO 存在?

這就是閉包的典型場景了,閉包的意義之一就是解決這種場景。

通過作用域鏈一篇後,我們知道,函式內的變數依賴於函式執行上下文 EC,一般來說,當呼叫函式時,建立函式執行上下文 EC,併入棧 ECS,當函式執行結束時,就將 EC 從 ECS 中移出,並釋放記憶體空間。

通常函式的行為的確是這樣,但當函式如果有返回值時,情況就不一樣了。雖然函式執行結束後它的 EC 確實被移出 ECS,但並沒有被回收,JavaScript 直譯器的垃圾回收機制也有引用計數的處理。

既然記憶體沒被回收,那麼 EC 就還存在,那麼當呼叫 c() 時,雖然 C 的函式執行上下文是新建立的,上下文的作用域鏈也是新建立的,但作用域鏈的取值是當前執行上下文的 VO 拼接上函式物件的內部屬性 [[Scope]]。

這個函式物件的內部屬性 [[Scope]] 儲存的就是這個函式的外層函式的執行上下文裡的作用域鏈,它的值並不是新建立的,一直儲存著外層函式呼叫時生成的外層函式上下文中的作用域鏈,通過它可以訪問到外層函式變數。

再談閉包概念

所以,實際上,網路上這麼多文章裡對閉包的各種解釋,其實都沒錯。如果對作用域鏈的原理理解清楚後,你會發現,其實函式就是閉包,因為由於作用域的機制,讓函式內部也持有建立函式的上下文的資料集合,所以函式符合閉包的特性。

只是在大部分場景下,函式執行結束,函式的 EC 就可以被回收,那麼這種場景閉包並沒有什麼實際應用意義。

除了函式,如果你可以讓某部分程式碼塊持有建立它的上下文的資料集合,那麼這也可以稱為閉包。

常見的一種就是在函式內返回一個物件,物件的某些屬性使用了物件外層的資料,如:

var model = (function () {
    var num = 1;
    return {
        num:num
    }
}());
model.num;

此時,也可以稱返回的這個物件是閉包。

對於閉包,我對它的理解,更傾向於,閉包並不是一種機制,也不是一種具體的事物(如執行上下文),反而,閉包是對原本存在的事物滿足某種場景下的一種稱呼。

也就是說,閉包,它其實是在原有機制,原有事物上的另一種稱呼。所以,網上也才有人會說,閉包是函式、閉包是內嵌的函式等等說法。其實,也不是說這是錯的,他們有的是從閉包特性角度解釋,有的是從閉包現象。

只是,這原本就存在的事物,你本可以就用它原本的稱呼,既然想要用閉包來稱呼它,那麼自然是這個時候,稱呼它為閉包有區別於原本事物的實際意義,所以也才有人會說當函式返回內部函式時,稱為閉包,因為這種時候,返回的這個函式就是用到閉包的特性來解決某些問題,所以稱這種現象為閉包當然就有實際應用場景意義了。

所以,我對閉包的理解,它並不是某個固定不變的東西,也不是某個具體的事物,只要符合閉包特性的原有事物,你都可以稱它為閉包。所以,對於網上那些對閉包的解釋,我的建議是,主謂互換一下,不要說閉包是函式,閉包是內嵌的函式等等,我們可以說,函式是閉包,內嵌的函式也是閉包。只要符合閉包特性的我們都可以稱它為閉包,當然如果還有閉包的實際應用意義,那麼稱它為閉包更可以被人接受。

閉包的應用

作為外部和函式內部變數通訊的橋樑

var model = (function () {
    var num = 1;
    function a() {
        console.log(num);
    }
    return {
        num:num
    }
}());
model.num;

外部是訪問不了函式內部的資訊,而閉包是指程式碼塊持有建立它的上下文的資料集合。那麼,如果在函式內部建立一個閉包,將這個閉包返回給外部,外部是否就可以通過這個閉包作為橋樑來間接與函式內部通訊了。

封裝

var Counter = (function() {
    var privateCounter = 0;
    function changeBy(val) {
        privateCounter += val;
    }
    return {
        increment: function() {
            changeBy(1);
        },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCounter;
        }
    }
})();

還是同樣的原因,外部是訪問不了函式內部的資訊,而閉包是指程式碼塊持有建立它的上下文的資料集合。

那麼,是否就可以藉助閉包的特性,將一些實現封裝在函式內部,通過閉包給外部提供有限的介面使用。

但要注意,函式本來執行結束,它的 EC 從 ECS 棧內移出時,通常就可被回收了,但如果用到了閉包的特性,導致外部持有著函式內部某個引用,此時函式的 EC 就不會被回收,那麼就會佔用著記憶體,使用不當,還會有可能造成記憶體洩漏。


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),公眾號中有我的聯絡方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~
dasuAndroidTv2.png

相關文章