天天看點

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

說明

浏覽器工作原理與實踐專欄學習筆記

前言

由于 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 函數時的調用棧

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

先使用函數執行上下文裡面的變量,輸出兩個​

​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 值。
浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

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()      

執行結果

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

執行流程

第一步是編譯并建立執行上下文

foo 函數的執行上下文

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?
  1. 函數内部通過 var 聲明的變量,在編譯階段全都被存放到變量環境裡面。
  2. 通過 let 聲明的變量,在編譯階段會被存放到詞法環境(​

    ​Lexical Environment​

    ​)中。
  3. 在函數的作用域塊内部,通過 let 聲明的變量并沒有被存放到詞法環境中。

第二步繼續執行到代碼塊裡面時

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

在詞法環境内部,維護了一個小型棧結構,棧底是函數最外層的變量,進入一個作用域塊後,就會把該作用域塊内部的變量壓到棧頂;當作用域執行完成之後,該作用域的資訊就會從棧頂彈出,這就是詞法環境的結構。(變量是指通過 let 或者 const 聲明的變量。)

當執行到作用域塊中的console.log(a)這行代碼時,就需要在詞法環境和變量環境中查找變量 a 的值了,具體查找方式是:

  1. 沿着詞法環境的棧頂向下查詢,如果在詞法環境中的某個塊中查找到了,就直接傳回給 JavaScript 引擎
  2. 如果沒有查找到,那麼繼續在變量環境中查找。

查找過程:

浏覽器原理 08 # 塊級作用域:var缺陷以及為什麼要引入let和const?

當作用域塊執行結束之後,其内部定義的變量就會從詞法環境的棧頂彈出

繼續閱讀