let和const命令
let
ES6新增了變量let來解決之前塊級作用域的問題。let所聲明的變量只會在代碼塊內有效。
可以看一下下面這個例子:
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
第一組代碼中的var是全局的,所有的i都指向一個i,所以會是10,但是在第二組代碼中。i由let聲明,只在塊級作用域里有效,所以輸出了6。
對于for循環(huán),有特別之處,設置循環(huán)的那部分是父作用域,而循環(huán)體內部是單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
由代碼可知,兩個i不一樣
不存在變量提升
使用var會導致變量提升,變量在聲明前被使用,不會報錯,只會輸出undefined。但是使用let之后再聲明之前使用變量會直接報錯。
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;
// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
暫時性死區(qū)(TDZ)
如果你在一個塊級作用域中使用了let,它所聲明的變量就在這個區(qū)域內不受外部的影響。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
在該塊級作用域中,tmp使用了let就被綁死了,在該區(qū)域未聲明tmp之前使用tmp就會報錯,而跟全局的tmp沒有什么關系。
ES6明確規(guī)定,如果區(qū)塊中存在let和const命令,這個區(qū)塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
總之,在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時性死區(qū)”(temporal dead zone,簡稱 TDZ)。
這個特性的出現,導致typeof操作不是百分之百安全的了。
# 現在
typeof x; // ReferenceError
let x;
# 之前
typeof x; // "undefined"
var x;
上面代碼中,變量x使用let命令聲明,所以在聲明之前,都屬于x的“死區(qū)”,只要用到該變量就會報錯。因此,typeof運行時就會拋出一個ReferenceError。
作為比較,如果一個變量根本沒有被聲明,使用typeof反而不會報錯。
ES6 規(guī)定暫時性死區(qū)和let、const語句不出現變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。這樣的錯誤在 ES5 是很常見的,現在有了這種規(guī)定,避免此類錯誤就很容易了。
總之,暫時性死區(qū)的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。
不允許重復聲明
let不允許在相同作用域內,重復聲明同一個變量。
塊級作用域
let實際是位js新增了塊級作用域。
- 允許塊級作用域的任意嵌套
- 外層作用域無法讀取內層作用域的變量
- 內層作用域可以定義外層作用域的同名變量
- 塊級作用域的出現,實際上使得獲得廣泛應用的立即執(zhí)行函數表達式(IIFE)不再必要了
塊級作用域和函數聲明
ES5中,以下情況是非法的:
// 情況一
if (true) {
function f() {}
}
// 情況二
try {
function f() {}
} catch(e) {
// ...
}
但是,瀏覽器沒有遵守這個規(guī)定,為了兼容以前的舊代碼,還是支持在塊級作用域之中聲明函數,因此上面兩種情況實際都能運行,不會報錯。
ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數。ES6 規(guī)定,塊級作用域之中,函數聲明語句的行為類似于let,在塊級作用域之外不可引用。
考慮到環(huán)境導致的行為差異太大,應該避免在塊級作用域內聲明函數。如果確實需要,也應該寫成函數表達式,而不是函數聲明語句。
// 函數聲明語句
{
let a = 'secret';
function f() {
return a;
}
}
// 函數表達式
{
let a = 'secret';
let f = function () {
return a;
};
}
另外,還有一個需要注意的地方。ES6 的塊級作用域允許聲明函數的規(guī)則,只在使用大括號的情況下成立,如果沒有使用大括號,就會報錯。
const
const聲明一個只讀的常量。一旦聲明,常量的值就不能改變了。const聲明的變量不得改變值,這意味著,const一旦聲明變量,就必須立即初始化,不能留到以后賦值,如果你只聲明而不賦值,就會發(fā)生錯誤。
const聲明的常量也存在暫時性死區(qū),只能在聲明的位置后面使用;也不能重復聲明。
本質
const實際上保證的,并不是變量的值不得改動,而是指向的內存地址不能改動。這邊就會涉及到說,把對象變成const的,那么只能保證指向對象的指針是不變的,至于它的數據結構是不是可變是不可以空數字的,所以要很小心。
const foo = {};
// 為 foo 添加一個屬性,可以成功
foo.prop = 123;
foo.prop // 123
// 將 foo 指向另一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only
const a = [];
a.push('Hello'); // 可執(zhí)行
a.length = 0; // 可執(zhí)行
a = ['Dave']; // 報錯
如果你真的想把對象鎖起來,可以使用Object.freeze方法。但是只在嚴格模式下會報錯,常規(guī)模式下,只是不起作用。
除了將對象本身凍結,對象的屬性也應該凍結。下面是一個將對象徹底凍結的函數:
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
頂層對象的屬性
頂層對象的屬性與全局變量掛鉤,被認為是JavaScript語言最大的設計敗筆之一。這樣的設計帶來了幾個很大的問題,首先是沒法在編譯時就報出變量未聲明的錯誤,只有運行時才能知道(因為全局變量可能是頂層對象的屬性創(chuàng)造的,而屬性的創(chuàng)造是動態(tài)的);其次,程序員很容易不知不覺地就創(chuàng)建了全局變量(比如打字出錯);最后,頂層對象的屬性是到處可以讀寫的,這非常不利于模塊化編程。另一方面,window對象有實體含義,指的是瀏覽器的窗口對象,頂層對象是一個有實體含義的對象,也是不合適的。
ES6為了改變這一點,一方面規(guī)定,為了保持兼容性,var命令和function命令聲明的全局變量,依舊是頂層對象的屬性;另一方面規(guī)定,let命令、const命令、class命令聲明的全局變量,不屬于頂層對象的屬性。也就是說,從ES6開始,全局變量將逐步與頂層對象的屬性脫鉤。