33 個 js 核心概念(一):函數調用棧,執行上下文與變量對象

前言

爲何會有這篇文章?

在書籍或博客上,咱們常常會看到「做用域鏈」、「閉包」、「變量提高」等概念,說明一個問題 —— 它們很重要。git

但不少時候,對於這些概念,看的時候以爲本身已經明白了,可過不了多久,再讓你說一說,可能就說不清楚了,之因此會這樣,是由於咱們對於 JavaScript 這門語言的運行機制不清楚。github

我相信搞明白了今天所講的內容,會對你理解那些知識大有裨益!數組

函數調用棧(call stack)

1. 什麼是棧?

相似 js 中的數組,棧也是用來存儲數據的一種數據結構。他的特色是後進先出(LIFO)瀏覽器

與之相對的一種數據結構稱爲隊列,隊列的特色是先進先出(FIFO)數據結構

能夠想象這樣一種場景:小明和同窗們放學回家,老師讓他在排在隊伍的最前面,他們天天回家路上都要通過一個衚衕,小明每次都是第一個進入衚衕,確定也是第一個出來,這就是所謂「先進先出」。閉包

但是有一天,小明他們走到衚衕裏發現衚衕口停了一輛車,把衚衕給堵死了,沒辦法,他們只能隊頭變隊尾往回撤,這時候,小明雖然最早進入衚衕,卻只能最後出去,最早出去的是排在隊尾的小華,也就是「後進先出」。函數

2. 什麼叫函數調用棧?

在 js 中函數的調用也遵守這樣以一個原則:最早調用的函數先放到調用棧中,假如這個函數內部又調用了別的函數,那麼這個內部函數就接着被放入調用棧中,直至再也不有函數調用。最早執行完畢的必定是最裏面的函數,執行事後彈出調用棧,接着執行上一層函數,直至全部函數執行完,調用棧清空。this

這樣說可能會不太明白,舉個例子:spa

// 其餘語句
function first() {
 console.log('first')
 function second() {
     console.log("second")
 }
 second();
 third();
 // 其餘語句
}
//其餘語句
function third() {
    console.log("third")
}
// 調用 first
first();

在上述代碼中,首先調用的是函數 first, 此時 first 進入函數棧,接着在 first 中調用函數 second,second 入棧,當 second 執行完畢後,second 出棧,third 入棧,接着 third 執行完出棧,執行 first 其餘代碼,直至 first 執行完,函數棧清空。3d

函數調用棧

執行上下文(Execution Context)

1. 什麼是執行上下文?

js 代碼在執行時,會進入一個執行環境,它會造成一個做用域。這個執行環境,即是執行上下文。

JavaScript 主要有三種執行環境:

  1. 全局執行環境: 代碼開始執行時首先進入的環境。
  2. 函數環境:函數調用時,會開始執行函數中的代碼。
  3. eval:不建議使用,可忽略。

2. 執行上下文的生命週期

上面講到 js 代碼執行時會生成一個執行上下文。而這個執行上下文的週期,分爲兩個階段:

  1. 建立階段。這個階段會生成變量對象(VO),創建做用域鏈以及肯定 this 的值。
  2. 執行階段。這個階段進行變量賦值,函數引用及執行代碼。

到這裏你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。
執行上下文的生命週期

3. 什麼是執行棧?

執行上下文也一樣遵循函數調用棧的規則,無非就是多加了一層 —— 全局執行上下文,函數執行完後會跳出執行棧,而全局執行上下文,會在關閉瀏覽器後跳出執行棧。

仍是上面的例子,咱們看一下執行棧。

執行上下文

變量對象

1. 什麼叫變量對象?

從上面其實能夠獲得答案,變量對象是 js 代碼在進入執行上下文時,js 引擎在內存中創建的一個對象,用來存放當前執行環境中的變量。

2. 變量對象(VO)的建立過程

