【ES6基礎】let和作用域

前端達人發表於2019-01-20

ECMAScript是一種由Ecma國際(前身為歐洲計算機制造商協會)以JavaScript為基礎制定的一種指令碼語言標準。目前,該標準基本上每年釋出一次新的ES規範標準,目前最新的標準是ECMAScript 2018(ES9),由於前端開發的應用場景日益複雜,自從費時六年之久ES6(ECMAScript 2015)的出現,增加了很多新的特性,讓JavaScript語言更加標準化和工程化。因此我們有必要重新學習JavaScript,這樣才能適應前端日新月異的發展。

從今天開始,小編將會介紹ES6及以後的相關內容,為了便於理解和學習,每篇文章儘量簡短。本篇文章小編將帶著大家一起學習如何使用新的語法let宣告變數。

本篇文章閱讀時間預計10分鐘。

你將會學到以下內容:

  • let基本介紹
  • 作用域介紹
    • 作用域
    • 全域性作用域和函式作用域
    • 塊級作用域
  • var和let的區別
  • 重複定義變數的問題
  • 提升概念的問題

let介紹

ES6引入了let,用let宣告變數,解決了JavaScript沒有塊級作用域的問題(注:ES3的catch分句會產生塊作用域)。

有其它語言背景的比如JAVA,C#開發者來說,這個概念並不難以理解,反而ES6之前,JavaScript沒有塊級作用域,對於新手而言,使用var宣告變數,會讓JavaScript不易懂和難以除錯,用不好,甚至會有記憶體洩露的可能性。為什麼會這樣,主要是沒有清楚作用域的概念,接下來我們首先了解下什麼是作用域。

作用域

作用域簡單的來說,就是一套尋找變數的規則,用於確定在何處以及如何查詢變數。說直白點:這些變數在哪裡?它們儲存在哪裡?編譯器如何找到它們?ES6程式碼之前,只有全域性作用域或函式作用域。

當一個塊或函式巢狀在另一個函式時,就發生了作用域巢狀。如圖所示,就有三個巢狀作用域:

【ES6基礎】let和作用域
  1. 全域性作用域,其中有一個識別符號:foo(整個綠色區域)
  2. foo建立的函式作用域,其中有三個識別符號:a,bar和b(整個黃色區域)
  3. bar建立的函式作用域,其中有一個識別符號:c(藍色區域)

如何在巢狀作用域中尋找變數呢:引擎從當前作用域開始查詢變數,如果找不到,就會向上一級繼續查詢。當抵達最外層全域性作用域時,無論找到還是沒有找到,查詢過程中都會停止。

全域性作用域和函式作用域

如何理解全域性作用域和函式作用域呢,使用var宣告變數時,如果在函式外宣告,就是全域性變數,任何函式都可以進行使用,這就是全域性作用域查詢。如果在函式內使用var宣告變數,就是函式作用域查詢,只能在函式內部進行訪問,外部不能進行訪問,如下段程式碼所示:

var a = 12; // 全域性作用域都能訪問
 function myFunction() {
   console.log(a); // alerts 12
   var b = 13;
   if(true) {
     var c = 14; // 函式內部可以訪問
     alert(b); // alerts 13
   }
   alert(c); // alerts 14
 }
 myFunction();
 alert(b); // alerts undefined複製程式碼

為什麼b變數訪問不到?因為變數b是在函式內進行宣告的,因此函式執行完後,由於垃圾資料回收機制的存在,引擎認為函式執行完了,變數應該進行銷燬。

如果你在函式內忘記寫了b標識前忘記寫了var,引擎就會自作聰明,在函式外全域性作用域為你自動宣告變數b,這樣在函式外就能訪問b變數了(全域性作用域)。

因此使用var進行宣告時,如果一不小心,你就會宣告一個全域性作用域的變數,更糟糕的情況還有可能汙染一個同名的變數,因此產生的BUG就很難查詢。

以下這個例子會更加明顯,也是開發者經常會出現的問題,i變數會繫結到外部作用域(函式或全域性作用域),汙染整個外部作用域:

for(var i=0;i<10;i++){
	console.log(i); //依次輸出1到9
}
console.log(i);//10複製程式碼

塊級作用域

幸好es6引入了let,避免了有var宣告變數的一些問題,讓變數和函式不僅可以屬於所處的作用域,也可以屬於某個程式碼塊(通常是{...}內部),有一點需要強調,在塊級作用域定義的變數,塊級作用域外是無法訪問的,如下段程式碼所示:

let a = 12; // 全域性作用域,可以訪問
function myFunction() {
   console.log(a); // alerts 12
   let b = 13;
   if(true) {
     let c = 14; // this is NOT accessible throughout the function!
     alert(b); // alerts 13
   }
   alert(c); // alerts undefined  {}外,因此無法訪問
 }
 myFunction();
 alert(b); // alerts undefined  {}外,因此無法訪問複製程式碼

