理解 JS 作用域鏈與執行上下文

leiting1998發表於2018-03-31

理解 JS 作用域鏈與執行上下文

貧道,感覺,JS的坑,不是一般地大。

變數提升:

變數提升( hoisting )。

我可恨的 var 關鍵字:

你讀完下面內容就會明白標題的含義,先來一段超級簡單的程式碼:

<script type="text/javascript">

    var str = 'Hello JavaScript hoisting';

    console.log(str);	// Hello JavaScript hoisting
    
</script>
複製程式碼

這段程式碼,很意外地簡單,我們的到了想要的結果,在控制檯列印出了:Hello JavaScript hoisting

現在,我將這一段程式碼,改一改,將 呼叫 放在前面, 宣告 放在後面。

很多語言比如說 C 或者 C++ 都是不允許的,但是 javaScript 允許

你們試著猜猜得到的結果:

<script type="text/javascript">

    console.log(str);		// undefined

    var str = 'Hello JavaScript hoisting';

    console.log(str);		// Hello JavaScript hoisting

</script>
複製程式碼

你會覺得很奇怪,在我們呼叫之前,為什麼我們的 str = undefined ,而不是報錯:未定義???

我將 var str = 'Hello JavaScript hoisting' 刪除後,試試思考這段程式碼的結果:

<script type="text/javascript">

	console.log(str);		// Uncaught ReferenceError: str is not defined

</script>
複製程式碼

現在得到了,我們想要的,報錯:未定義。

事實上,在我們瀏覽器會先解析一遍我們的指令碼,完成一個初始化的步驟,它遇到 var 變數時就會先初始化變數為 undefined

這就是變數提升(hoisting ),它是指,瀏覽器在遇到 JS 執行環境的 初始化,引起的變數提前定義。

在上面的程式碼裡,我們沒有涉及到函式,因為,我想讓程式碼更加精簡,更加淺顯,顯然我們應該測試一下函式。

<script type="text/javascript">
	
	console.log(add);			// ƒ add(x, y) { return x + y; }
	
	function add(x, y) {
        return x + y;
	}

</script>
複製程式碼

在這裡,我們並沒有呼叫函式,但是這個函式,已經被初始化好了,其實,初始化的內容,比我們看到的要多。

如何避免變數提升:

使用 letconst 關鍵字,儘量使用 const 關鍵字,儘量避免使用 var 關鍵字;

<script type="text/javascript">
	
	// console.log(testvalue1);		// 報錯:testvalue1 is not defined
	
	// let testvalue1 = 'test';
	
	/*---------我是你的分割線-------*/
	
	console.log(testvalue2);		// 報錯:testvalue1 is not defined

	const testvalue2 = 'test';

</script>
複製程式碼

但,如果為了相容也就沒辦法嘍,哈哈哈,致命一擊!!!

理解 JS 作用域鏈與執行上下文

執行上下文:

執行上下文,又稱為執行環境(execution context),聽起來很厲害對不對,其實沒那麼難。

理解 JS 作用域鏈與執行上下文

作用域鏈:

其實,我們知道,JS 用的是 詞法作用域 的。

關於 其他作用域 不瞭解的童鞋,請移步到我的《談談 JavaScript 的作用域》,或者百度一下。

每一個 javaScript 函式都表示為一個物件,更確切地說,是 Function 物件的一個例項。

Function 物件同其他物件一樣,擁有可程式設計訪問的屬性。和一系列不能通過程式碼訪問的 屬性,而這些屬性是提供給 JavaScript 引擎存取的內部屬性。其中一個屬性是 [[Scope]] ,由 ECMA-262標準第三版定義。

內部屬性 [[Scope]] 包含了一個函式被建立的作用域中物件的集合。

這個集合被稱為函式的 作用域鏈,它能決定哪些資料能被訪問到。

來源於:《 高效能JavaScript 》;

我好奇的是,怎樣才能看到這個,不能通過程式碼訪問的屬性???經過老夫的研究得出,能看到這個東西的方法;

開啟谷歌瀏覽器的 console ,並輸入一下程式碼:

function add(x, y) {
  return x + y;
}

console.log( add.prototype );   // 從原型鏈上的建構函式可以看到,add 函式的隱藏屬性。
複製程式碼

可能還有其他辦法,但,我只摸索到了這一種。

你需要這樣:

理解 JS 作用域鏈與執行上下文

然後這樣:

理解 JS 作用域鏈與執行上下文

好了,你已經看到了,[[Scope]] 屬性下是一個陣列,裡面儲存了,作用域鏈,此時只有一個 global

思考以下程式碼,並回顧 詞法作用域,結合 [[Scope]] 屬性思考,你就能理解 詞法作用域 的原理,

var testValue = 'outer';

function foo() {
  console.log(testValue);		// "outer"
  
  console.log(foo.prototype)	// 編號1
}

function bar() {
  var testValue = 'inner';
  
  console.log(bar.prototype)	// 編號2
  
  foo();
}

bar();
複製程式碼

以下是,執行結果:

編號 1 的 [[Scope]] 屬性:Scopes[1] :

理解 JS 作用域鏈與執行上下文

編號 2 的 [[Scope]] 屬性:Scopes[1]

