JavaScript之閉包,給自己的Js一場重生(系列七)

JavaScript之閉包

閉包——非常重要但又難以掌握的概念,理解閉包可以看作是某種意義上的重生——《你不知道的Js》
雖然關於閉包,雖然大家可能已經看膩了,但我仍要試着去總結下它!!!

目錄

前言

大家在閱讀這篇文章之前,不妨先閱讀一下我的前面幾篇文章作爲前置知識:

一、什麼是閉包

顧名思義,遇見問題先問爲什麼是我們一貫的思維方式,我們嘗試回答一下:

  1. 閉包就是函數內部的子函數—— 等於沒說
  2. 當函數可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。——靠譜
  3. 閉包就是能夠讀取其他函數內部變量的函數,在本質上是函數內部和函數外部鏈接的橋樑——靠譜
  4. 函數和對其周圍狀態(詞法環境)的引用捆綁在一起構成閉包(closure)——很靠譜

我們試着用代碼來描述一下上面的回答,看看你最中意哪一個~

1.1 閉包是函數內部的子函數

先看這段代碼:

function foo(params) {
    var a = '餘光';

    function bar() {
        console.log(a);
    }
    bar()
}

foo(); // 餘光

基於詞法作用域的查找規則,bar函數可以成功的打印a變量,並且它也是foo的子函數,但嚴格來說它並沒有清晰的表達出閉包這一概念,說它表達的是嵌套函數可以訪問聲明於大外部作用域的變量更準確一些。

1.2 閉包就是能夠讀取其他函數內部變量的函數,在本質上是函數內部和函數外部鏈接的橋樑

再來看下面的例子:

function foo(params) {
    var a = '餘光';

    function bar() {
        console.log(a);
    }
    return bar;
}

var res = foo();
res(); // 餘光

結果一致,這是因爲此時res是執行foo函數時返回的bar引用,bar函數得以保存了它餓詞法環境。

1.3 函數和對其周圍狀態(詞法環境)的引用捆綁在一起構成閉包(closure)

我們來看下面代碼:

var name = '餘光';

function foo() {
  console.log(name); // 餘光 
}

foo(); //餘光

foo的上下文被靜態的保存了下來,而且是在該函數創建的時候就保存了。下面我們來驗證一下:

var name = '餘光';

function foo() {
  console.log(name); // 餘光
}

(function (func) {
    var name = '老王';

    func()
})(foo); // 餘光

這裏我們就可以理解——函數被創建後就形成了閉包,他們保存了上層上下文的作用域鏈,並且保存在[[scope]]中,如果你對[[scope]]的概念已經模糊了,不妨花幾分鐘看看《JavaScript中的執行上下文》這篇文章。

1.4 總結

注意:閉包是函數內部的返回的子函數這句話本身沒錯,但要看從什麼角度出發:

ECMAScript中,閉包指的是:

  1. 從理論角度:所有的函數。因爲它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因爲函數中訪問全局變量就相當於是在訪問自由變量,這個時候使用最外層的作用域。
  2. 從實踐角度:以下函數纔算是閉包:
    • 即使創建它的上下文已經銷燬,它仍然存在(比如,內部函數從父函數中返回)
    • 在代碼中引用了自由變量

總結:

  • 閉包代碼塊創建該代碼塊的上下文中數據的結合
  • 閉包就是能夠讀取其他函數內部變量的函數,在本質上是函數內部和函數外部鏈接的橋樑
  • 不同的角度對閉包的解釋不同的

注意:這些並不是閉包的全部,就好像當你被問到——閉包是什麼的時候,你的上述回答並不能結束這個話題,往往會引申出更多的話題。

在這裏插入圖片描述

二、嘗試分析閉包

還是那段經典代碼:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo(); // local scope

首先我們要分析一下這段代碼中執行上下文棧和執行上下文的變化情況。

  1. 進入全局代碼,創建全局執行上下文,全局執行上下文壓入執行上下文棧
  2. 全局執行上下文初始化
  3. 執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 執行上下文被壓入執行上下文棧
  4. checkscope 執行上下文初始化,創建變量對象、作用域鏈、this等
  5. checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
  6. 執行 f 函數,創建 f 函數執行上下文,f 執行上下文被壓入執行上下文棧
  7. f 執行上下文初始化,創建變量對象、作用域鏈、this等
  8. f 函數執行完畢,f 函數上下文從執行上下文棧中彈出

