欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

通過實例了解JS執(zhí)行上下文運行原理

 更新時間:2020年06月17日 17:08:23   作者:聽風是風  
這篇文章主要介紹了通過實例了解JS執(zhí)行上下文運行原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

壹 ❀ 引

我們都知道,JS代碼的執(zhí)行順序總是與代碼先后順序有所差異,當先拋開異步問題你會發(fā)現(xiàn)就算是同步代碼,它的執(zhí)行也與你的預期不一致,比如:

function f1() {
  console.log('聽風是風');
};
f1(); //echo
function f1() {
  console.log('echo');
};
f1(); //echo

按照代碼書寫順序,應該先輸出 聽風是風,再輸出 echo才對,很遺憾,兩次輸出均為 echo;如果我們將上述代碼中的函數(shù)聲明改為函數(shù)表達式,結(jié)果又不太一樣:

var f1 = function () {
  console.log('聽風是風');
};
f1(); //聽風是風
var f1 = function() {
  console.log('echo');
};
f1(); //echo

這說明代碼在執(zhí)行前一定發(fā)生了某些微妙的變化,JS引擎究竟做了什么呢?這就不得不提JS執(zhí)行上下文的了。

貳 ❀ JS執(zhí)行上下文

JS代碼在執(zhí)行前,JS引擎總要做一番準備工作,這份工作其實就是創(chuàng)建對應的執(zhí)行上下文;

執(zhí)行上下文有且只有三類,全局執(zhí)行上下文,函數(shù)上下文,與eval上下文;由于eval一般不會使用,這里不做討論。

1.全局執(zhí)行上下文

全局執(zhí)行上下文只有一個,在客戶端中一般由瀏覽器創(chuàng)建,也就是我們熟知的window對象,我們能通過this直接訪問到它。

全局對象window上預定義了大量的方法和屬性,我們在全局環(huán)境的任意處都能直接訪問這些屬性方法,同時window對象還是var聲明的全局變量的載體。我們通過var創(chuàng)建的全局對象,都可以通過window直接訪問。

2.函數(shù)執(zhí)行上下文

函數(shù)執(zhí)行上下文可存在無數(shù)個,每當一個函數(shù)被調(diào)用時都會創(chuàng)建一個函數(shù)上下文;需要注意的是,同一個函數(shù)被多次調(diào)用,都會創(chuàng)建一個新的上下文。

說到這你是否會想,上下文種類不同,而且創(chuàng)建的數(shù)量還這么多,它們之間的關(guān)系是怎么樣的,又是誰來管理這些上下文呢,這就不得不說說執(zhí)行上下文棧了。

叁 ❀ 執(zhí)行上下文棧(執(zhí)行棧)

執(zhí)行上下文棧(下文簡稱執(zhí)行棧)也叫調(diào)用棧,執(zhí)行棧用于存儲代碼執(zhí)行期間創(chuàng)建的所有上下文,具有LIFO(Last In First Out后進先出,也就是先進后出)的特性。

JS代碼首次運行,都會先創(chuàng)建一個全局執(zhí)行上下文并壓入到執(zhí)行棧中,之后每當有函數(shù)被調(diào)用,都會創(chuàng)建一個新的函數(shù)執(zhí)行上下文并壓入棧內(nèi);由于執(zhí)行棧LIFO的特性,所以可以理解為,JS代碼執(zhí)行完畢前在執(zhí)行棧底部永遠有個全局執(zhí)行上下文。

function f1() {
  f2();
  console.log(1);
};
function f2() {
  f3();
  console.log(2);
};
function f3() {
  console.log(3);
};
f1();//3 2 1

我們通過執(zhí)行棧與上下文的關(guān)系來解釋上述代碼的執(zhí)行過程,為了方便理解,我們假象執(zhí)行棧是一個數(shù)組,在代碼執(zhí)行初期一定會創(chuàng)建全局執(zhí)行上下文并壓入棧,因此過程大致如下:

//代碼執(zhí)行前創(chuàng)建全局執(zhí)行上下文
ECStack = [globalContext];
// f1調(diào)用
ECStack.push('f1 functionContext');
// f1又調(diào)用了f2,f2執(zhí)行完畢之前無法console 1
ECStack.push('f2 functionContext');
// f2又調(diào)用了f3,f3執(zhí)行完畢之前無法console 2
ECStack.push('f3 functionContext');
// f3執(zhí)行完畢,輸出3并出棧
ECStack.pop();
// f2執(zhí)行完畢,輸出2并出棧
ECStack.pop();
// f1執(zhí)行完畢,輸出1并出棧
ECStack.pop();
// 此時執(zhí)行棧中只剩下一個全局執(zhí)行上下文

那么到這里,我們解釋了執(zhí)行棧與執(zhí)行上下文的存儲規(guī)則;還記得我在前文提到代碼執(zhí)行前JS引擎會做準備創(chuàng)建執(zhí)行上下文嗎,具體怎么創(chuàng)建呢,我們接著說。

肆 ❀ 執(zhí)行上下文創(chuàng)建階段

