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
- 如果變數名稱和已經宣告的形式引數或函式相同,那麼變數宣告將不起作用,保留後者,也就是說後者不會受前者宣告的干擾
- 對於所有找到的var變數宣告,都存入到VO/AO中,並初始化為
- 設定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宣告變數時必須初始化)