javascript變數物件函式呼叫棧作用域閉包等細解!

雲棲直播~發表於2018-06-30

說明

下面程式碼演示基於window系統chrome瀏覽器環境,版本號為63.0.3239.132,32位!相關結果可能會有一點出入,請也實際為準!

相關程式碼除錯的過程中檢視結果的步驟:

  • 開啟瀏覽器控制檯,切換到sources板塊,並選擇相應的原始檔;

  • 在對應的原始檔程式碼左邊的行號上打上斷點;

  • 然後重新整理瀏覽器,瀏覽器會在對應打斷點的程式碼出停止執行,此時我們根據需要按f11鍵一步一步的執行程式碼,並同時檢視程式碼的呼叫棧,作用域等情況,主要檢視source板塊下最右邊子板塊的Call Stack和Scope項。

相關概念梳理

其實從我自身出發,我覺的如果需要更好地理解變數物件的話,那麼需要對以下概念有一個比較基本的理解會更方便些!

  • 函式呼叫棧

為了理解函式呼叫棧,我們先寫一段程式碼:

  function fn1(){
    console.log(`fn1`);
    fn2();
  };
  function fn2(){
    console.log(`fn2`);
  };
  fn1();

我們在fn2函式呼叫的地方打上斷點,然後重新整理瀏覽器,檢視Call Stack選項,會看到下面的結果:

  fn1
  (anonymous)

此時再按一次f11鍵,此時Call Stack顯示結果如下:

  fn2
  fn1
  (anonymous)

這裡說明一下,anonymous指代全域性匿名呼叫函式環境,而fn1和fn2分別指代fn1函式作用域和fn2作用域環境。

我們梳理一下瀏覽器的呼叫過程:1 js進入全域性匿名函式環境,把這個所謂的匿名函式推入呼叫棧;2 發現此時又呼叫了fn1,於是把fn1函式推入呼叫棧,此時fn1在anonymous的上面; 3 緊接著,發現呼叫了fn2,於是把fn2推入呼叫棧,於是得到了上面的結果。

如果後續我們繼續按f11鍵除錯,會發現Call Stack會依次出現先面的結果:

  fn2
  fn1
  (anonymous)

  fn1
  (anonymous)

  (anonymous)

也就是說最後只剩下了全域性匿名函式環境,這裡強調一下,anonymous環境將會伴隨著程式執行一直存在,除非你關閉了瀏覽器。

於是我們總結得到這樣的結果:存在這樣一個呼叫棧,預設推入一個全域性匿名函式在棧底,當此時再呼叫其它全域性函式的時候,會把該函式推入棧,並在anonymous上面,如果該函式內部繼續呼叫了其它函式,那麼同樣道理,會把其它函式推入棧,放在該函式上面,最後當函式在呼叫完成的過程中,會依次退出該呼叫棧,退出的過程中會把許可權交給上一層函式,最後又迴歸了只剩下全域性匿名函式環境。於是我們說了這麼多,其實這就是函式呼叫棧!

函式呼叫棧你可以理解為函式呼叫前後包含關係:先進後出,後進先出!它描述了程式碼執行的先後順序以及當前程式碼執行控制許可權的擁有者關係等。

  • 函式作用域

我們都知道javascript是沒有塊級作用域的,只有函式作用域,怎麼理解?我們看下面的程式碼:

程式碼1

for(var k = 0;k<10;k++){
  //...
};
console.log(k);//輸出10

我們發現for迴圈程式碼塊執行完了之後,依然能得到k的值。再看下面的程式碼:

程式碼2

function fn3(){
  var a = `a`;
};
fn3();
console.log(a);//報錯 a is not defined

我們發現在函式裡面定義的變數a,在函式外面是拿不到的,這就是函式作用域能做到的。

函式作用域能讓我們定義一些函式內部使用的與外部環境同名的變數而不會跟外部環境衝突,我們用的較多的地方就是即時函式,如下:

var a = `outer`;
(function(){
  var a = `inner`;
  conosle.log(a);//輸出inner
})();
console.log(a);//輸出outer

當然了在es6及後續版本javascript中,我們可以使用let和const標誌符來定義塊級變數,這裡不作討論!

這裡再做一下擴充套件,作用域中涉及到最多的一個概念就是作用域鏈,這個是什麼意思呢!看下面的程式碼:

let a = `a`;
function fn4(){
  let b = `b`;
  return a+b;
};
let c = fn4();
console.log(c);//輸出`ab`

作用域鏈描述的是一種函式在執行的過程中查詢變數的方式,具體來說:函式執行,如果遇到某變數,會首先在自身作用域環境查詢改變數,如果不存在,會向上一層作用域查詢變數,也就是函式呼叫棧中當前執行函式的下一層函式環境查詢變數,依次類推,直到到全域性環境中查詢,如果在全域性中都沒有找到變數的話,那麼就會報錯!

其實原型鏈也是一種描述例項屬性查詢的過程,跟作用域鏈類似!

  • 閉包

恐怕接觸過javascript的開發者,聽到最多與其有關的概念就是閉包了。而且網上有很多文章和書籍都對閉包進行了說明,其實我覺的理解閉包並不難哈!我們來看段程式碼吧:

function fn5(){
  let a = 0;
  return function(){
    a++;
    console.log(a);
  };
};
let fn = fn5();
fn();

我們在fn呼叫的地方打一個斷點,隨後按一次f11鍵進入fn執行環境,看下Scope項結果,會發現Scope下有一項子項:

Closure(fn5)
|_ a

我可以明確的告訴你,此時的fn5對於fn來說就是一個閉包(你可以理解是一個環境概念,該環境維護了一些內部返回的匿名函式用到的一些外部變數)!

梳理一下,閉包指得是某個函式呼叫之後,自身執行環境已不復存在,但返回了一個函式,由於該返回函式內部用到了外部函式裡面的一些變數,並又該內部函式又賦值給了其它變數,導致雖然外部函式不存在了,但是它引用的那些外部變數卻不能回收的一個環境(閉包)。

閉包用好了,可以保證好多變更的作用域週期得以提升,減少變數命名衝突,但是過多的使用閉包,也會存在記憶體洩漏的問題,因為你的很多變數都沒有被垃圾回收器回收。

進入正題,變數物件

為了讓你對變數物件整體有個最初的概念。在詳細介紹之前,我對變數物件概念作如此表述:變數物件指函式執行過程中,函式自身用到資料從哪裡來的,函式怎麼管理這些資料的等等,其實變數物件裡面儲存了函式在執行過程中所有用到的資料以及某個時刻值等!

時間倉促,後續待更新……

原文釋出時間為:2018年06月21日
原文作者:掘金

本文來源: 掘金 如需轉載請聯絡原作者


相關文章