仍是不明白JavaScript - 執行環境、做用域、做用域鏈、閉包嗎?

JavaScript中的執行環境、做用域、做用域鏈、閉包一直是一個很是有意思的話題,不少博主和大神都分享過相關的文章。這些知識點不只比較抽象,不易理解,更重要的是與這些知識點相關的問題在面試中高頻出現。以前我也看過很多文章,依舊是似懂非懂,模模糊糊。最近,仔細捋了捋相關問題的思路,對這些問題的理解清晰深刻了很多,在這裏和你們分享。

本文已同步至個人我的主頁。歡迎訪問查看更多內容!若有錯誤或遺漏,歡迎隨時指正探討!謝謝你們的關注與支持!前端

這篇文章,我會按照執行環境、做用域、做用域鏈、閉包的順序,結合着JS中函數的運行機制來梳理相關知識。由於這樣的順序恰好也是這些知識點相互關聯且遞進的順序,同時這些知識點都又與函數有着千絲萬縷的聯繫。這樣講解,會更容易讓你們完全理解,至少我就是這樣理解清晰的。git

廢話再也不多說,咱們開始。github

執行環境

首先,咱們仍是要理解一下什麼是執行環境,這也是理清後面問題的基礎。web

執行環境是JavaScript中最爲重要的一個概念。執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。——《JavaScript高級程序設計》

抽象!不理解!不要緊,我來解釋:其實,執行環境就是JS中提出的一個概念,它是爲了保證代碼合理運行採用的一種機制。面試

一種概念...機制...更抽象,那它究竟是什麼?實際上,執行環境在JS機制內部就是用一個對象來表示的,稱做執行環境對象,簡稱環境對象數組

那麼,這個執行環境對象到底又是什麼時候、怎麼產生的呢?有如下兩種狀況:瀏覽器

  1. 在頁面中的腳本開始執行時,就會產生一個「全局執行環境」。它是最外圍(範圍最大,或者說層級最高)的一個執行環境,對應着一個全局環境對象。在Web瀏覽器中,這個對象就是Window對象。
  2. 當一個函數被調用的時候,也會建立一個屬於該函數的執行環境,稱做「局部執行環境」(或者稱做函數執行環境),它也對應着本身的環境對象。

所以,執行環境就分爲全局執行環境局部執行環境兩種,每一個執行環境都有一個屬於本身的環境對象。閉包

既然執行環境是使用一個對象表示的,那麼對象就有屬性。咱們來看看環境對象的三個有意思的屬性。變量對象[[scope]]this函數

環境對象

環境對象中的變量對象

《JS高程》中明確說明,執行環境定義了變量或函數有權訪問的其餘數據。那麼這些數據到底被放(存儲)在哪裏呢?性能

其實,每一個執行環境都有一個與之關聯的變量對象,在環境中定義的全部變量和函數都保存在這個對象中。咱們在代碼沒法訪問這個對象,但解析器在處理數據時會在內部使用它。

通俗地說就是:一個執行環境中的全部變量和函數都保存在它對應的環境對象的變量對象(屬性)中。

認識[[scope]]前先理解做用域

在講[[scope]]前,咱們就須要先弄清楚什麼是做用域了。由於做用域與[[scope]]之間存在着很是緊密的關係。

《JS高程》中沒有明確給出做用域的定義和描述。其實,做用域就是變量或者函數能夠被訪問的代碼範圍,或者說做用域就是變量和函數所起做用的範圍。

這樣看來做用域也像是一個概念,它是用來描述一個區域(或者說範圍)的。在JS中,做用域分爲全局做用域局部做用域兩種。

咱們來看看這兩種做用域的具體描述:

在頁面中的腳本開始執行時,就會產生一個「全局做用域」。它是最外圍(範圍最大,或者說層級最高)的一個做用域。全局做用域的變量、函數
能夠在代碼的任何地方訪問到。
當一個函數被建立的時候,會建立一個「局部做用域」。局部做用域中的函數、變量只能在某些局部代碼中能夠訪問到。

