字數:2869
閱讀時間:10分鐘
最新的ECMAScript規範中,變數宣告有var、function、let、const、import、class六種方法。
var
語法:
var varname [= value1 [, vaname1[,valname2 ...]]];
對應如下程式碼:
var x;
var x=1;
var x=1,y=2;
var x=y,y=2; //x->undefined y->2
複製程式碼
我們需要注意該語法的兩個特性:塊級作用域和變數提升。
var語法宣告的變數作用域為當前上下文(可以簡化理解為當前變數被包裹的函式或頂級作用域),這裡要特別注意,程式碼塊並沒有建立新的上下文,所以 var 沒有塊級作用域的概念。我們在一個迴圈體或者判斷條件語句中使用的變數都會儲存到當前函式對應的上下文中。例:
function test () {
console.log(i);
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);
}
test();
console.log(i);
//輸出結果:undefined 0 1 2 3 4 5 6 7 8 9 10 ReferenceError
複製程式碼
在這個示例中,變數i
是在迴圈體中宣告的,但是我們在迴圈體外仍然可以訪問到它,而在函式之外就會報引用錯誤。說明,i
的作用域在整個test函式中。
我們再來看看,在test函式中,我們在變數宣告之前就使用了該變數並且可以正常執行,這就是變數提升。js解析器在解析js程式碼時,會先遍歷該函式中的所有變數宣告,凡是使用var語句宣告的變數,都會將他們的宣告語句提升到函式頂端,即上述程式碼正真執行的是如下程式碼:
function test () {
var i; //變數提升
console.log(i);
for (i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);
}
test();
console.log(i);
複製程式碼
理解以上兩點,就基本上理解了var的使用方式了。至於像i=10
不使用var語句,直接宣告一個變數這種寫法,實質就是在全域性物件上新增一個屬性而已。例如,在瀏覽器中,其實等同於window.i = 10;
。看到這樣的程式碼我們明白什麼意思就行了,我們自己就千萬不要寫這種魔法程式碼了。(在程式碼頂層通過var語法宣告一個變數,實質也是在全域性變數上新增一個屬性)
function
語法:
function name([param [,param1,...]]){
[statements]
}
函式宣告的效果同 var 語句。需要注意下文 let 介紹中的函式宣告。
let
MDN對let取名的解釋:
Let是一個數學宣告,是採用於早期的程式語言如Scheme和Basic。 變數被認為是不適合更高層次抽象的低階實體,因此許多語言設計者希望引入類似但更強大的概念, 如在Clojure、f#、Scala,let可能意味著一個值,或者一個變數可以賦值,但不能被更改, 這反過來使編譯器能夠捕獲更多的程式設計錯誤和優化程式碼更好。 javascript從一開始就有var,所以他們只是需要另一個關鍵字,並只是借用了其他數十種語言, 使用let已經作為一個傳統的儘可能接近var的關鍵字,雖然在javascript 中 let只建立塊範圍區域性變數而已。
語法:
let letname [ = letval[,letname1 = letval1[,...]]] ;
let語法與var語法使用方式完全一樣,主要區別在於作用域和變數提升兩個特性的不同。
首先,let語法不會進行變數提升,在宣告前使用變數會報引用錯誤。然後,let是塊級作用域,並非作用於整個封閉函式。舉個栗子:
//變數提升
console.log(valVal); //undefined
var valVal = 'valVal';
console.log(letVal); //ReferenceError
let letVal = 'letVal';
//作用域
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); //ReferenceError
複製程式碼
注意事項
①TDZ(temporal dead zone)臨時死鎖域
只要在塊級作用域中宣告瞭let變數,該變數就和當前作用域繫結,不再受到外面作用域的影響。見如下示例:
{
let testval = 'val';
{
let testval = 'val1';
console.log(testval); //val1
}
console.log(testval); //val
}
{
var testval = 'val';
{
var testval = 'val1';
console.log(testval); //val1
}
console.log(testval); //val1
}
複製程式碼
如上所示,當js程式碼解析到一個程式碼塊中時,會先檢測程式碼塊中的let語句,然後將檢測到的變數繫結到當前作用域中,所有對該變數的操作都會作用於繫結作用域上的變數,不會受外側作用域的影響。因此,雖然外側作用域宣告瞭testval變數,但程式碼解析到內層作用域會重新繫結內層的testval變數,對變數的賦值操作也僅僅作用於當前作用域對應的變數上。因此,內層列印的變數和外層列印的變數會不一致。上例的第二段程式碼演示了var語法的效果,它就沒有臨時死鎖域的機制。
因此,我們在使用如下程式碼時需要注意了:
{
let testval = 'val';
{
console.log(testval); //ReferenceError
let testval = 'val1';
}
function testFun (x = y, y = 1) {
}
testFun(); //y is not defined
}
複製程式碼
②重複宣告
let語法不允許重複宣告,測試程式碼如下:
//重複宣告
{
var testval = 'val';
var testval = 'val1';
console.log(testval); //val1
}
{
let testval = 'val1';
var testval = 'val'; //重複定義錯誤
console.log(testval);
}
{
var testval = 'val'; //重複定義錯誤
let testval = 'val1';
console.log(testval);
}
{
let testval = 'val';
let testval = 'val1'; //重複定義錯誤
console.log(testval);
}
複製程式碼
var語法支援重複宣告,但是一旦使用let語法,就不允許重複宣告瞭。上例中,雖然,第二個程式碼塊和第三個程式碼塊中var語句的順序不一致,但都是var語句報錯,這也是上述TDZ機制導致的。
函式引數也是一樣,不允許重複宣告,示例如下:
{
function testFun(arg){
let arg = 'arg'; //重複定義錯誤
}
testFun(arg);
}
複製程式碼
③switch語句
由於switch語句中,case語句並沒有建立一個作用域塊,所以也不能重複宣告變數:
//switch
{
let caseVal = '1';
switch (caseVal) {
case '1':
let val = '1';
break;
case '2':
let val = '2'; //重複定義錯誤
break;
default:
}
}
複製程式碼
這裡插播一下,判斷時候產生新的塊狀作用域的最簡單的方式就是是否有一對大括號包裹。如果有,就是一個新的塊作用域。
這時,有人就會問了,那如果我寫一個沒有大括號的條件語句會如何呢?看如下示例:
{
let testVal = '';
if(0) let testVal = '1'; //Lexical declaration cannot appear in a single-statement context
}
{
let testVal = '';
if(0) var testVal = '1'; //重複定義錯誤
}
{
var testVal = '';
if (0) var testVal = '1'; //正確執行
}
複製程式碼
那串英文的意思是:詞法宣告不能出現在單個語句的上下文中。簡單來說,就是在單行的條件語句中,不允許使用let表示式。
④typeof
因為TDZ的機制,導致之前絕對安全的 typeof 語句不再安全。舉個栗子:
console.log(typeof letval); //報變數未定義錯誤
let letval = 1;
複製程式碼
所以,我們在使用變數時,還是先申明再使用為好。
⑤函式宣告
ES5中規定,函式只能在頂層作用域或函式作用域中宣告,不能在塊級作用域中宣告。但是為了相容ES5之前的程式碼,瀏覽器並沒有遵循這條規範,仍然支援在塊級作用域中宣告函式。
到了ES6中,明確允許在塊級作用域中宣告函式。並且,函式的宣告預設是使用let語法的,即在塊級作用域之外,我們是無法訪問到該函式。舉個栗子:
{
function testFun () {
console.log('testFun');
}
}
testFun();
複製程式碼
按照規範,理應報函式未定義錯誤。但實際執行結果為正確輸出了 testFun 。
原來因為這條規則對之前的程式碼影響太大,所以ES6在附錄B中規定,瀏覽器的實現可以不遵循這條規定,可以有自己的行為方式。遵循以下兩條:
-允許在塊級作用域中宣告函式
-函式宣告類似var語法,有變數提升機制。
建議:
由於這是過度的解決方案,最終還是會向規範靠攏,建議儘量不要在塊狀作用域中宣告函式。如果必須要這麼做,則使用 let 語句來宣告函式。
常見用法
塊級作用域比之前的函式作用域更為規範和便利,所以建議可以完全使用 let 替代 var。舉個經典的栗子:
//var用法
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 100);
}(i));
}
//let用法
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
複製程式碼
之前我們想在一個迴圈語句中執行一個非同步的函式,只能通過一個匿名函式來人為製造一個新的作用域。使用 let 語法,它每次迴圈都是一個新的作用域,實現起來就非常簡單合理了。
再看一個栗子:
//var
var strName = 'iTurst';
function testFun () {
console.log(strName); //undefined
var strName = 'new name';
}
testFun();
//let
let strName = 'iTurst';
function testFun () {
console.log(strName); //變數未定義錯誤
let strName = 'new name';
}
testFun();
複製程式碼
這種情況,使用 var 語句的時候,不會報任何錯誤,但是執行結果卻不是我們預期的結果,這種機制也非常不合理。在 let 語句中,我們就明確地知道程式碼問題在哪了。
const
語法:
cons name1 = value1 [, name2 = value2 [...]];
const 語法特性與 let 完全一致,只是 const 必須在宣告時初始化,而後無法繼續修改其值。
需要注意,如果 const 語句初始化的值是一個物件,物件的屬性我們還是可以做修改的。所以,在這種情況下我們可以將物件做特殊的處理。
class
語法:
class name [extends] {
//class body
}
class語句特性:
-class 語法申明的變數其實就是一個函式,和ES5中建立一個類得到的結果基本一致(除了屬性不可列舉)。
-不做變數提升
-不可重複定義
class TestClass {
}
console.log(typeof (TestClass)); //function
複製程式碼
import
這個是ES6新引入的模組機制,可以在當前模組中匯入其他模組匯出的任何型別變數,可以是一個簡單型別,也可以是一個物件。作用域範圍是當前整個模組。
有關模組的內容,我後續會有單獨的文章來詳細講述。
總結
①宣告一個類,我們使用 class 最為合適。
②變數宣告我們酌情使用 let 和 const
③函式宣告使用 function 或者 let funName = function()
。
④模組匯入使用 import。
參考資料:
http://es6.ruanyifeng.com/#docs/let
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let
歡迎關注我的微信公眾號: