貧道,感覺,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>
複製程式碼
在這裡,我們並沒有呼叫函式,但是這個函式,已經被初始化好了,其實,初始化的內容,比我們看到的要多。
如何避免變數提升:
使用 let
和 const
關鍵字,儘量使用 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 內部能正常查詢 我們需要的變數!。
我的一點疑惑
注意:在這裡,我無法證明一個問題。
- 全域性執行上下文初始化完畢之後,它是把所有的函式作用域鏈確定。
- 還是,初始化一個執行上下文,將本作用域的函式作用域鏈確定。
這是我的疑惑,我無法證明這個問題,但是,我更傾向於 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
以及我們宣告的變數,這個例子裡面是 x
、y
。
執行執行上下文 階段:
結束 執行上下文 階段:
好了,但是,這裡沒有涉及到呼叫其他函式。
其實,還有,我們的 JavaScript 引擎是如何管理,多個函式之間的 執行上下文 ???
管理多個執行上下文,其實是用的 上下文執行棧 具體請參考連結:請猛戳這裡,大佬寫的文章。
參考與鳴謝:
- 此文章主要參考自《高效能 JavaScript》