理解 JS 作用域鏈與執行上下文

因為,初始化時,[[Scope]] 已經被確定了,兩個函式無論是誰,如果自身的作用域沒找到的話,就會在全域性作用域裡尋找變數。

再思考另外一段程式碼:

var testValue = 'outer';

function bar() {
  var testValue = 'inner';
  
  foo();
  
  console.log(bar.prototype)	// 編號 1
  
  function foo() {
    console.log(testValue);		// "inner"
    
    console.log(foo.prototype);	// 編號 2 
  }
}

bar();
複製程式碼

編號 1 的 [[Scope]] 屬性:Scopes[1] :

理解 JS 作用域鏈與執行上下文

編號 2 的 [[Scope]] 屬性:Scopes[2] :

理解 JS 作用域鏈與執行上下文

這就解釋了,為什麼結果是,testValue = "inner"

當 需要呼叫 testValue 變數時;

先找本身作用域,沒有,JS 引擎會順著 作用域鏈 向下尋找 [0] => [1] => [2] => [...]。

在這裡,找到 bar 函式作用域,另外有趣的是,Closure 就是閉包的意思 。

理解 JS 作用域鏈與執行上下文

證明,全域性作用域鏈是在 全域性執行上下文初始化時 就已經確定的:

我們來做一個有趣的實驗,跟剛才,按照我描述的方法,你可以找到 [[Scope]] 屬性。

那這個屬性是在什麼時候被確定的呢???

很顯然,我們需要從,函式宣告前,函式執行時,和函式執行完畢以後三個方面進行測試:

console.log(add.prototype);		// 編號1 宣告前

function add(x, y) {

  console.log(add.prototype);	// 編號2 執行時
  return x + y;
}

add(1, 2);
console.log(add.prototype);		// 編號3 執行後
複製程式碼

編號1 宣告前:

理解 JS 作用域鏈與執行上下文

編號2 執行時:

理解 JS 作用域鏈與執行上下文

編號3 執行後:

理解 JS 作用域鏈與執行上下文

你可按照我的方法,做很多次實驗,試著巢狀幾個函式,在呼叫它們之前觀察作用域鏈。

作用域鏈,是在 JS 引擎 完成 初始化執行上下文環境,已經確定了,這跟我們 變數提升 小節講述得一樣。

它保證著 JS 內部能正常查詢 我們需要的變數!。

我的一點疑惑

注意:在這裡,我無法證明一個問題。

  1. 全域性執行上下文初始化完畢之後,它是把所有的函式作用域鏈確定。
  2. 還是,初始化一個執行上下文,將本作用域的函式作用域鏈確定。

這是我的疑惑,我無法證明這個問題,但是,我更傾向於 2 的觀點,如果知道如何證明請聯絡我。至少,《高效能JavaScript》中是這樣描述的。

知道作用域鏈有什麼好處?

試想,我們知道作用域鏈,有什麼用呢???

我們知道,如果作用域鏈越深, [0] => [1] => [2] => [...] => [n],我們呼叫的是 全域性變數,它永遠在最後一個(這裡是第 n 個),這樣的查詢到我們需要的變數會引發多大的效能問題?JS 引擎查詢變數時會耗費多少時間?

所以,這個故事告訴我們,儘量將 全域性變數區域性化 ,避免,作用域鏈的層層巢狀,所帶來的效能問題。

理解 執行上下文:

將這段程式碼,放置於全域性作用域之下。這一段程式碼,改編自《高效能JavaScript》。

function add(x, y) {
    return x + y;
}

var result = add(1, 2);
複製程式碼

這段程式碼也很簡潔,但在 JavaScript 引擎內部發生的事情可並不簡單。

正如,上一節,變數提升 所論述,JS 引擎會初始化我們宣告 函式 和 變數 。

那麼在 add(1, 2) 執行前,我們的 add 函式 [[Scope]] 內是怎樣的呢???

這裡有三個時期:初始化 執行上下文、執行 執行上下文、結束 執行上下文

很顯然,執行到 var result = add(1, 2) 句時,是程式正在準備:初始化執行上下文

理解 JS 作用域鏈與執行上下文

如上圖所示,在函式未呼叫之前,已經有 add 函式的[[Scope]]屬性所儲存的 作用域鏈 裡面已經有這些東西了。

當執行此函式時,會建立一個稱為 執行上下文 (execution context) 的內部物件。

一個 執行上下文 定義了一個函式執行時的環境,每次呼叫函式,就會建立一個 執行上下文 ;

一旦初始化 執行上下文 成功,就會建立一個 活動物件 ,裡面會產生 this arguments 以及我們宣告的變數,這個例子裡面是 xy

執行執行上下文 階段:

理解 JS 作用域鏈與執行上下文

結束 執行上下文 階段

理解 JS 作用域鏈與執行上下文

好了,但是,這裡沒有涉及到呼叫其他函式。

其實,還有,我們的 JavaScript 引擎是如何管理,多個函式之間的 執行上下文 ???

管理多個執行上下文,其實是用的 上下文執行棧 具體請參考連結:請猛戳這裡,大佬寫的文章。

參考與鳴謝:

  • 此文章主要參考自《高效能 JavaScript》

相關文章