執(zhí)行上下文創(chuàng)建分為創(chuàng)建階段與執(zhí)行階段兩個階段,較為難理解應該是創(chuàng)建階段,我們先說創(chuàng)建階段。

JS執(zhí)行上下文的創(chuàng)建階段主要負責三件事:確定this---創(chuàng)建詞法環(huán)境組件(LexicalEnvironment)---創(chuàng)建變量環(huán)境組件(VariableEnvironment)

這里我就直接借鑒了他人翻譯資料的偽代碼,來表示這個創(chuàng)建過程:

ExecutionContext = { 
  // 確定this的值
  ThisBinding = <this value>,
  // 創(chuàng)建詞法環(huán)境組件
  LexicalEnvironment = {},
  // 創(chuàng)建變量環(huán)境組件
  VariableEnvironment = {},
};

如果你有閱讀其它關(guān)于執(zhí)行上下文的文章讀到這里一定有疑問,執(zhí)行上下文創(chuàng)建過程不是應該解釋this,作用域與變量對象/活動對象才對嗎,怎么跟別的地方說的不一樣,這點我后面解釋。

1.確定this

官方的稱呼為This Binding,在全局執(zhí)行上下文中,this總是指向全局對象,例如瀏覽器環(huán)境下this指向window對象。

而在函數(shù)執(zhí)行上下文中,this的值取決于函數(shù)的調(diào)用方式,如果被一個對象調(diào)用,那么this指向這個對象。否則this一般指向全局對象window或者undefined(嚴格模式)。

2.詞法環(huán)境組件

詞法環(huán)境是一個包含標識符變量映射的結(jié)構(gòu),這里的標識符表示變量/函數(shù)的名稱,變量是對實際對象【包括函數(shù)類型對象】或原始值的引用。

詞法環(huán)境由環(huán)境記錄與對外部環(huán)境引入記錄兩個部分組成。

其中環(huán)境記錄用于存儲當前環(huán)境中的變量和函數(shù)聲明的實際位置;外部環(huán)境引入記錄很好理解,它用于保存自身環(huán)境可以訪問的其它外部環(huán)境,那么說到這個,是不是有點作用域鏈的意思?

我們在前文提到了全局執(zhí)行上下文與函數(shù)執(zhí)行上下文,所以這也導致了詞法環(huán)境分為全局詞法環(huán)境與函數(shù)詞法環(huán)境兩種。

全局詞法環(huán)境組件:

對外部環(huán)境的引入記錄為null,因為它本身就是最外層環(huán)境,除此之外它還記錄了當前環(huán)境下的所有屬性、方法位置。

函數(shù)詞法環(huán)境組件:

包含了用戶在函數(shù)中定義的所有屬性方法外,還包含了一個arguments對象。函數(shù)詞法環(huán)境的外部環(huán)境引入可以是全局環(huán)境,也可以是其它函數(shù)環(huán)境,這個根據(jù)實際代碼而來。

這里借用譯文中的偽代碼(環(huán)境記錄在全局和函數(shù)中也不同,全局中的環(huán)境記錄叫對象環(huán)境記錄,函數(shù)中環(huán)境記錄叫聲明性環(huán)境記錄,說多了糊涂,下方有展示):

// 全局環(huán)境
GlobalExectionContext = {
  // 全局詞法環(huán)境
  LexicalEnvironment: {
    // 環(huán)境記錄
    EnvironmentRecord: {
      Type: "Object", //類型為對象環(huán)境記錄
      // 標識符綁定在這里 
    },
    outer: < null >
  }
};
// 函數(shù)環(huán)境
FunctionExectionContext = {
  // 函數(shù)詞法環(huán)境
  LexicalEnvironment: {
    // 環(huán)境紀錄
    EnvironmentRecord: {
      Type: "Declarative", //類型為聲明性環(huán)境記錄
      // 標識符綁定在這里 
    },
    outer: < Global or outerfunction environment reference >
  }
};

3.變量環(huán)境組件

變量環(huán)境可以說也是詞法環(huán)境,它具備詞法環(huán)境所有屬性,一樣有環(huán)境記錄與外部環(huán)境引入。在ES6中唯一的區(qū)別在于詞法環(huán)境用于存儲函數(shù)聲明與let const聲明的變量,而變量環(huán)境僅僅存儲var聲明的變量。

我們通過一串偽代碼來理解它們:

let a = 20; 
const b = 30; 
var c;

function multiply(e, f) { 
 var g = 20; 
 return e * f * g; 
}

c = multiply(20, 30);

我們用偽代碼來描述上述代碼中執(zhí)行上下文的創(chuàng)建過程:

