相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)javascript
這部分原本打算放到簡單介紹的執行上下文和執行棧裏順帶說一句的,後來發現這裏面內容也很多,包括暫時性死區、函數及變量提高邏輯、es6中的塊級做用域等,就單開了一章。如下內容大概花費10分鐘左右,歡迎評論補充知識和點贊~前端
請說出 let,const,var 的區別java
大部分的回答是這樣的,node
而實際上, let / const
也有變量提高 。git
先來看個栗子:es6
console.log(aVar); // undefined
console.log(aLet); // causes ReferenceError: aLet is not defined
var aVar = 1;
let aLet = 2;
複製代碼
從結果上看,第二行沒有找到aLet致使程序報錯,代表let聲明的變量並無提高github
沒關係,再看兩個栗子(請在瀏覽器環境運行,node環境結果不同)面試
let x = 'global';
function func(){
console.log(x);
}
func(); // global
複製代碼
func運行時,在函數內沒有找到 x 的定義,沿着函數做用域鏈尋到外層關於 x 的定義。瀏覽器
let x = 'global';
function func1(){
console.log(x);
let x = 'func';
}
func1();
// Uncaught ReferenceError: Cannot access 'x' before initialization
// at func (<anonymous>:2:15)
// at <anonymous>:4:3
複製代碼
咦,報錯了?爲何func1沒有訪問到全局環境下的x呢?不要着急,仔細看下錯誤提示:沒法在初始化以前訪問x。函數
好好想一想,這個錯誤意味着在func內第一行程序已經知道在本函數內有一個變量叫x了,只不過沒有初始化(initialization)而已。
由此可得出結論**,因爲 _let x = ‘func’
_ 在函數做用域內存在變量提高,**阻斷了函數做用域鏈的向上延伸。儘管 x 發生了變量提高,可是在初始化賦值前(before initialization)不容許讀取。
這就引出了一個很重要的概念: 暫時性死區 (TDZ)
MDN 上關於暫時性死區的定義
let bindings are created at the top of the (block) scope containing the declaration, commonly referred to as 「hoisting」. Unlike variables declared with var, which will start with the value undefined, let variables are not initialized until their definition is evaluated. Accessing the variable before the initialization results in a ReferenceError. The variable is in a 「temporal dead zone」 from the start of the block until the initialization is processed.
let綁定是在包含聲明的(塊)範圍的頂部建立的,一般稱爲「提高」。不像用var聲明的變量,let聲明的變量不會被初始化(initialized)直到它們被定義位置的代碼開始執行,在初始化以前訪問變量會觸發一個ReferenceError。從塊的開始到變量初始化,變量都處於「暫時死區」。
簡單來講,let 僅僅發生了提高而沒有被賦初值,在顯式賦值以前,任何對變量的讀寫都會觸發ReferenceError 錯誤。從代碼塊(block)起始到變量賦值之前的這塊區域,稱爲該變量的暫時性死區。
當程序控制流程運行到特定做用域(scope ≈ Lexical Environment) 時:即模塊,函數,或塊級做用域。在該做用域中代碼真正執行以前,該做用域中定義的 let 和 const 變量會首先被建立出來,但由於在 let/const 變量被賦值(LexicalBinding)之前是不能夠讀寫的,因此存在暫時性死區。
來看下下面代碼驗證下你的理解:
function test(){
var foo = 33;
if(foo) {
let foo = (foo + 55); // ReferenceError
}
}
test();
複製代碼
因爲詞法做用域,表達式let foo = (foo + 55);中的foo被認爲是if塊中聲明的foo,而不是函數第一行聲明的var變量。在同一行中,if塊的foo已經在詞法環境中建立,因此程序不會沿着做用域鏈向上層尋找foo,但因爲變量還未初始化,處於暫時性死區中,訪問會觸發ReferenceError。
function go(n) {
// n here is defined!
console.log(n); // Object {a: [1,2,3]}
const a = n.a;
for (let n of n.a) { // ReferenceError
console.log(n);
}
}
go({a: [1, 2, 3]});
複製代碼
答案已經給了,歡迎在評論區留下你的看法
回到正題,從新看一下變量提高的邏輯.
當進入執行上下文時,
依據上述規則的邏輯,分析下列代碼:
代碼一:
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
// 依據規則二,函數表達式不會提高
// 依據規則三,相同變量名稱不會干擾
複製代碼
來看這段代碼:
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
// 依據規則二,函數聲明中若函數名相同,則後者徹底覆蓋前者
複製代碼
舉一個小栗子鞏固一下:
var a = 1;
function foo() {
a = 10;
console.log(a);
function a() {};
}
foo(); // 10
console.log(a); // 1
複製代碼
在foo中,函數a存在變量提高,至關於
var a = 1; // 定義一個全局變量 a
function foo() {
// 提高函數聲明function a () {}到函數做用域頂端, 函數a也是變量
var a = function () {}; // 定義局部變量 a 並賦值。
a = 10; // 修改局部變量 a 的值
console.log(a); // 打印局部變量 a 的值:10
return;
}
foo();
console.log(a); // 打印全局變量 a 的值:1
複製代碼
ES5 只有全局做用域和函數做用域,沒有塊級做用域。
ES6 的塊級做用域必須有大括號,若是沒有大括號,JavaScript 引擎就認爲不存在塊級做用域。
ES6 引入了塊級做用域,明確容許在塊級做用域之中聲明函數。ES6 規定,塊級做用域之中,函數聲明語句的行爲相似於let,在塊級做用域以外不可引用。
容許在塊級做用域內聲明函數。
函數聲明相似於var,即會提高到全局做用域或函數做用域的頭部。
同時,函數聲明還會提高到所在的塊級做用域的頭部。
友情提示,本篇文章建議與讓人恍然大悟的詞法做用域及做用域鏈講解和簡單介紹的執行上下文和執行棧一塊兒食用
ps: 我原本覺得暫時性死區是因爲建立執行上下文的方式致使的,結果搜資料的時候發現塊級做用域沒有單獨的執行上下文,只有詞法環境,若你知道塊級做用域與詞法環境的相關知識,歡迎在評論區留言,我會及時補充進來
相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)
若是你收穫了新知識,請給做者點個贊吧~
參考文檔: