javascript深刻理解-從做用域鏈理解閉包

1、概要

紅寶書(P178)對於閉包的定義:閉包就是有權訪問另一個函數做用域中變量的函數。html

MDN,對於閉包的定義:閉包就是指可以訪問自由變量的函數。git

那麼什麼是自由變量?自由變量就是在函數中使用,但既不是函數參數arguments,也不是函數的局部變量的變量,就是說另一個函數做用域中的變量。github

閉包組成?閉包 = 函數 + 函數可以訪問的變量面試

文章首發地址於sau交流學習社區:https://www.mwcxs.top/page/57...數組

2、分析

舉個栗子:閉包

var a = 1;

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

foo();

foo函數能夠訪問到變量a,可是a並非foo函數的局部變量,也不是foo函數的參數,因此a就是自由變量,那麼函數foo+foo函數能夠訪問自由變量a不就是構成了一個閉包嘛。函數

咱們再來看一個栗子:oop

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

其中date不是函數getDate的參數,也不是局部變量,因此date是自由變量。學習

總結起來就是兩點:this

一、是一個函數(好比:內部函數從父函數中返回)

二、可以訪問上級函數做用域中的變量(哪怕上級函數的上下文已經銷燬)

而後咱們再來看一個栗子(來自《JavaScript權威指南》)來分析:

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

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

這時候須要咱們來分析一下這段代碼中執行上下文棧和執行上下文的變化狀況。

簡要的分析一下執行過程:

一、進入全局代碼,建立全局執行上下文,全局執行上下文壓入執行上下文棧;

二、全局執行上下文初始化;

三、執行checkscope函數,建立sheckscope函數執行上下文,checkscope執行上下文被壓入執行上下文棧;

四、checkscope執行上下文初始化,建立變量對象,做用域鏈,this等;

五、checkscope函數執行完畢,checkscope執行上下文從執行上下文棧中彈出;

六、執行f函數,建立f函數執行上下文,f執行上下文壓入執行上下文棧;

七、f執行上下文初始化,建立變量對象,做用域鏈,this等;

八、f函數執行完畢,f函數上下文從執行上下文棧中彈出

image

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

答:函數f執行上下文維護了一盒做用域鏈,做用域鏈會指向checkscope做用域,做用域鏈是一個數組,結構以下:

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

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

注:AO 表示活動對象,儲存了函數的參數、函數內聲明的變量等

3、概念

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

湯姆大叔在關於閉包對的文章的定義。ECMAScript中,閉包指的是:

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

二、從實踐角度:如下函數纔算閉包:

(1)即便建立它的上下文已經摧毀了,它依然存在(好比,內部函數中返回)

(2)在代碼中引用了自由變量

4、面試必刷的題

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] 是同樣的。

注:

一、for 循環不會建立一個執行上下文,全部不會有 AO, i 的值是在全局對象的 AO 中,代碼初始的時候爲:

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

二、data[0] 是一個函數名,data[0]() 表示執行這個函數,當執行函數的時候,循環已經走完了;

三、函數可以讀取到的值跟函數定義的位置有關,跟執行的位置無關。

解決辦法:

改爲閉包,方法就是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
    }
}

data[0]Context 的 AO 並無 i 值,因此會沿着做用域鏈從匿名函數 Context.AO 中查找,這時候就會找 i 爲 0,找到了就不會往 globalContext.VO 中查找了,即便 globalContext.VO 也有 i 的值(值爲3),因此打印的結果就是0

5、思考題

把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]();

或者這樣:

var data = [];

var _loop = function _loop(i) {
    data[i] = function () {
        console.log(i);
    };
};

for (var i = 0; i < 3; i++) {
    _loop(i);
}

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

6、參考

https://github.com/mqyqingfen...

相關文章
相關標籤/搜索