貧道,感受,JS的坑,不是通常地大。javascript
變量提高( hoisting )。java
你讀完下面內容就會明白標題的含義,先來一段超級簡單的代碼:編程
<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>
複製代碼
在這裏,咱們並無調用函數,可是這個函數,已經被初始化好了,其實,初始化的內容,比咱們看到的要多。
使用 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 引擎是如何管理,多個函數之間的 執行上下文 ???
管理多個執行上下文,實際上是用的 上下文執行棧 具體請參考連接:請猛戳這裏,大佬寫的文章。