1. 簡介
在ES6以前,變量的聲明都是使用var關(guān)鍵字,且會(huì)進(jìn)行變量聲明提升。另外,我們曾經(jīng)講過,JS中是沒有塊級(jí)作用域的,這一點(diǎn)也帶來了很多的不便。ES6 新增了let和var兩個(gè)關(guān)鍵字,用來聲明變量。下面我們就來看看他們的用法。
2. let
我們來看下面一段代碼。
function f() {
var a = 1;
}
{
var b = 2;
}
// console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 2
當(dāng)變量a在函數(shù)f內(nèi)使用var聲明時(shí),在全局無法直接引用該變量。但是在全局函數(shù)內(nèi)用一對(duì)花括號(hào)包裹的區(qū)域中生命的變量b,卻可以在全局中直接引用。因?yàn)閷?duì)于JS來講,是沒有塊作用域的。這一點(diǎn)和JAVA等語言有著很大的不同,也帶來了很多不便。舉一個(gè)簡單的例子:
var i = 1;
... // 一堆其他的操作
for (var i = 0;i<3;i++) {
console.log(i); // 0 1 2
}
console.log(i); // 3
在for循環(huán)內(nèi)部生命的變量i其實(shí)是無效的,因?yàn)樵谕瑐€(gè)作用域(此處是全局作用域)已經(jīng)聲明過變量i。此時(shí)var i = 3;中的聲明會(huì)被忽略,而只保留i =3;(這塊內(nèi)容可以參考我的文章JS入門難點(diǎn)解析3-作用域)。所以,在for循環(huán)結(jié)束以后,你滿心以為會(huì)輸出最開始在全局聲明賦值的var i = 1;時(shí),結(jié)果卻是被循環(huán)改變的結(jié)果3。這明顯不是我們希望的結(jié)果,那么js中能否也使用塊級(jí)作用域呢,我們生命的變量可否只在塊級(jí)作用域中生效呢?ES6給我們提供了let??聪旅娲a:
let i = 1;
... // 一堆其他的操作
for (let i = 0;i<3;i++) {
console.log(i); // 0 1 2
}
console.log(i); // 1
將for循環(huán)中的var改成let,其聲明的變量i就只在循環(huán)內(nèi)生效了。是不是更加靈活方便了呢?當(dāng)然,let使用時(shí)有些需要注意的地方。
2.1 不存在變量提升
var命令會(huì)發(fā)生”變量提升“現(xiàn)象,即變量可以在聲明之前使用,值為undefined。(可以參考我的文章 JS入門難點(diǎn)解析2-JS的變量提升和函數(shù)提升)這種現(xiàn)象多多少少是有些奇怪的,按照一般的邏輯,變量應(yīng)該在聲明語句之后才可以使用。
為了糾正這種現(xiàn)象,let命令改變了語法行為,它所聲明的變量一定要在聲明后使用,否則報(bào)錯(cuò)。
// var 的情況
console.log(foo); // 2
var foo = 2;
// let 的情況
console.log(bar); // Uncaught ReferenceError: bar is not defined
let bar = 2;
2.2 暫時(shí)性死區(qū)
我們之前講到過,let沒有變量提升,也就是說在let聲明一個(gè)變量之前對(duì)其引用會(huì)報(bào)錯(cuò)。這個(gè)很好理解。但如果此時(shí)該變量在塊作用域外部也被聲明了呢?是否此時(shí)的引用是對(duì)外部該變量的引用呢?
看下面這段代碼:
var tmp = 123;
if (true) {
tmp = 'abc'; // Uncaught ReferenceError: tmp is not defined
let tmp;
}
這里,tmp = 'abc';一句會(huì)報(bào)錯(cuò)。也就是說,let不僅不允許其聲明的變量在其聲明前被引用,還不允許其引用外部的同名變量,相當(dāng)?shù)匕缘?。其?shí),ES6 明確規(guī)定,如果區(qū)塊中存在let和const命令,這個(gè)區(qū)塊對(duì)這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會(huì)報(bào)錯(cuò)。
在代碼塊內(nèi),使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時(shí)性死區(qū)”(temporal dead zone,簡稱 TDZ)。
if (true) {
// TDZ開始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ結(jié)束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
需要注意,TDZ有時(shí)會(huì)導(dǎo)致代碼出錯(cuò)。比如:
曾經(jīng)安全的typeof可能不在安全:
typeof y; // 未被let鎖定,輸出undefined
typeof x; // 被let鎖定,報(bào)ReferenceError
let x;
另外,有些TDZ導(dǎo)致的錯(cuò)誤會(huì)十分隱晦:
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 報(bào)錯(cuò)
改成
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
就okay了。還有如下情況:
// 不報(bào)錯(cuò)
var x = x;
// 報(bào)錯(cuò)
let x = x;
// ReferenceError: x is not defined
ES6 規(guī)定暫時(shí)性死區(qū)和let、const語句不出現(xiàn)變量提升,主要是為了減少運(yùn)行時(shí)錯(cuò)誤,防止在變量聲明前就使用這個(gè)變量,從而導(dǎo)致意料之外的行為。這樣的錯(cuò)誤在 ES5 是很常見的,現(xiàn)在有了這種規(guī)定,避免此類錯(cuò)誤就很容易了。
總之,暫時(shí)性死區(qū)的本質(zhì)就是,只要一進(jìn)入當(dāng)前作用域,所要使用的變量就已經(jīng)存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現(xiàn),才可以獲取和使用該變量。
2.3 不允許重復(fù)聲明
let不允許在相同作用域內(nèi)(指的是ES5中規(guī)定的作用域,不包含塊級(jí)作用域)重復(fù)聲明一個(gè)變量,不管是使用var還是let。
var a = 1;
let a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared
以及
let b = 2;
var b = 1;
console.log(b); // Uncaught SyntaxError: Identifier 'b' has already been declared
還有
let c = 1;
let c = 2;
console.log(c); // Uncaught SyntaxError: Identifier 'c' has already been declared
另外看下邊兩組代碼。
let a = 1;
if (true) {
var a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared
}
let a = 1;
function f(){
var a = 2;
console.log(a); // 2
}
f();
還有一點(diǎn),需要注意:
考慮到環(huán)境導(dǎo)致的行為差異太大,應(yīng)該避免在塊級(jí)作用域內(nèi)聲明函數(shù)。如果確實(shí)需要,也應(yīng)該寫成函數(shù)表達(dá)式,而不是函數(shù)聲明語句。
3. const
const的作用很簡單。const聲明一個(gè)只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
在代碼中,我們將長會(huì)將一些常量用一些有實(shí)際意義的名稱去命名。比如上面代碼段中的圓周率PI。
const聲明的變量不得改變值,這意味著,const一旦聲明變量,就必須立即初始化,不能留到以后賦值。對(duì)于const來說,只聲明不賦值,就會(huì)報(bào)錯(cuò)。
const foo;
// SyntaxError: Missing initializer in const declaration
const的作用域與let命令相同:只在聲明所在的塊級(jí)作用域內(nèi)有效,其聲明的常量也是不提升,同樣存在暫時(shí)性死區(qū),只能在聲明的位置后面使用。
需要注意的是,const實(shí)際上保證的,并不是變量的值不得改動(dòng),而是變量指向的那個(gè)內(nèi)存地址不得改動(dòng)。對(duì)于簡單類型的數(shù)據(jù)(數(shù)值、字符串、布爾值),值就保存在變量指向的那個(gè)內(nèi)存地址,因此等同于常量。但對(duì)于復(fù)合類型的數(shù)據(jù)(主要是對(duì)象和數(shù)組),變量指向的內(nèi)存地址,保存的只是一個(gè)指針,const只能保證這個(gè)指針是固定的,至于它指向的數(shù)據(jù)結(jié)構(gòu)是不是可變的,就完全不能控制了。因此,將一個(gè)對(duì)象聲明為常量必須非常小心。
如下:
const a = [];
a.push('Hello'); // 可執(zhí)行
a.length = 0; // 可執(zhí)行
a = ['Dave']; // 報(bào)錯(cuò)
上面代碼中,常量a是一個(gè)數(shù)組,這個(gè)數(shù)組本身是可寫的,但是如果將另一個(gè)數(shù)組賦值給a,就會(huì)報(bào)錯(cuò)。
如果真的想將對(duì)象凍結(jié),應(yīng)該使用Object.freeze方法。(可以參考我的文章 JS入門難點(diǎn)解析13-屬性描述符,數(shù)據(jù)屬性和訪問器屬性)
const foo = Object.freeze({});
// 常規(guī)模式時(shí),下面一行不起作用;
// 嚴(yán)格模式時(shí),該行會(huì)報(bào)錯(cuò)
foo.prop = 123;
除了將對(duì)象本身凍結(jié),對(duì)象的屬性也應(yīng)該凍結(jié)。下面是一個(gè)將對(duì)象徹底凍結(jié)的函數(shù)。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};