你離高質量程式碼封裝只差一個閉包,快來get吧!

前端有貓膩發表於2022-03-15

海闊憑魚躍,天高任鳥飛。Hey 你好!我是貓力Molly

閉包已經是一個老生常談的問題了,不同的人對閉包有不同的理解。今天我來淺談一下閉包,大家一起來就“閉包”這個話題,展開討論,希望能擦出一些不一樣的火花。

理解閉包之前,我們得先了解“上下文”和“作用域”兩個知識點

上下文

瀏覽器引擎在解析js程式碼的時候,大致會經過兩個階段。解析階段執行階段

解析階段:一段程式碼說白了只是一段有規則的程式碼文字而已,所以js引擎會拿到程式碼時會事先解析程式碼,初始化過程中會將變數,引數,函式,表示式,運算子等等提取並存起來,並且將變數預設賦值為undefined,函式預設為函式塊,確定上下文關係等一系列準備工作

執行階段:由上往下逐行執行程式碼,遇到對應的變數或函式,則去倉庫裡面匹配執行

console.log(str);
console.log(fun);
var str = "molly";
let str1;
console.log(str1);
function fun(a, b) {
    return a + b;
}

定義: 上下文可分為全域性上下文區域性上下文,上下文決定了變數或函式他們可以訪問哪些資料,以及他們的行為(在初始化階段就已經確定好),每一個上下文都有一個變數物件(環境記錄) ,這個上下文中定義的所有變數和函式都會儲存在這個變數物件當中,我們無法直接通過程式碼訪問到這個變數物件,但是我們可以通過打斷點的方式檢視到。

全域性上下文會在程式退出前(例如關閉網頁或退出瀏覽器)被銷燬,區域性上下文會在其程式碼執行完畢後被銷燬

那麼這個變數物件長啥樣呢?不著急,我們接著往下看

上下文執行棧

定義: 每個函式呼叫都有自己的上下文,當函式執行時,函式的上下文會被推入到一個上下文執行棧上,在函式執行完畢後,上下文執行棧會彈出該函式的上下文,將控制權返還給之前的執行上下文。

// 一個簡單的例子,斷點除錯呼叫棧和上下文變數物件
let a_name = "貓力";
var a_sex = "男";
var a = "111";
function a_molly(age) {
    let a_like = "愛學習";
    var a_like2 = "愛運動";
    a_say(a_like);
    var a = "222";
    console.log(a);
    let test = "來啦?";
}
function a_say(a_like) {
    let code = "敲程式碼";
    console.log(a_like);
}
a_molly();

執行如上示例程式碼,我們在控制檯打上斷點,來觀察執行棧(call stack) 的過程

 執行棧.jpg

注意觀察左邊的call stack(執行棧)和右邊的scope(環境記錄)還有斷點位置

通過觀察斷點除錯結果,我們可以得到以下結論:

  • 當指令碼程式初始化時,會往所有的上下文執行棧底部推入一個全域性上下文,也就是Global屬性
  • 每當執行到函式時,會往執行棧裡面追加一個 “函式上下文”
  • 當函式執行完畢之後,會清除對應的 “函式上下文”
  • 每個上下文內部,確定了可以訪問的資料

作用域和作用域鏈

上下文中的程式碼在執行的時候,會建立變數物件的一個作用域鏈。這個作用域鏈決定了各級上下文中的程式碼在訪問變數和函式時的順序。作用域是包含關係,全域性包含區域性。

總結:

上下文關聯了變數物件決定了函式可以訪問哪些資料,而作用域則是決定了資料訪問的規則。

簡單來說,作用域的訪問規則可以總結為:由內向外查詢訪問,內部可以訪問外部而外部無法訪問內部,這樣的訪問方式可以稱之為作用域鏈

函式引數被認為是當前上下文中的變數,因此也跟上下文中的其他變數遵循相同的訪問規則。

閉包

現在我們已經簡單瞭解了 “上下文”和“作用域”兩個知識點,再來談閉包就十分友好了

閉包的定義

紅寶書: 閉包指的是那些引用了另一個函式作用域中變數的函式

MDN: 一個函式和對其周圍狀態(lexica environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被建立出來。

阮一峰: 閉包就是能夠讀取其他函式內部變數的函式。只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成"定義在一個函式內部的函式"。

總結一下: 函式的執行,可以觸發另一個函式的定義(函式宣告,函式表示式),並且支援引用另一個函式作用域中的變數。那麼這個函式就是一個閉包

為什麼會有閉包?

綜上理論我們可以得知,區域性作用域可以訪問全域性作用域,而全域性無法訪問到區域性,兩個不相干的區域性也無法相互訪問,那麼,只要思想不滑坡,辦法總比困難多,要解決此類問題,我們需要變通一下,結論就是:“閉包”

閉包就好像是一座橋樑,把多個不相干的作用域串聯起來,實現互通。

閉包的原理正是利用了變數環境和作用域鏈訪問規則

閉包的用途

閉包最大的用途有兩個

  1. 可以讀取函式體的內部變數
  2. 讓閉包的變數始終保持在記憶體中

閉包的形式:

把函式視為一等公民,視為一個普通變數

1:返回一個函式

function fun(){
    var aaa = 111
    return function(b){
        return aaa+b
    }
}
fun()()
經典場景:防抖節流

2:返回一個函式變數

function fun(a){
    let fn =  function(b){
        return a+b
    }
    return fn;
}
fun()()

3:作為全域性的閉包函式

var call;
function fun(a){
    call =  function(b){
        return a+b
    }
}
fun()
call()

4:作為函式引數傳遞

function fun1(fn){
    fn() //這個fn函式就是閉包
}
function fun2(){
    let str = 'molly'
    function fun3(){
        console.log(str)
    }
    fun1(fun3)
}
fun2()

5:回撥函式


function ajax(data){
    console.log(data)
}
function sync(){
    const obj = {name:'molly',a:a}
    ajax(obj)
}

6:IIFE 立即執行函式

;!(function(){
   ...
});

經典場景:
for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
jquery自定義封裝外掛也是從立即執行函式開始的

閉包的優勢:

  1. 可以訪問到兩個不相干的作用域變數
  2. 作為一個沙箱,儲存變數
  3. 程式碼封裝,工具函式
  4. 函式作為值,進行入參和返回

閉包的劣勢:

  1. 記憶體洩露

如何規避記憶體洩露呢?

  1. 將函式指標指向null,納入垃圾回收範圍
  2. 將閉包執行完畢
function fun(a){
    return function(b){
        return a+b
    }
}
let a = fun()
a=null

再或者將閉包也執行完畢

a()

這裡簡單提一嘴為什麼閉包會導致記憶體洩露,js的垃圾回收機制大致分為 “標記清理”“計數引用” 兩種。當閉包內的變數在另一個函式中有使用時,這個變數則不會被識別為垃圾,而是常駐記憶體當中不會被清理。導致額外記憶體消耗

提問?

你知道有哪些巧用閉包的場景或程式碼麼?歡迎評論區留言討論!

感謝

歡迎關注我的個人公眾號前端有貓膩每天給你推送新鮮的優質好文。回覆 “福利” 即可獲得我精心準備的前端知識大禮包。願你一路前行,眼裡有光!

感興趣的小夥伴還可以加我微信:貓力molly前端交流群和眾多優秀的前端攻城獅一起交流技術,一起玩耍!

相關文章