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

貧道,感受,JS的坑,不是通常地大。javascript

變量提高:

變量提高( hoisting )。java

我可恨的 var 關鍵字:

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

<script type="text/javascript">

    var str = 'Hello JavaScript hoisting';

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

這段代碼,很意外地簡單,咱們的到了想要的結果,在控制檯打印出了:Hello JavaScript hoisting數組

如今,我將這一段代碼,改一改,將 調用 放在前面, 聲明 放在後面。瀏覽器

不少語言好比說 C 或者 C++ 都是不容許的,可是 javaScript 容許bash

大家試着猜猜獲得的結果:閉包

<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' 刪除後,試試思考這段代碼的結果:post

<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>
複製代碼

但,若是爲了兼容也就沒辦法嘍,哈哈哈,致命一擊!!!

執行上下文:

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

做用域鏈:

其實,咱們知道,JS 用的是 詞法做用域 的。

關於 其餘做用域 不瞭解的童鞋,請移步到個人《談談 JavaScript 的做用域》,或者百度一下。

每個 javaScript 函數都表示爲一個對象,更確切地說,是 Function 對象的一個實例。

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

內部屬性 [[Scope]] 包含了一個函數被建立的做用域中對象的集合。

這個集合被稱爲函數的 做用域鏈,它能決定哪些數據能被訪問到。

來源於:《 高性能JavaScript 》;

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

打開谷歌瀏覽器的 console ,並輸入一下代碼:

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

console.log( add.prototype );   // 從原型鏈上的構造函數能夠看到,add 函數的隱藏屬性。
複製代碼

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

你須要這樣:

而後這樣:

好了,你已經看到了,[[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] :

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

由於,初始化時,[[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] :

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

這就解釋了,爲何結果是,testValue = "inner"

當 須要調用 testValue 變量時;

先找自己做用域,沒有,JS 引擎會順着 做用域鏈 向下尋找 [0] => [1] => [2] => [...]。

在這裏,找到 bar 函數做用域,另外有趣的是,Closure 就是閉包的意思 。

證實,全局做用域鏈是在 全局執行上下文初始化時 就已經肯定的:

咱們來作一個有趣的實驗,跟剛纔,按照我描述的方法,你能夠找到 [[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 聲明前:

編號2 運行時:

編號3 執行後:

你可按照個人方法,作不少次實驗,試着嵌套幾個函數,在調用它們以前觀察做用域鏈。

做用域鏈,是在 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) 句時,是程序正在準備:初始化執行上下文

如上圖所示,在函數未調用以前,已經有 add 函數的[[Scope]]屬性所保存的 做用域鏈 裏面已經有這些東西了。

當執行此函數時,會創建一個稱爲 執行上下文 (execution context) 的內部對象。

一個 執行上下文 定義了一個函數執行時的環境,每次調用函數,就會建立一個 執行上下文 ;

一旦初始化 執行上下文 成功,就會建立一個 活動對象 ,裏面會產生 this arguments 以及咱們聲明的變量,這個例子裏面是 xy

運行執行上下文 階段:

結束 執行上下文 階段

好了,可是,這裏沒有涉及到調用其餘函數。

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

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

參考與鳴謝:

  • 此文章主要參考自《高性能 JavaScript》
相關文章
相關標籤/搜索