進擊的 JavaScript(三) 之 函數執行過程

理解js 的執行過程是很重要的,好比,做用域,做用域鏈,變量提高,閉包啊,要想明白這些,你就得搞懂函數執行時到底發生了什麼!

1、執行環境(Execution Context)又稱執行上下文

當代碼執行時都會產生一個執行環境。JavaScript中的執行環境能夠分爲三種。前端

  1. 全局環境:在瀏覽器中,全局環境被認爲是window對象,所以,全部的全局變量和函數都做爲window對象的 屬性 和 方法 建立的。
  2. 函數環境:當一個函數執行時,就會建立該函數的執行環境,在其中執行代碼。
  3. eval(不建議使用,可忽略)

函數內,沒有使用var 聲明的變量,在非嚴格模式下爲window的屬性,即全局變量。

java

2、函數調用棧(call stack)

js 是根據函數的調用(執行) 來決定 執行順序的。每當一個函數被調用時,js 會爲其建立執行環境,js引擎就會把這個執行環境 放入一個棧中 來處理。瀏覽器

這個棧,咱們稱之爲函數調用棧(call stack)。棧底永遠都是全局環境,而棧頂就是當前正在執行函數的環境。當棧頂的執行環境 執行完以後,就會出棧,並把執行權交給以前的執行環境。閉包

看栗子說話:函數

function A(){
   console.log("this is A");
   function B(){
       console.log("this is B");
   }
   B();
}

A();

那麼這段代碼執行的狀況就是這樣了。this

  1. 首先 A() ;A 函數執行了,A執行環境入棧。
  2. A函數執行時,遇到了 B(),B 又執行了,B入棧。
  3. B中沒有可執行的函數了,B執行完 出棧。
  4. 繼續執行A, A中沒有可執行的函數了,A執行完 出棧。

函數調用棧

再來個不常規的:spa

function A(){
    
    function B(){
        console.log(say);
    }
 
    return B;
}

var C = A();

C();
  1. 首先 A() ;A 函數執行了,A執行環境入棧。
  2. 繼續執行A, A中沒有可執行的函數了,A執行完 出棧。
  3. 而後C(), 這時的C 就是 B,A 執行後,把B返回 賦值給了C,B執行環境入棧。
  4. B中 沒有可執行的函數了,B執行完 出棧。

函數調用棧

眼尖的同窗,估計看出來了,它怎麼像閉包呢?其實,稍微改動下,它就是閉包了。設計

function A(){
    
    var say = 666
    
    function B(){
        console.log(say);
    }
 
    return B;
}

var C = A();

C();

//666

這就是閉包了,可是此次咱們不講閉包,你就知道,它是的執行是怎麼回事就行。

3d

3、執行過程

如今咱們已經知道,每當一個函數執行時,一個新的執行環境就會被建立出來。其實,在js引擎內部,這個環境的建立過程可分爲兩個階段:code

A. 創建階段(發生在調用(執行)一個函數時,可是在執行函數內部的具體代碼以前)

       1.創建活動對象;
        2.構建做用域鏈;
        3.肯定this的值。

B. 代碼執行階段(執行函數內部的具體代碼)
       1.變量賦值;
       2.執行其它代碼。

須要注意的是,做用域鏈是建立函數的時候就建立了,此時的鏈只有全局變量對象,保存在函數的[[Scope]]屬性中,而後函數執行時的,只是經過複製該屬性中的對象 來 構建做用域鏈。本文後面還有說明。

看圖更清晰!

執行上下文

若是把函數執行環境當作一個對象的話:

executionContextObj = {           //執行上下文對象
            AtiveObject: { },  //活動對象
            scopeChain: { },      //做用域鏈
            this: {}              //this
}

//下面這段內容,感興趣的能夠看下,不感興趣,就跳過哈。
也許你在別家看到跟個人不同,人家寫的是創建變量對象。下面我來講說我得想法吧!

以前我按照 首先創建變量對象,其後,變量對象轉變爲活動對象的規則 去理解,可是呢,經過我分析JavaScript高級程序設計第三版,4.2節 和 7.2節,發現根本就不符合邏輯。

而後,我根據分析,得出了個人結論:變量對象 是執行環境中保存着環境中定義的全部變量和函數 的對象 的統稱。而活動對象,是函數執行環境中建立的,它不只保存着函數執行環境中定義的變量和函數,而且獨有一個arguments 屬性。所以,活動對象也可稱之爲變量對象。

這樣,不少東西就說的通了。
好比(如下都是來自JavaScript高級程序設計第三版,4.2節 和 7.2節 中原文):

