JavaScript——執行環境、變數對象、作用域鏈

来源:http://www.cnblogs.com/liying0721/archive/2017/12/15/8044045.html
-Advertisement-
Play Games

前言 這幾天在看《javascript高級程式設計》,看到執行環境和作用域鏈的時候,就有些模糊了。書中還是講的不夠具體。通過上網查資料,特來總結,以備回顧和修正。 目錄: EC(執行環境或者執行上下文,Execution Context) ECS(執行環境棧Execution Context Sta ...


前言

這幾天在看《javascript高級程式設計》,看到執行環境和作用域鏈的時候,就有些模糊了。書中還是講的不夠具體。通過上網查資料,特來總結,以備回顧和修正。

目錄:

  • EC(執行環境或者執行上下文,Execution Context)
  • ECS(執行環境棧Execution Context Stack)
  • VO(變數對象,Variable Object)|AO(活動對象,Active Object)
  • Scope Chain(作用域鏈)和[[Scope]]屬性

EC——執行環境或執行上下文

每當控制器到達ECMAScript可執行代碼的時候,控制器就進入了一個執行上下文。

JavaScript中,EC分為三種:

  • 全局級別的代碼——這個是預設的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境
  • 函數級別的代碼——當執行一個函數式,運行函數體中的代碼
  • Eval的代碼——在Eval函數內運行的代碼

EC建立分為倆個階段:

  1. 進入上下文階段:發生在函數調用時,但是在執行具體代碼之前(比如,對函數參數進行具體化之前)
  2. 執行代碼階段:變數賦值,函數引用,執行其它代碼

我們可以將EC看做是一個對象:

EC={
    VO:{/* 函數中的arguments對象, 參數, 內部的變數以及函數聲明 */},
    this:{},
    Scope:{ /* VO以及所有父執行上下文中的VO */}
}

ECS——執行環境棧

一系列活動的執行上文從邏輯上形成一個棧。棧底總是全局上下文,棧頂是當前(活動的)執行上下文。當在不同的執行上文間切換(退出的而進入新的執行上下文)的時候,棧會被修改(通過壓棧或退棧的形式)。

壓棧:全局EC → 局部EC1 → 局部EC2 → 當前EC

出棧:全局EC ←全局EC1 ←全局EC2 ←當前EC

我們可以用數組的形式來表示環境棧:

ECS=[局部EC,全局EC];

每次控制器進入一個函數(哪怕該函數被遞歸調用或者作為構造器),都會發生壓棧的操作。過程類似JavaScript數組的Push和Pop操作。

當JavaScript代碼文件被瀏覽器載入後,預設最先進入的是一個全局的執行上下文。當在全局上下文中調用執行一個函數時,程式流就進入該被調用函數內,此時引擎就會為該函數創建一個新的執行上下文,並且將其壓入到執行上下文堆棧的頂部。瀏覽器總是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,然後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行並且彈出堆棧,直到回到全局的上下文。

VO——變數對象|AO——活動對象

 

VO

每一個EC都對應一個變數對象VO,在該EC中定義的所有變數和函數都存在其對應的VO中。

VO分為全局上下文VO(全局對象,Global Object,我們通常說的Global對象)和函數上下文的AO

VO: {
  // 上下文中的數據 (變數聲明(var), 函數聲明(FD), 函數形參(function arguments))
}
  • 進入執行上下文時,VO的初始化過程具體如下:
    1. 函數的形參(當進入函數執行上下文時)—— 變數對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值為undefined
    2. 函數聲明(FunctionDeclaration, FD) —— 變數對象的一個屬性,其屬性名和值都是函數對象創建出來的;如果變數對象已經包含了相同名字的屬性,則替換它的值
    3. 變數聲明(var,VariableDeclaration) —— 變數對象的一個屬性,其屬性名即為變數名,其值為undefined;如果變數名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。

註意:改過程是有先後順序的。

  • 執行代碼階段時,VO中的一些屬性undefined值將會確定。

AO

在函數的執行上下文中,VO是不能直接訪問的。它主要扮演被稱作活躍對象(activation object)(簡稱:AO)的角色。

這句話怎麼理解呢,就是當EC環境為函數時,我們訪問的是AO,而不是VO。

VO(functionContext) === AO;

AO是在進入函數的執行上下文時創建的,併為該對象初始化一個arguments屬性,該屬性的值為Arguments對象。

AO = {
  arguments: {
    callee:,
    length:,
    properties-indexes: //函數傳參參數值
  }
};

FD的形式只能是如下這樣:

function f(){
  
}

示例

 

 

VO示例

alert(x); // function
 
var x = 10;
alert(x); // 10
 
x = 20;
 
function x() {};
 
alert(x); // 20

進入執行上下文時:

ECObject={
  VO:{
    x:<reference to FunctionDeclaration "x">
  }
};

執行代碼時:

ECObject={
  VO:{
    x:20 //與函數x同名,替換掉,先是10,後變成20
  }
};

對於以上的過程,我們詳細解釋下。

在進入上下文的時候,VO會被填充函數聲明;同一階段,還有變數聲明 ” X ”,但是,正如此前提到的,變數聲明是在函數聲明和函數形參之後,並且,變數聲明不會對已經存在的統一名字的函數聲明和函數形參發生衝突。因此,在進入上下文的階段,VO填充如下形式:

VO = {};
 
VO['x'] = <引用了函數聲明'x'>
 
// 發現var x = 10;
// 如果函數“x”還未定義
// 則 "x" 為undefined, 但是,在我們的例子中
// 變數聲明並不會影響同名的函數值
 
VO['x'] = <值不受影響,仍是函數>

執行代碼階段,VO被修改如下:

VO['x'] = 10;
VO['x'] = 20;

如下例子再次看到在進入上下文階段,變數存儲在VO中(因此,儘管else的代碼塊永遠都不會執行到,而“b”卻仍然在VO中)

if (true) {
  var a = 1;
} else {
  var b = 2;
}
 
alert(a); // 1
alert(b); // undefined, but not "b is not define
 

AO示例

unction test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}
 
