夯實基礎中篇-圖解作用域鏈和閉包

我不是大熊哦發表於2022-04-18

前言

本文承接上篇 夯實基礎上篇-圖解 JavaScript 執行機制,請先閱讀上篇~

講基礎不容易,本文通過 7個demo和6張圖,和大家一起學習溫故作用域鏈和閉包,本文大綱:

  1. 什麼是作用域鏈
  2. 什麼是詞法作用域
  3. 什麼是閉包
  4. 閉包的實際使用案例

什麼是作用域鏈

正文開始~

請思考下面 demo 的 name 列印什麼

    function test() {
      console.log(name)
    }
    function test1() {
      const name = 'test1的name'
      test()
    }
    const name = 'global的name'
    test1()

通過執行上下文來分析程式碼的執行流程,執行到 test 函式時:

image.png

那 test 函式裡的 name 是哪個呢?這就涉及到了作用域鏈的定義:變數和函式的查詢鏈條就是作用域鏈。它決定了各級上下文中的程式碼在訪問變數和函式時的順序:查詢變數和函式時,先在當前執行上下文找,當前沒有,到下一個執行上下文找,沒有再到下一個,直到全域性執行上下文,都沒有就報錯使用未定義的變數或函式

而在每個執行上下文的變數環境中,都包含了一個外部引用 outer,用來指向外部的執行上下文,鏈條結構是當前執行上下文 > 包含當前上下文的上下文1 > 包含上下文1的上下文2 ...

而這個 demo 會列印global的name,原因是 test 執行上下文的 outer 指向全域性執行上下文,包括 test1 的 outer 也是指向全域性執行上下文:

image.png

也許會有同學疑惑,為什麼 test 的 outer 指向全域性執行上下文,而不是 test1,這是因為在 JavaScript 執行過程中,其作用域鏈是由詞法作用域決定的。

什麼是詞法作用域

詞法作用域就是作用域是由程式碼中函式宣告的位置來決定的,它是靜態的作用域,通過它就能夠預測程式碼在執行過程中如何查詢識別符號,它與函式是怎樣呼叫的沒有關係。所以剛才的例子列印的是global的name

看個具體例子:

    const count = 0
    function test() {
      const count = 1

      function test1() {
        const count = 2

        function test2() {
          const count = 3
        }
      }
    }

其包含關係和作用域鏈:

image.png

image.png

事實上在 Global Scope 全域性作用域(Window)之前,還有一個 Script Scope 指令碼作用域,它存放的是當前 Script 內可訪問的 let 變數和 const 變數,而 var 變數存放在 Global 上的就不在 Script Scope,它類似於是指令碼範圍內的全域性作用域。在下面的 demo 中再舉例。

什麼是閉包

閉包指的是那些引用了另一個函式作用域中變數的函式,通常是在巢狀函式中實現的。

比如這個例子:

    var globalVariable = 1
    const scriptVariable = 2
    
    function test() {
      let name = 'Jaychou'

      return {
        getName() {
          const count = 1
          return name
        },
        setName(newValue) {
          name = newValue
        }
      }
    }

    const testFun = test()
    console.log(testFun.getName()) // Jaychou
    testFun.setName('小明')
    console.log(testFun.getName()) // 小明

大家可以根據作用域鏈的知識,思考一下執行到console.log(testFun.getName())的 getName 裡面的時候作用域鏈是怎樣的~

我們用瀏覽器的開發者工具看一下:

image.png
作用域鏈是當前作用域 》test 函式的閉包 》Script 作用域 》Global 作用域

  1. 為什麼叫 test 函式的閉包?因為當const testFun = test()的 test 函式執行完之後,test 的函式執行上下文已經被銷燬了,但它返回的{ getName(){}, setName(){} }物件被 testFun 引用著,而 getName 和 setName 引用著 test 函式內定義的 name 變數,所以這些被引用的變數依然需要被儲存在記憶體中,而這些變數的集合稱為閉包 Closure;
  2. 目前閉包內的 name 變數就只能通過 getName 和 setName 去訪問和設定,而這也是閉包的作用之一:封裝私有變數;
  3. 剛才說的 Script Scope 中儲存著 scriptVariable 變數,globalVariable 變數是 var 宣告的,所以在 Global Scope(Window)中。

再看1個具體案例理解閉包:

    const globalCount = 0

    function test() {
      const count = 0
      return test1

      function test1() {
        const count1 = 1
        return test2

        function test2() {
          const count2 = 2
          console.log('test2', globalCount + count + count1 + count2)
        }
      }
    }
    test()()()

image.png
執行到 test2 內部的 console.log 那一行時,其作用域鏈是當前作用域 》test1 的閉包 》test 的閉包 》Script Scope 》Global Scope

閉包使用建議:當不需要使用了之後,注意要解除引用著閉包的變數,這樣閉包才會被釋放。比如第1個案例的 testFun 如果不需要用了,就把它釋放 testFun = null。

閉包的實際使用案例

封裝私有變數

就是剛才的 getName、setName 案例,通過 getName 獲取 name,通過 setName 設定 name

封裝單例

    const Single = (function () {
      let instance = null

      return function () {
        if (!instance) {
          instance = {
            name: 'jaychou',
            age: 40
          }
        }
        return instance
      }
    })()
    const obj1 = new Single()
    const obj2 = new Single()
    console.log(obj1 === obj2) // true

這裡只是舉個例子,具體的 instance 是什麼型別,支援什麼功能要看實際專案。

防抖和節流

防抖:

    function debounce(fn, delay) {
      let timer = null;

      return function () {
        let context = this;
        let args = arguments;

        timer && clearTimeout(timer);

        timer = setTimeout(function () {
          fn.apply(context, args);
        }, delay);
      }
    }

節流:

    function throttle(fn, interval) {
      let last = 0;

      return function () {
        let now = +new Date()
        if (now - last >= interval) {
          fn.apply(this, arguments);
          last = now;
        }
      }
    }

更完整的防抖和節流的實現可參考 Lodash ,這裡主要是演示閉包的使用

總結

閉包的使用場景很多,功能很強大,可以說在前端專案中經常可見例如 React Hooks 等等,這裡只列舉了幾個很簡單的很實用的應用場景。

總結

本文主要介紹了作用域鏈和閉包,沿著 夯實基礎上篇-圖解 JavaScript 執行機制 來一起看的話應該比較容易理解,若對大家有所幫助,請不吝點贊關注~

相關文章