javascript ES6 新特性之 let
let的作用是聲明變量,和var差不多。
let是ES6提出的,在了解let之前,最好先熟悉var的原理。
JavaScript有一個機制叫“預解析”,也可以叫“提升(Hoisting)機制”。很多剛接觸JavaScript的人都會被這個機制弄混。比如:
// var 的情況 console.log(a); // 輸出undefined var a = 2;
在預編譯階段,JavaScript引擎會將上面的a函數修改成下面的寫法:
var a; //聲明且初始化為undefined console.log(a); a=2;
我們把上面的 var 變成 let ;
// let 的情況 console.log(a); // 報錯ReferenceError let a = 2; //相當於在第一行先聲明bar但沒有初始化,直到賦值時才初始化
由此我們得出:
變量提升現象:瀏覽器在運行代碼之前會進行預解析,首先解析函數聲明,定義變量,解析完之後再對函數、變量進行運行、賦值等。
-不論var聲明的變量處於當前作用域的第幾行,都會提升到作用域的頭部。
-var 聲明的變量會被提升到作用域的頂部並初始化為undefined,而let聲明的變量在作用域的頂部未被初始化
在ES6中對塊級作用域做了進一步強化,從而使變量在生命周期內能被更好的控制。
塊級聲明用於聲明在指定塊的作用域之外無法訪問的變量。
“塊級作用域”也可以稱為“詞法作用域”。
- 塊級作用域存在於
- 函數內部
塊中(字符 { 和 } 之間的區域)
比如 if 和 for 在ES6中也被定義成一個塊級。
let聲明的用法與var相同,用let代替var來聲明變量,就可以把變量的作用域限制在當前代碼塊中。
而且let聲明不會被提升(在預解析的過程中,不會把聲明變量放在所有代碼的之前),因此開發者通常會將let聲明語句放在封閉代碼塊的頂部,以便整個代碼塊都可以訪問。
var a = 123; if (true) { a = 456; // ReferenceError let a; } console.log(a); //輸出值為123,全局 a 與局部 a 不影響
上面這小段代碼,先聲明全局變量 a = 123; 按照我們以往的思維,如果if 判斷語句中沒有 let a;則最後會輸出 456;但是if 判斷語句作為塊作用域,內部在未聲明變量的時候直接給 a 賦值為 456;因此會報錯。
由此可見,用let來聲明變量比var更嚴緊。
let的另一個特性是禁止在同一個作用域下重復聲明。
var a = 10; let a = 20; // 拋出語法錯誤 // Uncaught SyntaxError: Identifier ‘a‘ has already been declared // 很直接的告訴開發者變量a已經被定義過了。
不管之前用var還是let聲明,只要後面再重復聲明同一個變量,都會報錯。
let a = 10; var a = 20; // 也會報錯
上面是的報錯是因為在同一個作用域下,用let重復聲明。
熟悉JavaScript的開發者都知道,var是可以重復聲明的,而後面聲明的操作會覆蓋前面的聲明。
如果不在同一個作用域下,是可以用let來重復聲明相同名的變量。
let a = 30; if (true) { let a = 40; console.log(a); // 輸出40 } console.log(a); // 輸出30
同時 let 還有一個功能是防止變量泄露,
用var聲明
for (var i=0; i<10; i++) {} console.log(i); // 輸出 10
用let聲明
for (let i=0; i<10; i++) {} console.log(i); // 拋出錯誤:Uncaught ReferenceError: i is not defined
最後我們總結出:let
- 在同一個作用域下,不可以被重復聲明
- 可以重新賦值
- 可以防止變量泄露
接下來我們看一個 var 和 let 的實戰練習:
我們先看一個正常的for循環,普通函數裏面有一個for循環,for循環結束後最終返回結果數組
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = i; } return arr; } console.log(foo()) //輸出結果為 [0,1,2,3,4]
有時我們需要在for循環裏面添加一個匿名函數來實現更多功能,看下面代碼
//循環裏面包含閉包函數
function foo(){
var arr = [];
for(var i=0;i<5;i++){
arr[i] = function(){
return i;
}
}
return arr;
}
console.log(foo()); //執行5次匿名函數本身 --> [ [Function], [Function], [Function], [Function], [Function] ]
console.log(foo()[1]); //執行第2個匿名函數本身 --> [Function]
console.log(foo().length); //最終返回的是一個數組,數組的長度為5 --> 5
console.log(foo()[0]()); //數組中的第一個數返回的是5 --> 5
上面這段代碼就形成了一個閉包:
閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的常見的方式,就是在一個函數內部創建另一個函數,通過另一個函數訪問這個函數的局部變量。
在for循環裏面的匿名函數執行 return i 語句的時候,由於匿名函數裏面沒有i這個變量,所以這個i他要從父級函數中尋找i,而父級函數中的i在for循環中,當找到這個i的時候,是for循環完畢的i,也就是5,所以這個 foo 得到的是一個數組[5,5,5,5,5]。
那我們怎麽才能輸出 [1,2,3,4,5],在上述中我們說了在閉包內部函數調用 i 時沒有找到轉而向上層父級去找,那我們在調用內部函數時將值傳給內部函數不就可以不需要去父級找了,在 JavaScript 函數中有有匿名函數的自我執行,即 在函數體外面加一對圓括號,形成一個表達式,在圓括號後面再加一個圓括號,裏面可傳入參數。
(function(){ console.log(123); })();
//輸出 123
var a = 123;
(function(b){
console.log(b);
})(a);
//輸出 123
根據上面的執行結果我們可以將目標函數改造如下:
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = (function(num){ return num; })(i); } return arr; } console.log(foo()); // [ 0, 1, 2, 3, 4 ] console.log(foo()[1]); // 1 console.log(foo().length); // 5 console.log(foo()[0]); // 0
這樣在每次調用 foo 的時候內部的匿名函數都會自我執行,並且將 i 傳入匿名函數進行返回。
我們現在來看下面的代碼:
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = function(){ return i; } } return arr; } console.log(foo()[0]()); // 5
function foo(){
var arr = [];
for(let i=0;i<5;i++){
arr[i] = function(){
return i;
}
}
return arr;
}
console.log(foo()[0]()); // 0
在上面的代碼中,我們分別用 var 和 let 來聲明 i 的值,獲得的結果卻是不一樣的,當為 var 時我們在上面的時候已經解釋過了,但是為什麽當 var 換為 let 之後會變呢,只是由於使用 let 聲明塊級變量,這樣每次循環時就會在自己的作用域內找 i 的變量,而不是去全局找 i 的變量。
我們再來看一道面試時會經常問到的題目:
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); },1000); } // 5,5,5,5,5 for(let i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); },1000); } // 0,1,2,3,4
在setTimeout的時候,匿名函數function(){console.log(i);}會被聲明創建,當匿名函數執行的時候,會查找當前運行環境的 i 的值。
var聲明的 i ,運行環境的 i 的值為5,但是let聲明的 i,運行環境中 i 的值是每一個循環創建匿名函數時候的 i。
所以得到了0-4的值。
let替換var,可以很好的解決閉包的問題。
javascript ES6 新特性之 let