test(10); // call

當進入test(10)的執行上下文時,它的AO為:

testEC={
    AO:{
        arguments:{
            callee:test
            length:1,
            0:10
        },
        a:10,
        c:undefined,
        d:<reference to FunctionDeclaration "d">,
        e:undefined
    }
};

由此可見,在建立階段,VO除了arguments,函數的聲明,以及參數被賦予了具體的屬性值,其它的變數屬性預設的都是undefined。函數表達式不會對VO造成影響,因此,(function x() {})並不會存在於VO中。

當執行test(10)時,它的AO為:

testEC={
    AO:{
        arguments:{
            callee:test,
            length:1,
            0:10
        },
        a:10,
        c:10,
        d:<reference to FunctionDeclaration "d">,
        e:<reference to FunctionDeclaration "e">
    }
};

可見,只有在這個階段,變數屬性才會被賦具體的值。

作用域鏈

在執行上下文的作用域中查找變數的過程被稱為標識符解析(indentifier resolution),這個過程的實現依賴於函數內部另一個同執行上下文相關聯的對象——作用域鏈。作用域鏈是一個有序鏈表,其包含著用以告訴JavaScript解析器一個標識符到底關聯著那一個變數的對象。而每一個執行上下文都有其自己的作用域鏈Scope。

一句話:作用域鏈Scope其實就是對執行上下文EC中的變數對象VO|AO有序訪問的鏈表。能按順序訪問到VO|AO,就能訪問到其中存放的變數和函數的定義。

Scope定義如下:

Scope = AO|VO + [[Scope]]

其中,AO始終在Scope的最前端,不然為啥叫活躍對象呢。即:

Scope = [AO].concat([[Scope]]);

這說明瞭,作用域鏈是在函數創建時就已經有了。

那麼[[Scope]]是什麼呢?

[[Scope]]是一個包含了所有上層變數對象的分層鏈,它屬於當前函數上下文,併在函數創建的時候,保存在函數中。

[[Scope]]是在函數創建的時候保存起來的——靜態的(不變的),只有一次並且一直都存在——直到函數銷毀。 比方說,哪怕函數永遠都不能被調用到,[[Scope]]屬性也已經保存在函數對象上了。

var x=10;
function f1(){
  var y=20;
  function f2(){
    return x+y;
  }
}

以上示例中,f2的[[scope]]屬性可以表示如下:

f2.[[scope]]=[
  f2OuterContext.VO
]

f2的外部EC的所有上層變數對象包括了f1的活躍對象f1Context.AO,再往外層的EC,就是global對象了。
所以,具體我們可以表示如下:

f2.[[scope]]=[
  f1Context.AO,
  globalContext.VO
]

對於EC執行環境是函數來說,那麼它的Scope表示為:

functionContext.Scope=functionContext.AO+function.[[scope]]

註意,以上代碼的表示,也體現了[[scope]]和Scope的差異,Scope是EC的屬性,而[[scope]]則是函數的靜態屬性。

(由於AO|VO在進入執行上下文和執行代碼階段不同,所以,這裡及以後Scope的表示,我們都預設為是執行代碼階段的Scope,而對於靜態屬性[[scope]]而言,則是在函數聲明時就創建了)

