變量對象與做用域鏈

一般,咱們在函數內部使用函數外部的變量時會很天然,並無想過爲何可以直接使用函數外部的變量而在函數外部卻不能直接使用函數內部的變量,一切都顯得理所固然。佛曰,凡事必有因,這個因就是做用域鏈,在瞭解做用域鏈如何起做用前咱們應該知道與其息息相關的做用域和變量對象。前端

做用域(Scope)

當咱們的代碼在執行時,對於特定的代碼,咱們應該去哪查找變量?咱們又該如何查找這些變量?答案就是做用域,也就是說做用域肯定了如何在某些位置存儲變量以及如何在稍後查找這些變量。在 JS 中採用詞法做用域(Lexical Scope),也就是靜態做用域。數組

靜態做用域bash

對於靜態做用域,函數的做用域與書寫代碼的位置直接相關,也就說做用域在在函數定義時就肯定了。那是如何肯定的呢?就是詞法的嵌套結構: 在函數內部的環境引用函數外部的環境,函數外部的環境也能夠引用它外部的環境,如此,直到全局環境。像下面的洋蔥圈,全局環境是洋蔥的最外層,裏面的每一層(函數環境)都嵌套其中。函數

做用域示意圖

來看下面的例子:post

var name = 'lily';

  function getName() {
    console.log('name: ', name);
  }

  function getMyName() {
      var name = 'lucy';
      getName();
  }

  getMyName();
複製代碼

輸出的結果是什麼呢?對於靜態做用域,函數 getName 執行時,先在函數內部查找變量,若是沒有,則在上層查找,顯然它的上層環境在定義時就已經肯定,爲全局環境,所以找到了 lily,輸出name: lilythis

動態做用域spa

與靜態做用域相對的就是動態做用域,函數的做用域在函數調用時才肯定。 假如採用動態做用域(如 bash),當函數 getMyName 執行時,變量 name 的值已經變成了 lucy,所以調用 getName 函數時輸出name: lucy。以下,執行 bash ./test.sh 就會輸出name: lucy3d

#test.sh
name='lily';

function getName() {
  echo name: $name;
}

function getMyName() {
  local name='lucy';
  getName;
}

getMyName;
複製代碼

前面咱們說到做用域肯定了如何在某些位置存儲變量,這個位置就是變量對象,而如何查找這些變量,就須要做用域鏈。code

變量對象(Variable Object, VO)

咱們知道變量對象決定了變量的儲存,所以咱們須要瞭解變量對象是如何建立的。過程大體以下:cdn

  • 建立 arguments 對象,檢查當前環境的參數,初始化屬性和屬性值。
  • 檢查函數聲明,當前環境中每發現一個函數就在 VO 中用函數名建立一個屬性,以此來引用函數。若是函數名存在,就覆蓋這 個屬性。
  • 檢查變量,當前環境中每發現一個變量就在 VO 中用變量名建立一個屬性,並初始化其值爲 undefined。若是變量名存在, 則不進行任何處理(注意這是在建立階段,執行階段會被賦值),繼續檢查。

進入代碼執行階段,函數環境的變量對象會變成活動對象 AO(Active Object),變成活動對象前,其內部屬性不能被訪問。對於全局環境,其變量對象就是 window 對象自身,能夠直接訪問其內部屬性。須要注意的是在 ES5 中變量對象和活動對象的概念被融合到了詞法環境(lexical environments)模型(環境記錄: Environment Record 和對外部環境的引用: outer reference)中,ES5 後到如今的 ES8 還有一些新的概念(Realms 領域,做業 Job 等)被提出。 來看下面的例子:

function calcArea(r) {
  var width = 20;
  var squareArea = function squareArea() {
    return width * width;
  };

  function circleArea() {
    return 3.14 * r * r;
  };

  return circleArea() + squareArea();
}

calcArea(10);
複製代碼

當調用 calcArea(10)時建立階段執行環境的快照以下:

calcAreaExecutionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 10,
      length: 1
    },
    r: 10,
    width: undefined,
    squareArea: undefined,
    circleArea: pointer to function circleArea()
  },
  this: { ... }
}
複製代碼

能夠看到在建立階段,只處理定義變量的名字,不爲變量賦值,一旦建立完成進入執行階段就會爲變量賦值。建立階段執行環境的快照以下:

calcAreaExecutionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 10,
      length: 1
    },
    r: 10,
    width: 20,
    squareArea: pointer to function squareArea(),
    circleArea: pointer to function circleArea()
  },
  this: { ... }
}
複製代碼

由此變量提高就比較容易理解了,來看以下例子

console.log(hello); // [Function: hello]
function hello() { console.log('how are u') }
var hello = 10;
複製代碼

能夠看到打印輸出的值爲[Function: hello],爲何能在變量聲明前使用呢?咱們來看上述代碼的執行流程

  • 首先進入全局環境建立階段,檢查函數聲明,將函數 hello 放入變量對象(全局環境爲 window 對象)。
  • 檢查變量聲明,發現變量 hello 已經存在,則跳過。
  • 進入執行階段,執行代碼console.log(hello)時,會在全局環境的變量對象中尋找 hello,找到了函數 hello。

執行階段執行環境快照以下:

globalExecutionContext = {
  scopeChain: { ... },
  VO: window,
  this: window
}
複製代碼