在for迴圈體,使用var和let的區別更加明顯,一個是在全域性作用域進行查詢變數,一個是在塊級作用域查詢變數,塊級作用域每一次執行都會產生一個作用域。首先在for迴圈裡,使用var宣告變數,如下段程式碼所示:

for(var i=0;i<5;i++){
	setTimeout(function() {
		console.log(i); 
	}, 1000);
}
// 輸出 5 5 5 5 5複製程式碼

由於JavaScript是單執行緒,事件迴圈機制的存在(不太瞭解事件迴圈機制的,大家可以檢視《JavaScript基礎——你真的清楚JavaScript是什麼嗎?》),主執行緒執行for迴圈後,才會執行SetTimeOut裡的函式,由於使用var宣告的變數,作用域會繫結for迴圈的上一層作用域,由於for迴圈執行完後,i的變數自然就等於5,因此setTimeOut在執行內部函式時,查詢i變數的值,才會輸出5。如圖所示變數尋找路徑:

【ES6基礎】let和作用域

將var替換let,將會輸出什麼結果,如下段程式碼所示:

for(let i=0;i<5;i++){
	setTimeout(function() {
		console.log(i);
	}, 1000);
}
// 輸出 0,1,2,3,4複製程式碼

由於塊級作用域的存在,每次迴圈,就會產生一個迴圈體塊級作用域,因此才會達到預期的輸出。如圖所示變數尋找路徑:

【ES6基礎】let和作用域

var和let的比較

對比項

let

var

宣告變數

可以被釋放

可以被提升

重複定義檢查

可被用於塊狀作用域

重複定義變數問題

用var在同一個作用域重複定義變數,後者將會覆蓋前者宣告的變數的值,如下段程式碼所示:

var a = 0;
var a = 1;
alert(a); // alerts 1
function myFunction() {
 var b = 2;
 var b = 3;
 alert(b); // alerts 3
}
myFunction();複製程式碼

使用let在同一作用域下重複定義變數,將會產生SyntaxError的錯誤,如下段程式碼所示:

let a = 0;
let a = 1; // SyntaxError
function myFunction() {
 let b = 2;
 let b = 3; // SyntaxError
 if(true) {
	let c = 4;
	let c = 5; // SyntaxError
 }
}
myFunction();複製程式碼

如果你在巢狀作用域裡進行重新定義變數,雖然變數名相同,但是不是同一變數,如下段程式碼所示:

var a = 1;
let b = 2;
function myFunction() {
	var a = 3; // different variable
	let b = 4; // different variable
	if(true) {
	  var a = 5; // overwritten
	  let b = 6; // different variable
	  console.log(a); // 5
	  console.log(b); // 6
}
	  console.log(a); // 5
	  console.log(b); // 4
}
myFunction();
console.log(a);
console.log(b);複製程式碼

提升概念的問題

初學JavaScript的同學,直覺上會認為編譯器會由上到下一行行的執行,其實並不正確,函式宣告和變數宣告都會被提升(使用var宣告變數,let宣告變數將不會被提升)。函式首先會被提升,然後才是變數提升。

首先我們看下段函式提升的程式碼:

bookName("ES8 Concepts");
function bookName(name) {
	console.log("I'm reading " + name);//I'm reading ES8 Concepts
}複製程式碼

正常輸出,由於函式會提升至執行語句前。在來看以下程式碼,使用變數的方式宣告函式:

bookName("ES8 Concepts"); //TypeError: bookName is not a function
var bookName = function(name) {
	console.log("I'm reading " + name);
}複製程式碼

為什麼會這樣呢,JavaScript引擎只會先提升函式,在提升變數宣告,引擎將會對上述程式碼這樣調整,程式碼如下:

var bookName; // 變數宣告提升至最上面
bookName("ES8 Concepts"); // bookName is not function 
// because bookName is undefined
bookName = function(name) { // 變數賦值不會被提升
	console.log("I'm reading " + name);
}複製程式碼

如果使用let替換var宣告函式呢?將會有什麼提示輸出呢?如下段程式碼所示:

bookName("ES8 Concepts"); // ReferenceError: bookName is not defined
let bookName = function(name) {
	console.log("I'm reading " + name);
}複製程式碼

從中可以看出,使用let宣告的變數將不會產生變數宣告提升。這樣的好處就是,讓我們更好的按照由上到下的常規方式書寫程式碼,儘量避擴音升問題產生的難以查詢的問題。

小節

今天的文章就到這裡,從中我們可以看出let可以說是var的進化版,為了避免產生奇奇怪怪的問題,讓我們能按照大多數高階語言書寫程式碼的思維方式,在絕大部分情況下,我們應該使用let宣告變數,讓我們的程式碼更加易於維護和使用。

本文部分內容參:《你不知道的JavaScript》

更多精彩內容,請微信關注”前端達人”公眾號!

【ES6基礎】let和作用域


相關文章