史上最詳細的做用域鏈和閉包解釋

有關做用域鏈和原型鏈對於學習js的小夥伴都應該不陌生,今天我們就看看最詳細的做用域鏈解釋,目錄中可能有些一些不常見的名詞,可是這些對於理解做用域鏈和閉包頗有做用,並且基本均可以理解js運行機制了😁前端

目錄

  • 執行環境
  • 函數建立時添加的[[Scopes]]屬性
  • 函數執行時建立的 變量對象(活動對象)
  • 函數執行時建立的做用域鏈 爲Scope
  • 全局執行環境
  • 函數執行環境
  • 閉包的造成
  • 閉包與變量


執行環境(ECMAScript中最重要的概念)

定義:
  • 定義了函數和變量有權訪問其餘數據
  • 決定了函數或變量的行爲

代碼實現:web

const result = add(3,2)// 使用function直接聲明非匿名函數能夠實現「函數聲明提高」
 function add(a,b){
     return a+b
 }
 const value = add(1,2)
複製代碼

圖片展現:數組

產物:瀏覽器

  • 爲該環境中定義的函數添加了[[Scopes]]屬性(稍後詳細解釋)
  • 在執行環境的時候建立的 變量對象(活動對象)(稍後詳細解釋)
  • 在執行環境的時候,經過複製該函數的[[Scopes]]屬性建立的做用域鏈 Scope(稍後詳細解釋)

函數建立時添加的[[Scopes]]屬性

定義:
在每個函數被建立的時候都會爲該函數添加一個[[Scopes]]屬性,該屬性包含了父級(能夠理解爲包含該函數的函數執行環境)的執行環境的做用域鏈



函數執行時建立的 變量對象(活動對象)

定義:
每一個(函數)執行環境都會建立一個變量對象(活動對象),這個變量對象上面包含該執行環境中全部定義的變量和函數還有arguments參數,



函數執行時建立的做用域鏈

定義:
  • 每一個函數建立的時候,都會預先爲該函數添加一個[[Scopes]]屬性,該屬性是父級(能夠理解爲包含該函數的函數執行空間)執行環境的做用域鏈
  • 每一個(函數)執行環境都會取該函數的[[Scopes]]屬性,並copy一份做爲當前的做用域鏈 Scope
  • 並把該函數執行環境的變量對象,推入到當前做用域的最前端

執行中須要讀取的變量都會從該做用域鏈的前端查找(圖中的Local對象),查找過程當中若是沒有找到須要的變量,就會一直找到做用鏈的最頂端window(圖中的Global對象),這就是做用域鏈的查找 bash



全局執行環境

定義:
全局執行環境是js最外層的執行環境,在不一樣的執行環境最外層的執行環境也不一樣,在web中window是全局執行環境。該執行環境一直存在,window的變量對象也一直存在的。

注意:
全局執行環境一直會存在,當關閉瀏覽器時纔會被銷燬markdown



函數執行環境

執行步驟:
  • 1 當執行流到當前函數時,該函數的執行環境就會被推入到一個環境棧(能夠理解爲包含該函數的函數執行環境)中執行閉包

    • 從該函數的[[Scopes]]屬性copy一份,做爲做用域鏈
    • 執行時建立變量對象並初始化它,把該函數中的arguments、定義的變量、定義的函數都會賦值到該變量對象上面。
    • 把當前的變量對象,推入到做用域鏈的最前端
    • 接下來的執行中須要讀取的變量,從該做用域鏈的前端查找,查找過程當中若是沒有找到就會一直找到做用鏈的最頂端window
  • 2 當函數執行環境完畢後(無返回引用類型),該執行環境就會被彈出,把執行權還給以前的執行環境。函數

  • 3 此時函數的執行環境會被銷燬,裏面的變量對象和做用域鏈都會被銷燬學習

閉包的造成

定義:
閉包是指有權訪問另外一個函數做用域的變量和函數:

語言實現:
當一個函數中返回一個引用地址並賦值給一個變量時,就會成一個閉包,只有當變量的引用地址改變或者爲null時,js的垃圾回收機制不定時觸發是,閉包纔會被銷燬

優化

代碼實現:

function process (a){
    return function(b){
        return a+b
    }
}
const add =process(3)
add(2)// 5
add=null
複製代碼

缺點:
因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過 度使用閉包可能會致使內存佔用過多,咱們建議讀者只在絕對必要時再考慮使用閉 包。雖然像 V8 等優化後的 JavaScript 引擎會嘗試回收被閉包占用的內存,但請你們 仍是要慎重使用閉包

閉包與變量

經典for中訪問i的問題,看代碼
function createFn(){
    var result =[];
    for(var i=0;i<10;i++){
        result[i]=function(){  console.log(i)}
     }
    return result; 
}
var res=createFn()
res[0]()
複製代碼

這個問題你們應該都碰見過,打印的是0仍是10呢,正確答案是10 爲何是10呢,我們看看"res【0】()"的做用鏈裏面包含了幾個 變量對象 答案是3個

  • 做用域鏈的最前端是 當前匿名數的變量對象
  • 第二個是 createFn的變量對象 裏面包含i 是最後一次賦值的i,這時的i已是10了
  • 第三個是 window的變量對象

我們看看,上面的代碼的執行順序

  • 當createFn()執行時

    • 會複製該函數的[[Scopes]],做爲做用域鏈,
    • 接着創造 該函數的變量對象並把定義在函數內的i也放在 變量對象 上面
    • 把該變量對象推入到當前做用域鏈的最前端
    • 代碼沒for循環沒執行一次, 變量對象i的值就會改變一次,for執行完畢的時候此時的i已是10了
  • 當執行放在數組中的某個匿名函數的時候

    • 會複製該函數的[[Scopes]],做爲做用域鏈,

    • 接着創造 該函數的變量對象並把定義在函數內變量和函數放在 變量對象,

    • 把該變量對象推入到當前做用域鏈的最前端

    • 當代碼運行到 console.log(i) 會在當前的做用域的最前端開始查找i

      • 當前做用域鏈的最前端沒有i,去查找下一個做用域
      • 這是發現了1,返回i中止做用域鏈的查找。此時的i已是10

解決方案,我們能夠考慮一下,每次循環的i都放在一個變量對象上面,接下來在建立匿名函數的時候把當前的做用域鏈copy一份放在每一個匿名函數的[[Scopes]],在每一個匿名函數運行的時候,經過做用域鏈查找取得每次寫入到變量對象的i的值

function createFn(){
    var result =[];
    for(var i=0;i<10;i++){
        result[i]=function(num){  
        return function(){ 
        debugger;
        console.log(num)
        }
        }(i)
     }
    return result; 
}
var res=createFn()
res[0]()
複製代碼

看圖

接着看下圖就會更明白

若是有不足的地方,請你們指出來,我們一塊兒交流,多謝 小夥伴 「Tim在掘金lv-1」 提出這個 for中閉包去i的經典案例

相關文章
相關標籤/搜索