//全局執(zhí)行上下文
GlobalExectionContext = {
  // this綁定為全局對象
  ThisBinding: <Global Object>,
  // 詞法環(huán)境
  LexicalEnvironment: { 
    //環(huán)境記錄
   EnvironmentRecord: { 
    Type: "Object", // 對象環(huán)境記錄
    // 標識符綁定在這里 let const創(chuàng)建的變量a b在這
    a: < uninitialized >, 
    b: < uninitialized >, 
    multiply: < func > 
   }
   // 全局環(huán)境外部環(huán)境引入為null
   outer: <null> 
  },
 
  VariableEnvironment: { 
   EnvironmentRecord: { 
    Type: "Object", // 對象環(huán)境記錄
    // 標識符綁定在這里 var創(chuàng)建的c在這
    c: undefined, 
   }
   // 全局環(huán)境外部環(huán)境引入為null
   outer: <null> 
  } 
 }

 // 函數(shù)執(zhí)行上下文
 FunctionExectionContext = {
   //由于函數(shù)是默認調(diào)用 this綁定同樣是全局對象
  ThisBinding: <Global Object>,
  // 詞法環(huán)境
  LexicalEnvironment: { 
   EnvironmentRecord: { 
    Type: "Declarative", // 聲明性環(huán)境記錄
    // 標識符綁定在這里 arguments對象在這
    Arguments: {0: 20, 1: 30, length: 2}, 
   }, 
   // 外部環(huán)境引入記錄為</Global>
   outer: <GlobalEnvironment> 
  },
 
  VariableEnvironment: { 
   EnvironmentRecord: { 
    Type: "Declarative", // 聲明性環(huán)境記錄
    // 標識符綁定在這里 var創(chuàng)建的g在這
    g: undefined 
   }, 
   // 外部環(huán)境引入記錄為</Global>
   outer: <GlobalEnvironment> 
  } 
 }

不知道你有沒有發(fā)現(xiàn),在執(zhí)行上下文創(chuàng)建階段,函數(shù)聲明與var聲明的變量在創(chuàng)建階段已經(jīng)被賦予了一個值,var聲明被設(shè)置為了undefined,函數(shù)被設(shè)置為了自身函數(shù),而let const被設(shè)置為未初始化。

現(xiàn)在你總知道變量提升與函數(shù)聲明提前是怎么回事了吧,以及為什么let const為什么有暫時性死域,這是因為作用域創(chuàng)建階段JS引擎對兩者初始化賦值不同。

上下文除了創(chuàng)建階段外,還有執(zhí)行階段,這點大家應該好理解,代碼執(zhí)行時根據(jù)之前的環(huán)境記錄對應賦值,比如早期var在創(chuàng)建階段為undefined,如果有值就對應賦值,像let const值為未初始化,如果有值就賦值,無值則賦予undefined。

伍 ❀ 關(guān)于變量對象與活動對象

回答前面的問題,為什么別人的博文介紹上下文都是談作用域,變量對象和活動對象,我這就成了詞法環(huán)境,變量環(huán)境了。

我在閱讀相關(guān)資料也產(chǎn)生了這個疑問,一番查閱可以確定的是,變量對象與活動對象的概念是ES3提出的老概念,從ES5開始就用詞法環(huán)境和變量環(huán)境替代了,因為更好解釋。

在上文中,我們通過介紹詞法環(huán)境與變量環(huán)境解釋了為什么var會存在變量提升,為什么let const沒有,而通過變量對象與活動對象是很難解釋的,由其是在JavaScript在更新中不斷在彌補當初設(shè)計的坑。

其次,詞法環(huán)境的概念與變量對象這類概念也是可以對應上的。

我們知道變量對象與活動對象其實都是變量對象,變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。而在函數(shù)上下文中,我們用活動對象(activation object, AO)來表示變量對象。

那這不正好對應到了全局詞法記錄與函數(shù)詞法記錄了嗎。而且由于ES6新增的let const不存在變量提升,于是正好有了詞法環(huán)境與變量環(huán)境的概念來解釋這個問題。

所以說到這,你也不用為詞法環(huán)境,變量對象的概念鬧沖突了。

我們來總結(jié)下上面提到的概念。

陸 ❀ 總結(jié)

1.全局執(zhí)行上下文一般由瀏覽器創(chuàng)建,代碼執(zhí)行時就會創(chuàng)建;函數(shù)執(zhí)行上下文只有函數(shù)被調(diào)用時才會創(chuàng)建,調(diào)用多少次函數(shù)就會創(chuàng)建多少上下文。

2.調(diào)用棧用于存放所有執(zhí)行上下文,滿足FILO規(guī)則。

3.執(zhí)行上下文創(chuàng)建階段分為綁定this,創(chuàng)建詞法環(huán)境,變量環(huán)境三步,兩者區(qū)別在于詞法環(huán)境存放函數(shù)聲明與const let聲明的變量,而變量環(huán)境只存儲var聲明的變量。

4.詞法環(huán)境主要由環(huán)境記錄與外部環(huán)境引入記錄兩個部分組成,全局上下文與函數(shù)上下文的外部環(huán)境引入記錄不一樣,全局為null,函數(shù)為全局環(huán)境或者其它函數(shù)環(huán)境。環(huán)境記錄也不一樣,全局叫對象環(huán)境記錄,函數(shù)叫聲明性環(huán)境記錄。

5.你應該明白了為什么會存在變量提升,函數(shù)提升,而let const沒有。

6.ES3之前的變量對象與活動對象的概念在ES5之后由詞法環(huán)境,變量環(huán)境來解釋,兩者概念不沖突,后者理解更為通俗易懂。

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

最新評論