對於以上的代碼EC,我們可以給出其Scope的表示:

exampelEC={
  Scope:[
    f2Context.AO+f2.[[scope]],
    f1.context.AO+f1.[[scope]],
    globalContext.VO
  ]
}

接下來,我們給出以上其它值的表示:

  • globalContext.VO
globalContext.VO={
  x:10,
  f1:<reference to FunctionDeclaration "f1">
}
  • f2Context.AO
f2Context.AO={
  f1Context.AO={
    arguments:{
      callee:f1,
      length:0
    },
    y:20,
    f2:<reference to FunctionDeclaration "f2">
  }
}
  • f2.[[scope]]
f2Context.AO={
  f1Context.AO:{
    arguments:{
      callee:f1,
      length:0
    },
    y:20,
    f2:<reference to FunctionDeclaration "f2">
  },
  globalContext.VO:{
    x:10,
    f1:<reference to FunctionDeclaration "f1">
  }
}
  • f1.[[scope]](f1的所有上層EC的VO)
f1.[[scope]]={
  globalContext.VO:{
    x:undefined,
    f1:undefined
  }
}

好,我們知道,作用域鏈Scope呢,是用來有序訪問VO|AO中的變數和函數,對於上面的示例,我們給出訪問的過程:

  • x,f1
- "x"
-- f2Context.AO // not found
-- f1Context.AO // not found
-- globalContext.VO // found - 10

f1的訪問過程類似。

  • y
- "y"
-- f2Context.AO // not found
-- f1Context.AO // found -20

我們發現,在變數和函數的訪問過程,並沒有涉及到[[scope]],那麼[[scope]]存在的意義是什麼呢?

這個還是看下一篇文章吧。

總結

  1. EC分為倆個階段,進入執行上下文和執行代碼。
  2. ECStack管理EC的壓棧和出棧。
  3. 每個EC對應一個作用域鏈,VO|AO(AO,VO只能有一個),this。
  4. 函數EC中的Scope在進入函數EC是創建,用來有序方位該EC對象AO中的變數和函數。
  5. 函數EC中的AO在進入函數EC時,確定了Arguments對象的屬性;在執行函數EC時,其它變數屬性具體化。
  6. 函數的[[scope]]屬性在函數創建時就已經確定,並保持不變。

 

 

轉自:https://segmentfault.com/a/1190000000533094#articleHeader1


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 查看本地git配置信息 git config --global -e 查看自己科學上網的代理地址和埠信息 為git添加代理 git config --global http.proxy https://127.0.0.1:51643 git config --global https.proxy ...
  • [1]電商訂單 [2]職責鏈模式重構 [3]AOP [4]文件上傳 ...
  • 至此已完成NodeJsInputFileSysten模塊的講解,下一步就是實際實用的模塊: 掛載到compiler對象上的輸入模塊其實是帶有緩存的輸入模塊,源碼整理如下(用ES6的class重寫): 這裡的核心是利用Storage來生成一個緩存容器,緩存對應的讀操作。 有兩個需要註意的地方。 一個是 ...
  • 在cachedInput、output、watch三大文件系統中,output非常簡單,沒有必要講,其餘兩個模塊依賴於input模塊,而input主要是引用了graceful-fs的部分API,所以這節來講講graceful-fs。 上一節整理的源碼如下: 內容包含: 1、工具方法 2、patch引 ...
  • [1]享元模式初識 [2]文件上傳 [3]適用性 [4]對象池 ...
  • 相同: 1、兩者都能隱藏元素。 不同: 1、display:none 不占頁面空間,visiblity:hidden 占據原先頁面空間。 這裡必須說明的是,元素不占頁面空間後,取該元素或其內部元素的寬高值永遠是0。如果想隱藏又想取到寬高值,那就得用visiblity:hidden。 2、displa ...
  • 最近遇到這方面知識,就自己找了一些資料,進行了一些總結 什麼是分段傳輸? 當引入了一個http首部。這個首部標識了實體採用chunked編碼傳輸,chunked編碼可以將實體分塊兒進行傳輸,並且chunked編碼的每一塊內容都會自標識長度。這給了web開發者一個啟示,如果需要多個數據,而多個數據均返 ...
  • string對象 1.str.length () 輸出字元串長度 2.charAt()返回指定位置的字元串 返回指定位置的字元串 參數為 下標 0 開始 如果參數不在0-str.length範圍內則返回空字元串 3.concat()鏈接字元串 鏈接字元串 用於鏈接兩個或者多個字元串 4.indexO ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...