ES6之let(理解閉包)和const命令
最近做項目的過程中,使用到了ES6,因為之前很少接觸,所以使用起來還不夠熟悉。因此購買了阮一峰老師的ES6標准入門,在此感謝阮一峰老師的著作。
我們知道,ECMAScript 6即ES6是ECMAScript的第五個版本,因為在2015年6月正式發布,所以又成為ECMAScript2015。ES6的主要目的是為了是JS用於編寫復雜的大型應用程序,成為企業級的開發語言。
說明:由於有時候我們希望得知es6代碼的具體實現原理或者說希望能夠轉化為es5使用,我們可以使用http://babeljs.io/來實現在線將es6代碼轉化為es5代碼。
第一部分:let命令
一.塊級作用域(重點)。
我們知道,在javascript中只有全局作用域和函數作用域,並不存在塊級作用域。這樣,在使用時就會出現一些問題。 下面我們先來舉例說明let塊級作用域的使用。
例1:
代碼如下所示:
{ var a=5; let b=10; } console.log(a); console.log(b);
我們在控制台得到的結果如下所示:
也就是說,var聲明的變量由於不存在塊級作用域所以可以在全局環境中調用,而let聲明的變量由於存在塊級作用域所以不能在全局環境中調用。
例2:這個例子是一個非常經典的例子。
var a=[]; for(var i=0;i<10;i++){ a[i]=function(){ console.log(i); }; } a[6](); //10
var a=[]; for(let i=0;i<10;i++){ a[i]=function(){ console.log(i); }; } a[6](); //6
我們可以看到,兩個例子中,唯一的區別是前者for循環中使用var來定義i,得到的結果是10.而后者使用的是let來定義i,最終得到的結果是6.這是為什么呢?阮一峰老師在書中的解釋並不是很清楚,所以下面我會發表個人見解:
關於這個問題,表面上確實不是很好理解,查詢了很多資料,許多人講到了很多晦澀難懂的知識,似乎很高大上,但是實際上並不難,下面根據我的理解進行解釋,如有問題,歡迎批評指正,如果大家能夠有些收獲就再好不過了。
例二前者(var i)具體執行過程如下:
var a=[];
var i=0;//由於var來聲明變量i,所以for循環代碼塊不具備塊級作用域,因此i認為是全局變量,直接放在全局變量中。
a[0]=function(){
console.log(i);//這里之所以i為i而不是0;是因為我們只是定義了該函數,未被調用,所以沒有進入該函數執行環境,i當然不會沿着作用域鏈向上搜索找到i的值。
}// 由於不具備塊級作用域,所以該函數定義就是全局作用域。
var i=1;//第二次循環,這時var i=1;覆蓋了前面的var i=0;即現在i為1;
a[1]=function(){
console.log(i);//解釋同a[0]函數。
}
var i=2;// 第三次循環,這時 i=2,在全局作用域中,所以覆蓋了前面的i=1;
a[2]=function(){
console.log(i);
}
......第四次循環 此時i=3 這個以及下面的i不斷的覆蓋前面的i,因為都在全局作用域中
......第五次循環 此時i=4
......第六次循環 此時i=5
......第七次循環 此時i=6
......第八次循環 此時i=7
......第九次循環 此時i=8
var i=9;
a[9]=function(){
console.log(i);
}
var i=10;// 這時i為10,因為不滿足循環條件,所以停止循環。
緊接着在全局環境中繼續向下執行。
a[6]();//這時調用a[6]函數,所以這時隨即進入a[6]函數的執行環境,即a[6]=function(){console.log(i)};執行函數中的代碼 console.log(i); 因為在函數執行環境中不存在變量i,所以此時會沿着作用域鏈向上尋找(可參考我的博文《深入理解作用域和作用域鏈》),即進入了全局作用域中尋找變量i,而全局作用域中i=10覆蓋了前面所有的i值,所以說這時i為10,那么a[6]的值就是10了。
說明:對於例如a[1]=function(){console.log(i)};而不是a[1]=function{console.log(1)},可以在控制台中輸出a[1]函數,即可得到驗證。
例二后者(let i)具體執行過程如下:
var a=[];//創建一個數組a;
{ //進入第一次循環
let i=0; //注意:因為使用let使得for循環為塊級作用域,此次let i=0在這個塊級作用域中,而不是在全局環境中。
a[0]=function(){
console.log(i);
}; //注意:由於循環時,let聲明i,所以整個塊是塊級作用域,那么a[0]這個函數就成了一個閉包。
}// 聲明: 我這里用{}表達並不符合語法,只是希望通過它來說明let存在時,這個for循環塊是塊級作用域,而不是全局作用域。
講道理,上面這是一個塊級作用域,就像函數作用域一樣,函數執行完畢,其中的變量會被銷毀,但是因為這個代碼塊中存在一個閉包,閉包的作用域鏈中包含着(或着說是引用着)塊級作用域,所以在閉包被調用之前,這個塊級作用域內部的變量不會被銷毀。(更多閉包知識,可以看我的博文《JavaScript之閉包》)
{ //進入第二次循環
let i=1; //注意:因為let i=1; 和 上面的let i=0;出在不同的作用域中,所以兩者不會相互影響。
a[1]=function(){
console.log(i);
}; //同樣,這個a[i]也是一個閉包
}
......進入第三次循環,此時其中let i=2;
......進入第四次循環,此時其中let i=3;
......進入第五次循環,此時其中let i=4;
......進入第六次循環,此時其中let i=5;
......進入第七次循環,此時其中let i=6;
......進入第八次循環,此時其中let i=7;
......進入第九次循環,此時其中let i=8;
{//進入第十次循環
let i=9;
a[i]=function(){
console.log(i);
};//同樣,這個a[i]也是一個閉包
}
{
let i=10;//不符合條件,不再向下執行。於是這個代碼塊中不存在閉包,let i=10;在這次循環結束之后難逃厄運,隨即被銷毀。
}
a[6]();//調用a[6]()函數,這時執行環境隨即進入下面這個代碼塊中的執行環境:funcion(){console.log(i)};
{
let i=6;
a[6]=function(){
console.log(i);
}; //同樣,這個a[i]也是一個閉包
}
a[6]函數(閉包)這個執行環境中,它會首先尋找該執行環境中是否存在 i,沒有找到,就沿着作用域鏈繼續向上到了其所在的代碼塊執行環境,找到了i=6,於是輸出了6,即a[6]();的結果為6。這時,閉包被調用,所以整個代碼塊中的變量i和函數a[6]()被銷毀。
相信大家仔細看完上面的函數執行的過程,對let var 塊級作用域 閉包就有一個很好的理解了。我認為重要的是對於函數執行過程的理解!
二.不存在變量提升
這里是說使用let不會像使用var一樣存在一個變量提升的現象。變量提升是什么呢?在沒有接觸es6之前我對此也不清楚,但是我想大家一定都聽說過函數聲明提升:函數聲明來定義函數即可實現函數聲明提升,這樣,我們可以先調用函數,后聲明函數;而函數表達式方法不會實現函數聲明提升,這樣,如果先調用函數,后聲明函數,則會拋出錯誤!!(對於函數聲明提升更多知識可以看我的博文《JavaScript函數之美~》)。 那么可以以此類推,var定義變量:可以先使用,后聲明;而let定義變量:只可先聲明,后使用。
例3:
var num1=100; console.log(num1); let num2=200; console.log(num2); console.log(i); var i=10; console.log(j); let j=5;
我們可以看到結果如下:
即前兩個都是先聲明后使用,沒有問題。而后兩個都是先使用,后聲明,用var 聲明的顯示undefined,而 let聲明的直接報錯。
說明:console.log(i);
var i=10;
實際上相當於:
var i;
console.log(i);
i=10;
所以會出現undefined的情況。
三.暫時性死區
暫時性死區即:只要一進入當前作用域,所要使用的變量就已經存在,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。
例5:
var tmp=123; if(true){ tmp="abc"; let tmp; }
結果如下:
也就是說:雖然上面的代碼中存在全局變量tmp,但是塊級作用域內let又聲明了一個局部變量tmp,導致后者綁定了塊級作用域,所以在let聲明變量前,對tmp賦值會報錯。此即暫時性死區。
注意:ES6規定暫時性死區和不存在變量提升就是為了減少運行時的錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。
暫時性死區就是: 只要塊級作用域內存在let,那么他所聲明的變量就綁定了這個區域,不再受外部的影響。
暫時性死區即 Temperary Dead Zone,即TDZ。
注意:暫時性死區也意味着 typeof 不再是一個百分之百安全的操作。 如下:
if (true) { console.log(typeof x); let x; }
這里如果沒有let x,那么typeof x的結果是 undefined,但是如果使用了let x,因為let不存在變量提升,所以這里形成了暫時性死區,即typeof x也是會報錯的。。。 從這里可以理解暫時性死區實際上就是這一部分是有問題的 。
四.不允許重復聲明
function func (){ let b=100; var b=10; } function add(num){ let num; return num+1; } function another(){ let a=10; let a=5; }
上述三個得到的結果均為:
只是前兩者為 b和num被聲明過了。注意:第二個函數,雖然我們沒有明確的聲明,但是參數實際上是相當於用var聲明的局部變量。
第二部分:const命令
什么使const命令呢?實際上它也是一種聲明常量的方式。const命令用來聲明常量,一旦聲明,其值就不能改變。初次之外,const和let十分相似。也就是說前者是用於聲明常量的,后者是用於聲明變量的。
1.const聲明常量,一旦聲明,不可改變。
const a=10;
a=100;
結果如下
2.既然const一旦聲明不可改變,所以在聲明時必須初始化。
const a;
結果如下:
3.const所在的代碼塊為塊級作用域,所以其變量只在塊級作用域內使用或其中的閉包使用。
if(true){ const a=10; } console.log(a);
結果如下:
4.const聲明的變量不存在變量提升。
if(true){ console.log(a); const a=10; }
結果如下:
5.const不可重復聲明常量。
var a=10; const a=5;
結果如下:
6.const命令只是保證了變量名指向的地址不變,並不保證該地址的數據不變。
const a={}; a.name="zzw"; console.log(a.name); const b=[]; b.push("zzw"); console.log(b); const c={}; c={name:"zzw"};
結果如下:
因此,我們使用const所指向的地址不可變,但是地址的內容是可以變得。
7.如果希望將對象本身凍結,可以使用Object.freeze()方法。
const a=Object.freeze({}); a.name="zzw"; console.log(a.name); //undefined
於是通過Object.freeze()方法我們就不可以再改變對象的屬性了(無效)。