在這裏插入圖片描述

f 函數執行的時候,checkscope 函數上下文已經被銷燬了啊(即從執行上下文棧中被彈出),怎麼還會讀取到 checkscope 作用域下的 scope 值呢?

當我們瞭解了具體的執行過程後,我們知道 f 執行上下文維護了一個作用域鏈:

因爲這個作用域鏈:

  • f 函數依然可以讀取到 checkscopeContext.AO 的值;
  • f 函數引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷燬了,JavaScript 依然會讓 checkscopeContext.AO 活在內存中;
  • f 函數依然可以通過 f 函數的作用域鏈找到它,正是因爲 JavaScript 做到了這一點,從而實現了閉包這個概念。

多麼浪漫的思想——只要你需要我,那我我本應該被銷燬,你也能找到我~

在這裏插入圖片描述

三、經典問題

3.1 多個對象引用同一個[[Scope]],你遇到過嗎?

直接上代碼:

var child1;
var child2;
function parent() {
    var x = 1;

    child1 = function () {
        console.log(++x)
    };
    child2 = function () {
        console.log(--x)
    };
}
parent();
child1(); // 2
child1(); // 3
child2(); // 2

大家可能不理解,child1child他們兩個函數在創建後都保存了上層上下文,萬萬沒想到,同一個上下文創建的閉包是共用一個[[scope]]屬性的,某個閉包對其中[[Scope]]的變量做修改會影響到其他閉包對其變量的讀取。

3.2 閉包輕鬆解決的經典問題

大家一定對下面這段代碼很眼熟:

var arr = []
for(var i = 0; i < 10; i++){
    arr[i] = function () {
        console.log(i)
    }
}
arr[0](); // 10
arr[1](); // 10
arr[2](); // 10
arr[3](); // 10

我們這麼解釋它:同一個上下文中創建的閉包是共用一個[[Scope]]屬性的

因此上層上下文中的變量i是可以很容易就被改變的。

arr[0],arr[1]…arr[9]他們共用一個[[scope]],最終執行的時候結果當然一樣。

如何利用閉包來解決這個問題呢?

var arr = []
for(var i = 0; i < 10; i++){
    arr[i] = (function (i) {
        return function () {
            console.log(i);
        }
    })(i)
}
arr[0](); // 0
arr[1](); // 1
arr[2](); // 2
arr[3](); // 3

我們通過立即執行匿名函數的方式隔離了作用域,當執行 arr[0] 函數的時候,arr[0] 函數的作用域鏈發生了改變:

arr[0]Context = {
    Scope: [AO, 匿名函數Context.AO globalContext.VO]
}

匿名函數執行上下文的AO爲:

匿名函數Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

我們看到,這時函數的[[Scope]]屬性就有了真正想要的值了,爲了達到這樣的目的,我們不得不在[[Scope]]中創建額外的變量對象。要注意的是,在返回的函數中,如果要獲取i的值,那麼該值還是會是10。

3.3 總結

  • 函數內的所有內部函數都共享一個父作用域,因此創建的閉包是共用的。
  • 利用閉包隔離作用域的特性可以解決共享作用域的問題

在這裏插入圖片描述

參考

寫在最後

JavaScript內功系列:

  1. this、call、apply詳解,系列(一)
  2. 從原型到原型鏈,系列(二)
  3. 從作用域到作用域鏈,系列(三)
  4. JavaScript中的執行上下文(四)
  5. JavaScript中的變量對象(五)
  6. JavaScript之自執行函數表達式(六)
  7. 本文
  8. javaScript中的值傳遞都經歷了什麼?

關於我

  • 花名:餘光
  • WX:j565017805
  • 沉迷JS,水平有限,虛心學習中

其他沉澱

如果您看到了最後,不妨收藏、點贊、評論一下吧!!!
持續更新,您的三連就是我最大的動力,虛心接受大佬們的批評和指點,共勉!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章