若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。

當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。
而後,使用 arguments 和其餘命名參數的值來初始化函數的活動對象。

每一個執行環境都有一個表示變量的對象——變量對象。

此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執
行環境做用域鏈的前端。對於這個例子中 compare() 函數的執行環境而言,其做用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。

有興趣的能夠去看看這本書上說的,有不一樣的想法能夠積極留言,我們好好探討,哈哈。

(一)創建階段

一、創建活動對象(AO)

A. 創建arguments對象,檢查當前上下文中的參數,創建該對象下的屬性以及屬性值 。

B. 檢查當前環境中的函數聲明(使用function 聲明的)。每找到一個函數聲明,就在活動對象下面用函數名創建一個屬性,屬性值就是指向該函數在內存中的地址的一個引用,若是上述函數名已經存在於活動對象下,那麼則會被新的函數引用所覆蓋。

C. 檢查當前上下文中的變量聲明(使用 var 聲明的)。每找到一個變量聲明,就在活動對象下面用變量名創建一個屬性,該屬性值爲undefined。若是該屬性名已存在,則忽略新的聲明。

function test(){
    function a(){};
    var b;
}
test();

test 函數 的活動對象:

testAO: {    //test變量對象
    arguments: { ... };
    a:function(){};
    b:undefined
}

變量做用域
javaScript 中,只有兩種變量做用域,一種是局部變量做用域,又稱函數做用域。另外一個則是全局做用域。

什麼變量提高問題的根本緣由就在創建階段了。

console.log(A);

function A(){};

console.log(B);

var A = 666;
var B = 566;

console.log(A);
console.log(B);

//function A
//undefined
//666
//566

上面的實際順序就是這樣的了

function A(){};
//var A; 這個var 聲明的 同名 A,會被忽略

var B = undefined;

console.log(A);

console.log(B);

A = 666;   //給A 從新賦值
B = 566;   //給B 賦值

console.log(A);
console.log(B);

注意第三點,使用var 聲明時,若是VO對象下,該屬性已存在,忽略新的var 聲明。
由於A 使用 function 聲明,VO對象下,建立A屬性,而後 var 聲明時,檢索發現已經有該屬性了,就會忽略 var A 的聲明,不會把A 設置爲 undefined。

二、構建做用域鏈
做用域鏈的最前端,始終都是當前執行的代碼所在函數的活動對象。下一個AO(活動對象)爲包含本函數的外部函數的AO,以此類推。最末端,爲全局環境的變量對象。

注意:雖然做用域鏈是在函數調用時構建的,可是,它跟調用順序(進入調用棧的順序)無關,由於它只跟 包含關係(函數 包含 函數 的嵌套關係) 有關。

可能比較繞口,仍是來個小栗子,再來個圖

function fa(){
    var va = "this is fa";
    
    function fb(){
        var vb = "this is fb";
    
        console.log(vb);
        
        console.log(va);
    }
    return fb;
}
var fc = fa();
fc();

//"this is fb"
//"this is fa"

函數調用棧的狀況就是這樣:

函數調用棧

那麼把函數 fb 的執行環境比做對象(創建階段):

fbEC = {           //執行上下文對象

            fbAO: {   //活動對象 AO
            
                  arguments: { ... };   //arguments 對象

                  vb: undefined   //變量聲明創建的屬性,設置爲undefined
            },
            
            scopeChain: [ AO(fa), AO(fb), VO(window) ],      //做用域鏈
            
            this: { ... }              //this
}

fb做用域的展開就是這樣的:

做用域鏈
fb 函數 被 fa 函數 包含, fa 函數 被 window 全局環境包含。做用域鏈只跟包含關係有關!

注意:做用域鏈是單向的,所以,函數內的能夠訪問函數外 和 全局的變量,函數,可是反過來,函數外,全局內 不能訪問函數內的變量,函數。

三、肯定 this 指向
因此說 this 的指向,是在函數執行時肯定的。

(二)執行階段

一、變量賦值
根據代碼順序執行,遇到變量賦值時, 給對應的變量賦值。

function getColor(){
    console.log(color);
    
    var color;
    console.log(color);
    
    color = "red";
    console.log(color);
}
getColor();
//undefined
//undefined
//"red";

三、執行其餘代碼。

當函數執行完畢後,局部活動對象就會被銷燬(也就是說,局部的變量,函數,arguments 等都會被銷燬),內存中僅保存全局做用域(全局執行環境的變量對象)。

這句話對理解閉包很重要,隨後,我會出一個閉包的文章,敬請期待!

相關文章
相關標籤/搜索