看一個例子:

var g = 'Global';

function outer() {
  var out = 'outer';

  function inner() {
    var inn = 'inner';
  }
}

上面這個例子,產生的做用域就以下圖所示:

做用域

請注意上面這兩段話!!!是否是以爲很熟悉,似曾相識?!沒錯,這兩段話和介紹全局/局部執行環境(全局/局部環境對象)時候的描述幾乎一摸同樣!做用域是否是和環境對象有着千絲萬縷的聯繫呢?與此同時,咱們再仔細回憶一下:一、做用域就是變量或者函數能夠被訪問的代碼範圍。二、一個執行環境中定義的全部變量和函數都保存在它對應的環境對象中。

結合上面所述,其實不可貴出:儘管做用域的描述更像是一個概念,但若是必定要將它具象化,問它究竟是什麼東西,與執行環境有什麼關係?其實,做用域所對應的(不是相等、等於)是環境對象中的變量對象。

明白了這些,咱們就能夠來看看環境對象中的[[scope]]屬性。

環境對象中的[[scope]]

首先,要明確的是,環境對象中的[[scope]]屬性值是一個指針,它指向該執行環境的做用域鏈。

到底什麼是做用域鏈呢?做用域鏈本質上就是一個有序的列表,而列表中的每一項都是一個指向不一樣環境對象中的變量對象的指針

那麼,這個做用域鏈究竟是怎麼造成的呢?它裏面指向變量對象的指針的順序又是如何規定的呢?咱們用下面這個簡單的例子說明。

var g = 'Hello';

function inner() {
  var inn = 'Inner';
  var res = g + inn;
  return res;
}

inner();

當執行了inner();這一行代碼後,代碼執行流進入inner函數內部,此時,JS內部會先建立inner函數的局部執行環境,而後建立該環境的做用域鏈。這個做用域鏈的最前端,就是inner執行環境本身的環境對象中的變量對象,做用域鏈第二項,就是全局環境的環境對象中的變量對象。這條做用域鏈以下圖所示:

做用域鏈

造成了這樣的做用域鏈以後,就能夠有秩序地訪問一個變量了。以這個例子爲例:當執行inner();進入函數體內後,執行g + inn;一行,須要訪問變量g、inn,此時JS內部機制就會沿着這條做用域鏈查找所需變量。在當前inner函數的做用域中找到了變量inn,值爲'Inner',查找終止。可是卻沒有找到變量g,因而沿着做用域鏈向上查找,進入全局做用域,在全局變量對象中找到了變量g,值爲'Hello',查找終止。計算得出res'HelloInner',並在最後返回結果。

與上面所講機制徹底相同,若是是多層執行環境嵌套,則做用域鏈是這麼造成的:

當代碼執行進入一個執行環境時,JS內部會開始建立該環境的做用域鏈。做用域鏈的 最前端,始終都是 當前執行環境的執行環境對象中的變量對象。若是這個環境是 局部執行環境(函數執行環境),則將其 活動對象做爲 變量對象。做用域鏈中的下一個是來自 外層環境對象的變量對象,而再下一個則是來自 再外層環境對象的變量對象...... 這樣 一直延續到全局環境對象的變量對象。因此,全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。

講到這裏,可能你已經對執行環境、執行環境對象、變量對象、做用域、做用域鏈的理解已經他們之間的關係有了一個較清晰的認識。也有可能,對這麼多的抽象問題仍是有些懵懵懂懂。不要緊,咱們用下面這一張圖,將上面的全部內容串聯起來,來直觀感覺和理解他們。

var g = 'Global';
function outer() {
  var out = 'outer';
  function inner() {
    var inn = 'inner';
  }
  inner();
}
outer();

環境、環境對象、變量對象、做用域、做用域鏈

