衆所周知,在ES6以前,聲明變量的關鍵字就只有var。var 聲明變量要麼是全局的,要麼是函數級的,而沒法是塊級的。node
var a=1; console.log(a); //1 console.log(window.a); //1
function test(){ var b=2; function print(){ console.log(a,b); } print(); } test(); //1 2 console.log(b); //Uncaught ReferenceError: b is not defined
for(var i=0;i<=10;i++){ var sum=0; sum+=i; } console.log(i); //11 console.log(sum); //10 聲明在for循環內部的i和sum,跳出for循環同樣可使用。
再來看看下面這個栗子:安全
HTML: <ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> </ul>
JS: window.onload = function(){ var aLi = document.getElementsByTagName('li'); for (var i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(i); }; }
這是一道很經典的筆試題,也是不少初學者常常犯錯並且找不到緣由的一段代碼。想要實現的效果是點擊不一樣的<li>標籤,alert出其對應的索引值,可是實際上代碼運行以後,咱們會發現無論點擊哪個<li>標籤,alert出的i都爲4。由於在執行for循環以後,i的值已經變成了4,等到點擊<li>標籤時,alert的i值是4。在ES6以前,大部分人會選擇使用閉包來解決這個問題,今天咱們使用ES6提供的let來解決這個問題。接下來就看看let的神奇吧。閉包
window.onload = function(){ var aLi = document.getElementsByTagName('li'); for (let i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(i); } }; }
有看出什麼區別嗎?奧祕就在for循環中var i=0變成了let i=0,咱們僅僅只改了一個關鍵字就解決了這個問題,還避免了使用閉包可能形成的內存泄漏等問題。函數
上述代碼中的for 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中, 事實上它將其從新綁定到了循環的每個迭代中, 確保使用上一個循環迭代結束時的值從新進行賦值。this
後面就讓咱們好好來了解一下let這個神奇的關鍵字吧。spa
let 關鍵字能夠將變量綁定到所在的任意做用域中(一般是 { .. } 內部)。換句話說,let爲其聲明的變量隱式地了所在的塊做用域。 ----《你所不知道的JavaScript(上)》P32code
上述代碼,能夠經過另外一種方式來講明每次迭代時進行從新綁定的行爲:
對象
window.onload = function(){ var aLi = document.getElementsByTagName('li'); for (let i=0;i<aLi.length;i++){ let j = i; aLi[j].onclick = function(){ alert(j); } }; }
在這裏還有個點要說明的,就是 for
循環還有一個特別之處,就是循環語句部分是一個父做用域,而循環體內部是一個單獨的子做用域。 blog
這就很好理解上面這段代碼的意思了。每次循環體執行的時候,let聲明的變量 j 會從父做用域(循環語句塊)取值保存到本身的塊級做用域內,因爲塊級做用域內的變量不受外部干擾,因此每次循環體生成的塊級做用域相互獨立,各自保存着各自的 j 值。遞歸
來看一下 let 和 var 的一些異同吧。
function varTest() { var x = 31; if (true) { var x = 71; // same variable! console.log(x); // 71 } console.log(x); // 71 } function letTest() { let x = 31; if (true) { let x = 71; // different variable console.log(x); // 71 } console.log(x); // 31 }
能夠看出在letTest函數的 if 判斷中從新聲明的x並不會影響到 if 代碼塊以外的代碼,而varTest函數中用var聲明的卻會。這是由於let聲明的變量只在代碼塊(一般是{ }所造成的代碼塊)中有效。
咱們都知道,var聲明的變量會有變量提高的做用,以下
console.log(a); //1 var a=1; console.log(b); //undefined var b;
能夠看出,雖然代碼中console調用a在前,聲明a在後,可是因爲在js中,函數及變量的聲明都將被提高到函數的最頂部,也就是說(var聲明的)變量能夠先使用再聲明。
而後,使用let,const(後面會說起)聲明的變量卻不存在變量提高。
console.log(foo); // Uncaught ReferenceError: foo is not defined let foo = 2; console.log(foo1); // Uncaught ReferenceError: foo1 is not defined let foo1;
ES6明確規定,若是區塊中存在let命令,這個區塊對這些命令聲明的變量,從一開始就造成了封閉做用域。凡是在聲明以前就使用這些變量,就會報錯。因此在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)。
總之,暫時性死區的本質就是,只要一進入當前做用域,所要使用的變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,才能夠獲取和使用該變量。
注:「暫時性死區」也意味着typeof再也不是一個百分之百安全的操做,由於會使typeof報錯。
if (true) { let aa; let aa; // Uncaught SyntaxError: Identifier 'aa' has already been declared } if (true) { var _aa; let _aa; // Uncaught SyntaxError: Identifier '_aa' has already been declared } if (true) { let aa_; var aa_; // Uncaught SyntaxError: Identifier 'aa_' has already been declared }
let不容許在相同做用域內,重複聲明同一個變量。
ES5中全局對象的屬性與全局變量基本是等價的,可是也有區別,好比經過var聲明的全局變量不能使用delete從 window/global ( global是針對與node環境)上刪除,不過在變量的訪問上基本等價。
ES6 中作了嚴格的區分,使用 var 和 function 聲明的全局變量依舊做爲全局對象的屬性,使用 let
, const
命令聲明的全局變量不屬於全局對象的屬性。
let let_test = 'test'; console.log(window.let_test); // undefined console.log(this.let_test); // undefined var var_test = 'test'; console.log(window.var_test); // test console.log(this.var_test); // test
除了let之外,ES6還引入了const,一樣能夠用來建立塊做用域變量,但其值是固定的(常量)。使用const聲明變量的時候,必須同時賦值,不然會報錯。而且以後任何試圖修改值的操做都會引發錯誤.
const data; //Uncaught SyntaxError: Missing initializer in const declaration
if (true) { var a = 2; const b = 3; // 包含在 if 中的塊做用域常量 a = 3; // 正常 ! b = 4; // Uncaught TypeError: Assignment to constant variable. } console.log( a ); // 3 console.log( b ); // Uncaught ReferenceError: b is not defined
注:複合類型const變量保存的是引用。由於複合類型的常量不指向數據,而是指向數據(heap)所在的地址(stack),因此經過 const 聲明的複合類型只能保證其地址引用不變,但不能保證其數據不變。
const arr= [1, 2]; // 修改數據而不修改引用地址,正確執行 arr.push(3); // [1, 2, 3] // 修改 arr 常量所保存的地址的值,報錯 arr = []; // Uncaught TypeError: Assignment to constant variable.
簡單的使用const沒法完成對象的凍結。能夠經過Object.freeze()方法實現對對象的凍結。使用Object.freeze()方法返回的對象將不能對其屬性進行配置(definedProperty()不可用)同時不能添加新的屬性和移除(remove)已有屬性。完全凍結對象時須要遞歸的對它的對象屬性進行凍結。
let obj = { a: 1, b: { b1: 2 } }; obj.b.b1 = 3; console.log(obj.b.b1 ); //3 function freeze(obj){ Object.freeze(obj); Object.values(obj).forEach(function (value,index) { if(typeof value === 'object'){ freeze(value); } }) } freeze(obj); obj.b.b1 = 4; console.log(obj.b.b1); //3
塊級做用域的出現,讓普遍使用的 IIFE (當即執行匿名函數)再也不必要。
// 匿名函數寫法 (function () { var jQuery = function() {}; // ... window.$ = jQuery })(); // 塊級做用域寫法 { let jQuery = function() {}; // ... window.$ = jQuery; }
附:在ES6以前,關鍵字with和關鍵字try/catch都會建立相關的塊級做用域。關鍵字with已經不推薦使用了,咱們在這裏就很少描述。在ES3規範中規定try/catch的catch分句會建立一個塊做用域,其中聲明的變量僅在catch內部有效。
try { undefined(); // 執行一個非法操做來強制製造一個異常 } catch (err) { console.log( err ); // 可以正常執行! } console.log( err ); // Uncaught ReferenceError: err is not defined