1. 程式人生 > 實用技巧 >js中的執行上下文--宣告提升,暫時性死區等

js中的執行上下文--宣告提升,暫時性死區等

在JavaScript程式碼開始執行前,js解析器做好了一系列的準備工作,並且規定了變數、函式的訪問路徑。(這個為個人的理解,如有理解錯誤,請大佬們指正!)
JavaScript在程式碼執行前做了什麼?會建立執行上下文,也就是準備好程式碼執行時的環境,包括變數物件、作用域鏈、this
問題的提出:

function test() {
    console.log(bar)
    var bar = 2
}
test()//undefined

console.log(foo)//undefined
var foo = 1
//上述行為是常說的變數提升,實際上就是建立執行上下文時做好的工作。以及需要了解變數的訪問規則

一、執行上下文

在js程式碼執行前,會建立所謂的執行上下文,也就是所謂的程式碼執行環境。那麼該執行環境有什麼作用?--->實際上,程式碼執行時,關於變數的訪問、函式的呼叫及this的指向繫結,都和執行上下文有著密切的關係,可以認為,都是從執行上下文中獲得它們的值。

1.1執行環境

JavaScript程式碼有三種執行環境,對應著三種執行上下文

  • global code:全域性作用域下的程式碼。不包括任何function體內的程式碼
  • Function Code:函式呼叫執行的程式碼,不包括其自身內部的函式的程式碼
  • Eval Code:eval()執行的程式碼

1.2EC三個屬性

執行上下文是一個物件,它具有三個屬性:scope chain

,variable object,this

  • 變數物件(variable object):vars、function declarations,arguments...
  • 作用域鏈(scope chain):variable object + all parent scopes
  • this:context object
1.2.1變數物件variable object

​變數物件是與執行上下文相關的資料作用域。儲存了在上下文定義的變數和函式宣告。

  • variable declaration,初始化為undefined
  • Function declaration,儲存函式名稱並持有函式體的引用
  • 函式的形參,初始化為undefined
  • 函式執行上下文中,沒有使用var宣告的變數為全域性變數,所以不在變數物件中
  • 函式表示式也不包含在變數物件中
  • let,const宣告的變數初始化為uninitialed
1.2.2作用域鏈scope chain

​全域性執行上下文沒有外部的作用域,因此定義其作用域鏈為自身的變數物件。

​函式執行上下文中的作用域鏈:實際上,當函式定義的時候,函式所有的外層變數物件(即集合)都會儲存在函式的內部屬性[[scope]]中,當建立函式執行上下文時,首先建立了該物件的屬性--作用域鏈,並把[[scope]]複製到scope chain中,但這並不是完整的作用域鏈。接著便是變數物件variable object的建立,建立過程見下文——JavaScript程式碼的執行前後做了什麼--執行上下文建立的完整過程。建立完成後,把變數物件複製到scope chain的頂端,形成完整的作用域鏈。

1.2.3this

this在建立執行上下文時進行繫結

1.2.3.1全域性程式碼中的this

this的繫結指向始終為window物件

console.log(this)//window,非嚴格模式下
//嚴格模式下為undefined
1.2.3.2函式程式碼的this
  • 預設繫結:函式呼叫時,若無字首,則this繫結為window

    function test() {
        console.log(this)
    }
    test()//window
    
    function f1(){
        function f2(){
            console.log(this)
        }
        f2()
    }
    f1()//window
    
  • 隱式繫結:函式呼叫時,前面存在呼叫它的物件,則this會隱式繫結到這個物件上

    function f1(){
        console.log(this.name,this)
    }
    f1()//window
    let obj = {
        name:'hello',
        fn:f1
    }
    obj.fn()//
    
  • 顯式繫結:call,apply,bind改變this

  • new繫結:

    new一個函式,實際進行了的操作:

    • 建立一個空物件,將它的引用賦給this,繼承函式的原型
    • 通過this將屬性和方法新增到這個物件
    • 若沒有手動返回其他的物件,則最後返回this指向的物件(即例項)
    • 以構造器的prototype屬性為原型建立一個物件
    • 將這個物件和呼叫引數傳給構造器執行,apply。。
    • 如果構造器沒有手動返回物件,則返回第一步的物件

    這個例項中的方法中(即物件的方法內的程式碼中)的this指向所建立的例項

  • 箭頭函式中的this

    ​ 箭頭函式沒有自己的this,this指向取決於外層作用域中的this,一旦箭頭函式的this繫結成功,也無法被再次修改,但可以修改外層函式的this指向達到間接修改箭頭函式this的目的。

二.JavaScript程式碼的執行前後做了什麼----EC的建立過程

當一段JavaScript程式碼執行的時候,JavaScript直譯器會建立執行上下文,包含了兩個階段:

  • 建立階段(函式被呼叫,但在開始執行函式內程式碼之前)
    • 建立scope chain
    • 建立VO/AO
      • 根據函式的引數,建立並初始化arguments object
      • 掃描函式內部程式碼,查詢函式宣告
        • 對於所有找到的函式宣告,將函式名和函式引用存入到VO/AO
        • 如果VO/AO中已有同名的函式,那麼就進行覆蓋
      • 掃描函式內部程式碼,查詢變數宣告
        • 對於所有找到的var變數宣告,都存入到VO/AO中,並初始化為undefined
        • let,const變數宣告,初始化為uninitialed
        • 如果變數名稱和已經宣告的形式引數或函式相同,那麼變數宣告將不起作用,保留後者,也就是說後者不會受前者宣告的干擾
    • 設定this的值
  • 啟用/程式碼執行階段
    • 設定變數的值、函式的引用,解釋/執行程式碼

三、結合執行上下文,解釋變數及函式的訪問規則--宣告提升

js程式碼開始執行時,瀏覽器便會建立全域性執行上下文(詳見EC的建立過程),每有一次函式呼叫,則建立一次函式執行上下文。
進行變數、函式及this值的訪問時,都會在當前執行上下文中的作用域鏈進行訪問。

宣告提升:

console.log(var1);//undefined
var var1 = 1;
console.log(var1);//1
console.log(fn);
function fn(){
      console.log('1111');
}

解析:訪問時,都是從執行上下文中的作用域鏈進行取值,建立執行上下文時,var宣告的變數被初始化為undefined,因此在變數被賦值前進行訪問,列印了undefined,賦值後訪問,列印1;
函式的宣告提升同理,只不過建立執行上下文時,函式的識別符號初始化為函式體的引用,故列印的為函式體。

let,const的暫時性死區:宣告前不能進行訪問

console.log(a);
let a = 1;
let b;
console.log(b)

解析:建立執行上下文時,被初始化為uninitialed,訪問uninitialed值會丟擲錯誤,而賦值後,則可以訪問let,const宣告的變數。(程式碼執行到let宣告語句時,若沒有進行賦值操作,則預設賦值為undefined,const宣告變數時必須初始化)