對於這張圖,有一些須要注意的地方:

  1. 當函數調用時,纔會建立函數的執行環境和它的環境對象,再建立函數的活動對象,再建立函數環境的做用域鏈。
  2. 上圖中間一列變量對象中,outerinner的變量對象實際上是該函數的活動對象。全局環境是沒有活動對象的,只有在函數環境中,纔會使用函數的活動對象來做爲它的變量對象
  3. 函數的活動對象,是在函數建立時使用函數內置的arguments類數組和其餘命名參數來初始化的。因此實際上,函數的變量對象中應該還包含一個指向arguments類數組的指針

有了對做用域、做用域鏈的理解,最後,咱們來講一說閉包。

閉包

什麼是閉包

閉包就是有權訪問另外一個函數做用域中的變量的函數。——《JavaScript高級程序設計》

對於閉包,最簡單的大白話能夠這麼理解:

外部函數聲明內部函數,內部函數引用外部函數的局部變量,這些變量不會被釋放!——這是我曾經看到的別人的說法

或者這麼理解:

當在一個函數中返回另外一個函數的時候(是返回一個函數,不是返回函數的調用或者函數的執行結果),就會造成閉包,被返回的這個函數就叫作閉包函數。——這是我本身的理解

上面兩句話看似不一樣,其實本質是同樣的。來看一個最簡單的閉包的例子:

function sum() {
  var num1 = 100;

  // 這裏返回的是函數(體),不是函數的調用
  return function(num2) {
    return num1 + num2;
  }
}

// 此時result指向sum返回的那個匿名函數
// 注意!此時該匿名函數並無被執行
let result = sum();

result(200);

那麼,上面幾行代碼,爲何就會造成閉包呢?咱們來分析一下,代碼執行中JS內部到底作了什麼?

首先,有一點必須明確,就是通常狀況下,一個函數執行完內部的代碼,函數調用時所建立的執行環境、環境對象(包括變量對象、[[scope]]等)都會被銷燬,它們的生命週期就只有函數調用到函數執行結束這一段時間

可是上面的例子,就會出現例外。

當執行sum()時,調用該函數,建立它的環境對象,其中做用域鏈中第一項是本身環境的變量對象,第二項是全局環境的變量對象。當建立匿名函數的時候,也會建立匿名函數的環境對象,其中做用域鏈第一項是本身環境的變量對象,第二項是sum環境的變量對象,第三項是全局變量對象。

這時,問題就來了。按說,當函數sum執行完return以後,他的執行環境、變量對象、做用域鏈都會被銷燬。但是這時候卻不能銷燬他的變量對象,由於返回的匿名函數(此時由result指向該函數)並無執行,這個匿名函數的做用域鏈中還引用着sum函數的變量對象。換句話說,即便,sum執行完了,其執行環境的做用域鏈會被銷燬,可是它的變量對象還會保存在內存中,咱們在sum函數外部,還能訪問到它內部的變量num1num2,這就是造成閉包的真正緣由。可是,當result()執行完後,這些變量對象、做用域鏈就會被銷燬。

閉包存在的問題

由於閉包造成後,會在函數執行完仍將他的變量對象保存在內存中,當引用時間過長或者引用對象不少的時候,會佔用大量內存,嚴重影響性能。

來看下面的例子:(這個例子曾經是Tencent微衆銀行的筆試原題,出如今《JS高程》的7.2.3章節——P184)

function assignHandler() {
  var element = document.getElementById("someElement");
  element.onclick = function(){
    alert(element.id);
  };
}

assignHandler函數中定義的匿名函數是做爲element元素的事件處理函數的,且內部使用了element元素(訪問元素的id`),所以assignHandler函數執行完,對於element的引用也會一直存在,element元素會一直保存在內存中。

將上面的例子改爲下面這樣,就能解決這個問題。

function assignHandler(){
  var element = document.getElementById("someElement");

  // 這裏獲取element的id,爲其建立一個副本
  // 這樣是爲了在下面事件處理函數中解除對element元素的引用
  var id = element.id;

  element.onclick = function(){
    alert(id);
  };

  // 將element置爲null,斷開對element元素的引用
  // 這樣方便垃圾回收機制回收element所佔的內存
  element = null;
}
相關文章
相關標籤/搜索