說明
浏覽器工作原理與實踐專欄學習筆記
前言
由于 JavaScript 存在變量提升這種特性,進而導緻了很多與直覺不符的代碼,這也是 JavaScript 的一個重要設計缺陷。
- 分析為什麼在 JavaScript 中會存在變量提升,以及變量提升所帶來的問題
- 介紹如何通過塊級作用域并配合 let 和 const 關鍵字來修複這種缺陷
作用域(scope)
作用域是指在程式中定義變量的區域,該位置決定了變量的生命周期。通俗地了解,作用域就是變量與函數的可通路範圍,即作用域控制着變量和函數的可見性和生命周期。
在 ES6 之前,ES 的作用域隻有兩種:
- 全局作用域中的對象在代碼中的任何地方都能通路,其生命周期伴随着頁面的生命周期。
- 函數作用域就是在函數内部定義的變量或者函數,并且定義的變量或者函數隻能在函數内部被通路。函數執行結束之後,函數内部定義的變量會被銷毀。
ES6 之前是不支援塊級作用域的
在ES3開始, try /catch
分句結構中也具有塊作用域。
塊級作用域
塊級作用域就是使用一對大括号包裹的一段代碼,比如函數、判斷語句、循環語句,甚至單獨的一個
{}
都可以被看作是一個塊級作用域。
//if塊
if(1){}
//while塊
while(1){}
//函數塊
function foo(){}
//for循環塊
for(let i = 0; i<100; i++){}
//單獨一個塊
{}
變量提升所帶來的問題
1. 變量容易在不被察覺的情況下被覆寫掉
var myname = "極客時間"
function showName(){
console.log(myname);
if(0){
var myname = "極客邦"
}
console.log(myname);
}
showName()
開始執行 showName 函數時的調用棧
先使用函數執行上下文裡面的變量,輸出兩個
undefined
2. 本應銷毀的變量沒有被銷毀
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
在建立執行上下文階段,變量 i 就已經被提升了,是以當 for 循環結束之後,變量 i 并沒有被銷毀。最後列印出來的是 7。
ES6 是如何解決變量提升帶來的缺陷
ES6 引入了 let 和 const 關鍵字,進而使 JavaScript 也能像其他語言一樣擁有了塊級作用域。
let 和 const 的用法:
let x = 5
const y = 6
x = 7
y = 9 //報錯,const聲明的變量不可以修改
ES6 是如何通過塊級作用域來解決上面的問題的?
1、存在變量提升的代碼:
function varTest() {
var x = 1;
if (true) {
var x = 2; // 同樣的變量!
console.log(x); // 2
}
console.log(x); // 2
}
在編譯階段,會生成varTest 函數的執行上下文:
隻生成了一個變量 x,函數體内所有對 x 的指派操作都會直接改變變量環境中的 x 值。
2、把 var 關鍵字替換為 let 關鍵字,改造後的代碼如下:
function letTest() {
let x = 1;
if (true) {
let x = 2; // 不同的變量
console.log(x); // 2
}
console.log(x); // 1
}
let 關鍵字是支援塊級作用域的,是以在編譯階段,JavaScript 引擎并不會把 if 塊中通過 let 聲明的變量存放到變量環境中,這也就意味着在 if 塊通過 let 聲明的關鍵字,并不會提升到全函數可見。
JavaScript 是如何支援塊級作用域的
JavaScript 引擎是通過變量環境實作函數級作用域的,那麼 ES6 又是如何在函數級作用域的基礎之上,實作對塊級作用域的支援呢?
先看一下下面這段代碼
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
執行結果
執行流程
第一步是編譯并建立執行上下文
foo 函數的執行上下文
- 函數内部通過 var 聲明的變量,在編譯階段全都被存放到變量環境裡面。
- 通過 let 聲明的變量,在編譯階段會被存放到詞法環境(
)中。Lexical Environment
- 在函數的作用域塊内部,通過 let 聲明的變量并沒有被存放到詞法環境中。
第二步繼續執行到代碼塊裡面時
在詞法環境内部,維護了一個小型棧結構,棧底是函數最外層的變量,進入一個作用域塊後,就會把該作用域塊内部的變量壓到棧頂;當作用域執行完成之後,該作用域的資訊就會從棧頂彈出,這就是詞法環境的結構。(變量是指通過 let 或者 const 聲明的變量。)
當執行到作用域塊中的console.log(a)這行代碼時,就需要在詞法環境和變量環境中查找變量 a 的值了,具體查找方式是:
- 沿着詞法環境的棧頂向下查詢,如果在詞法環境中的某個塊中查找到了,就直接傳回給 JavaScript 引擎
- 如果沒有查找到,那麼繼續在變量環境中查找。
查找過程:
當作用域塊執行結束之後,其内部定義的變量就會從詞法環境的棧頂彈出