【進階2-2期】JavaScript深刻之從做用域鏈理解閉包

(關注福利,關注本公衆號回覆[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導)前端

本週正式開始前端進階的第二期,本週的主題是做用域閉包,今天是第7天。webpack

本計劃一共28期,每期重點攻克一個面試重難點,若是你還不瞭解本進階計劃,點擊查看前端進階的破冰之旅git

若是以爲本系列不錯,歡迎轉發,您的支持就是我堅持的最大動力。github

本期推薦文章

JavaScript深刻之閉包 ,因爲微信不能訪問外鏈,點擊閱讀原文就能夠啦。web

推薦理由

本文是從做用域鏈的角度來介紹閉包,不一樣於上文圖解做用域和閉包,本文語言簡練,結構清晰,相比上文要容易理解些。建議搭配上文一塊兒閱讀。面試

閱讀筆記

紅寶書(p178)上對於閉包的定義:閉包是指有權訪問另一個函數做用域中的變量的函數算法

MDN 對閉包的定義爲:閉包是指那些可以訪問自由變量的函數跨域

其中自由變量,指在函數中使用的,但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另一個函數做用域中的變量。數組

使用上一篇文章的例子來講明下自由變量【進階2-1期】深刻淺出圖解做用域鏈和閉包瀏覽器

function getOuter(){
  var date = '1127';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate('今天是:'); //"今天是:1127"
}
getOuter();

其中date既不是參數arguments,也不是局部變量,因此date是自由變量。

總結起來就是下面兩點:

  • 一、是一個函數(好比,內部函數從父函數中返回)
  • 二、能訪問上級函數做用域中的變量(哪怕上級函數上下文已經銷燬)

分析

首先來一個簡單的例子

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

var foo = checkscope(); // foo指向函數f
foo();                    // 調用函數f()

簡要的執行過程以下:

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

那麼問題來了, 函數f 執行的時候,checkscope 函數上下文已經被銷燬了,那函數f是如何獲取到scope變量的呢?

上文(【進階2-1期】深刻淺出圖解做用域鏈和閉包)介紹過,函數f 執行上下文維護了一個做用域鏈,會指向指向checkscope做用域,做用域鏈是一個數組,結構以下。

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

因此指向關係是當前做用域 --> checkscope做用域--> 全局做用域,即便 checkscopeContext 被銷燬了,可是 JavaScript 依然會讓 checkscopeContext.AO(活動對象) 活在內存中,f 函數依然能夠經過 f 函數的做用域鏈找到它,這就是閉包實現的關鍵

概念

上面介紹的是實踐角度,其實閉包有不少種介紹,說法不一。

湯姆大叔翻譯的關於閉包的文章中的定義,ECMAScript中,閉包指的是:

  • 一、從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。
  • 二、從實踐角度:如下函數纔算是閉包:

    • 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
    • 在代碼中引用了自由變量

面試必刷題

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

若是知道閉包的,答案就很明顯了,都是3

循環結束後,全局執行上下文的VO是

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

執行 data[0] 函數的時候,data[0] 函數的做用域鏈爲:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

因爲其自身沒有i變量,就會向上查找,全部從全局上下文查找到i爲3,data[1] 和 data[2] 是同樣的。

解決辦法

改爲閉包,方法就是data[i]返回一個函數,並訪問變量i

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
      return function(){
          console.log(i);
      }
  })(i);
}

data[0]();    // 0
data[1]();    // 1
data[2]();    // 2

循環結束後的全局執行上下文沒有變化。

執行 data[0] 函數的時候,data[0] 函數的做用域鏈發生了改變:

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

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

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

由於閉包執行上下文中貯存了變量i,因此根據做用域鏈會在globalContext.VO中查找到變量i,並輸出0。

思考題

上面必刷題改動一個地方,把for循環中的var i = 0,改爲let i = 0。結果是什麼,爲何???

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

參考

JavaScript深刻之閉包

往期文章查看

每週計劃安排

每週面試重難點計劃以下,若有修改會通知你們。每週一期,爲期半年,準備明年跳槽的小夥伴們能夠把本公衆號[置頂]()了。

  • 【進階1期】 調用堆棧
  • 【進階2期】 做用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函數
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模塊化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網絡概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】性能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff算法
  • 【進階23期】MVVM雙向綁定
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter源碼解析
  • 【進階28期】ReactRouter源碼解析

交流

本人Github連接以下,歡迎各位Star

http://github.com/yygmind/blog

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

若是你想加羣討論每期面試知識點,公衆號回覆[加羣]便可

相關文章
相關標籤/搜索