做用域鏈

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈,它是由當前環境與上層環境的一系列變量對象組成的,保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。 在執行環境中查找變量,若是這個變量不是局部變量(包括局部函數或形參),這個變量就稱爲自由變量,要找到自由變量就須要做用域鏈。

來看下面的例子:

var firstName = 'Michael';
function getName() {
  var middleName = 'Jeffrey';
  function fullName() {
    var lastName = 'Jordan';
    return firstName + middleName + lastName;
  }
  return fullName();
}

getName();
複製代碼

上面的代碼會建立三個執行環境,全局環境、函數 getName 局部環境以及函數 fullName 局部環境,它們的變量對象分別爲 VO(global)、VO(getName)以及 VO(fullName)。函數 fullName 的做用域鏈是如何與這些變量對象關聯起來的呢?步驟以下:

  • 函數 fullName 建立時,保存做用域鏈到內置屬性[[scope]]

    fullName 在解析時(getName 調用時)根據靜態做用域能夠知道它的做用域鏈包括 VO(global)和 AO(getName)(getName 執行 VO(getName)變爲 AO(getName))。

    fullName.[[scope]] = [
        AO(getName),
        VO(global)
      ];
    複製代碼
  • 函數 fullName 激活(調用執行)時,建立執行上下文壓入棧頂

    ECStack = [
        fullName EC,
        getName EC,
        globalContext
      ]
    複製代碼
  • fullName 函數並不當即執行,開始準備工做,首先複製[[scope]]屬性建立做用域鏈

    fullNameEC = {
      scopeChain: fullName.[[scope]], // 建立做用域鏈
    }
    複製代碼
  • 用 arguments 建立活動對象,並加入形參、變量聲明、函數聲明

    fullNameExecutionContext = {
        scopeChain: [AO(getName), VO(global)],
        activeObject: {
          arguments: {
            length: 0
          },
          lastName: undefined,
        },
        this: { ... }
      }
    複製代碼
  • 將活動對象推入 fullName 做用域鏈的前端

    fullNameEC = {
      AO: {
        arguments: {
          length: 0
        },
        lastName: undefined,
      }
      scopeChain: [AO(fullName), AO(getName), VO(global)], // 做用域鏈
    }
    複製代碼
  • 準備工做完成,開始執行函數,爲 AO(fullName)對象的變量賦值

    fullNameEC = {
      AO: {
        arguments: {
          length: 0
        },
        lastName: 'Jordan',
      }
      scopeChain: [AO(fullName), AO(getName), VO(global)], // 做用域鏈
    }
    複製代碼
  • fullName 執行完畢,其上下文彈出執行棧

    ECStack = [
        getName EC,
        globalContext
      ]
    複製代碼

這裏,咱們用一個數組來表示做用域鏈,數組的第一個元素即鏈條的最前端爲當前執行環境的活動對象,數組的最後一個元素即鏈條的最末端爲全局執行環境的變量對象。當前執行環境在執行階段訪問變量會先從做用域鏈的最前端開始查找變量,若是沒有則在包含環境中查找,若是包含環境中沒有則繼續向上查找,如此,直到全局環境中的變量對象,返過來並不成立,也就是說在全局做用域並不能訪問函數內部的變量。

做用域鏈示意圖

延長做用域鏈

在 js 中,某些語句能夠在做用域鏈前端臨時添加一個變量對象,該變量對象會在代碼執行完畢後移除。具體來講當執行流進入到下列兩種語句時,做用域就會獲得加長:

  • try-catch 語句的 catch 塊

    在執行 catch 語句塊時,建立一個包含拋出錯誤對象聲明的變量對象,將其加入做用域鏈前端。 以下,在 catch 塊中,錯誤對象 e 被添加到了其做用域鏈前端,這使得在 catch 塊內部可以訪問到錯誤對象。執行完後,catch 塊內部的變量對象被銷燬,所以在 catch 塊外部就不能訪問到錯誤對象 e 了(ie8 能夠訪問到,ie9 修復了這個問題)。

    var test = () => {
      try {
        throw Error("出錯誤了");
      } catch(e) {
        console.log(e);  //Error: 出錯誤了
      }
      console.log(e);  //Uncaught ReferenceError: e is not defined
    }
    test();
    複製代碼
  • with(obj)語句

    將 obj 對象加入到做用域鏈前端。 以下,語句with(persion)將對象 persion 添加到了函數 getName 做用域鏈的前端,語句var myName = name在查找變量 name 時 會首先在其做用域鏈前端,即 person 對象中查找,查找到 name 屬性爲 snow。又由於 with 語句的變量對象是隻讀的,在本層定義的變量,不能存儲到本層,而是存儲到它的上一層做用域。這樣在函數 getName 的做用域內就能訪問到變量 myName 了。

    var persion = { name: 'snow' };
    var name = 'summer';
    var getName = () => {
      with(persion) {
        var myName = name;
      }
      return myName;
    }
    console.log(getName())
    => snow
    複製代碼

結論

  • 全局環境沒有 arguments 對象。
  • 咱們編寫代碼時並不能訪問函數的變量對象,但解釋器在處理數據使其成爲活動對象時就可使用它。
  • 做用域鏈的搜索始終是從做用域鏈的前端開始,而後逐級的向後回溯,直到全局環境,不能反向搜索。
  • 各個環境間的聯繫是線性的,有次序的。
相關文章
相關標籤/搜索