變量對象的建立,是在執行上下文建立階段,依次通過如下三個過程:

  1. 建立 arguments 對象。對於函數執行環境,首先查詢是否有傳入的實參,若是有,則會將參數名是實參值組成的鍵值對放入arguments 對象中,不然,將參數名和 undefined,組成的鍵值對放入 arguments 對象中。

    function bar(a, b, c) {
        console.log(arguments);  // [2, 4]
        console.log(arguments[2]); // undefined
      }
      bar(2,4)
  2. 檢查當前環境中的函數聲明。當遇到同名的函數時,後面的會覆蓋前面的。

    console.log(a); // function a() {console.log('fjdsfs') }
    function a() {
        console.log('24');
    }
    function a() {
      console.log('fjdsfs')
    }

    在上面的例子中,在執行第一行代碼以前,函數聲明已經建立完成,後面的對以前的聲明進行了覆蓋。

  3. 檢查當前環境中的變量聲明並賦值爲undefined當遇到同名的函數聲明,爲了不函數被賦值爲 undefined ,會忽略此聲明

    console.log(a); // function a() {console.log('fjdsfs') }
    console.log(b); // undefined
    function a() {
      console.log('24');
    }
    function a() {
    console.log('fjdsfs');
    }
    var b = 'bbbbbbbb';
    var a = 46;

    在上例咱們能夠看到,在代碼以前前,a 仍舊是一個函數,而 b 是 undefined。

根據以上三個步驟,對於變量提高也就知道是怎麼回事了。

3. 變量對象變爲活動對象

執行上下文的第二個階段,稱爲執行階段,在此時,會進行變量賦值,函數引用並執行其餘代碼,此時,變量對象變爲活動對象

咱們仍是舉上面的例子:

console.log(a); // function a() {console.log('fjdsfs') }
   console.log(b); // undefined
   function a() {
       console.log('24');
   }
   function a() {
     console.log('fjdsfs');
   }
   var b = 'bbbb';
   console.log(b); // 'bbbb'
   var a = 46; 
   console.log(a);  // 46
   var  b = 'hahahah';
   console.log(b); // 'hahah'

在上面的代碼中,代碼真正開始執行是從第一行 console.log() 開始的,自這以前,執行上下文是這樣的:

// 建立過程
EC= {
  VO: {}; // 建立變量對象
  scopeChain: {}; // 做用域鏈
}
VO = {
  argument: {...}; // 當前爲全局上下文,因此這個屬性值是空的
  a: <a reference> // 函數 a  的引用地址
  b: undefiend  // 見上文建立變量對象的第三步
}

根據步驟,首先是 arguments 對象的建立;其次,是檢查函數的聲明,此時,函數 a 聲明瞭兩次,後一次將覆蓋前一次;最後,是檢查變量的聲明,先聲明瞭變量 b,將它賦值爲 undefined,接着遇到 a 的聲明,因爲 a 已經聲明爲了一個函數,因此,此條聲明將會被忽略。

到此,變量對象的建立階段完成,接下來時執行階段,咱們一步一步來。

  1. 執行 console.log(a),咱們知道,此時 a 是第二個函數,因此會輸出function a() {...}
  2. 執行 console.log(b),不出咱們所料,將會輸出 undefined
  3. 執行賦值操做: b = 'bbbb'
  4. 執行 console.log(b) ,此時,b 已經賦值,因此會輸出 'bbbb'
  5. 執行賦值操做: a = 46;
  6. 執行 console.log(a) ,此時,a 的值變爲 46。
  7. 執行賦值操做: b = 'hahahah'
  8. 執行 console.log(b), b 已經被從新賦值,輸出 hahahah

由上面咱們能夠看到,在執行階段,變量對象是跟着代碼不斷變化的,此時,咱們把變量對象成爲活動對象。

執行到最後一步時,執行上下文變成了這樣。

// 執行階段
EC = {
  VO = {};
  scopeChain: {};
}
 // VO ---- AO
AO = {
  argument: {...};
  a: 46;
  b: 'hahahah';
  this: window;
}

以上,就是變量對象在代碼執行前及執行時的變化。

剛開始就說過,這部分概念將會對你理解後面的知識有很大的幫助,因此剛開始接觸的話可能會有些晦澀,建議就是認真讀兩遍,結合後面的知識,常常回過頭來看看。

最後留一道題,給你們做爲練手,觀察觀察執行上下文及變量對象的變化。

console.log(a);
console.log(b);
var a = 4;
function a() {
  console.log('我是a1');
  b(3, 5);
}
var a = function a() {
  console.log('我是a2');
  b(3, 5);
}
var b = function (m, n) {
   console.log(arguments);
   console.log('b')
}
a();
原文地址: 阿木木的博客
相關文章
相關標籤/搜索