let、const命令——ES6學習筆記
let
基本使用
ES6 新增了let
命令,用來宣告變數。它的用法類似於var
,但是所宣告的變數,只在let
命令所在的程式碼塊內有效。
for
迴圈的計數器,就很合適使用let
命令。
var a = []; // 輸出 10 ,如果是let宣告的,輸出 6 (這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。) for (var i = 0; i < 10; i++) { //for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。 a[i] = function () { console.log(i); }; } a[6]();
不存在變數提升
var
命令會發生“變數提升”現象,即變數可以在宣告之前使用,值為undefined
。這種現象多多少少是有些奇怪的,按照一般的邏輯,變數應該在宣告語句之後才可以使用。
為了糾正這種現象,let
命令改變了語法行為,它所宣告的變數一定要在聲明後使用,否則報錯。
暫時性死區
只要塊級作用域記憶體在let
命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError 在let命令宣告變數tmp之前,都屬於變數tmp的“死區”。
let tmp;
}
上面程式碼中,存在全域性變數tmp
let
又聲明瞭一個區域性變數tmp
,導致後者繫結這個塊級作用域,所以在let
宣告變數前,對tmp
賦值會報錯。ES6 明確規定,如果區塊中存在let
和const
命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。總之,在程式碼塊內,使用let
命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。
“暫時性死區”也意味著typeof
不再是一個百分之百安全的操作。
typeof x; // ReferenceError //變數x使用let命令宣告,所以在宣告之前,都屬於x的“死區”,只要用到該變數就會報錯。因此,typeof執行時就會丟擲一個ReferenceError。 let x; //作為比較,如果一個變數根本沒有被宣告,使用typeof反而不會報錯。 typeof undeclared_variable // "undefined" // 不報錯 var x = x; // 報錯 let x = x; // 在變數x的宣告語句還沒有執行完成前,就去取x的值,導致報錯”x 未定義“。 // ReferenceError: x is not defined
在沒有let
之前,typeof
運算子是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的程式設計習慣,變數一定要在宣告之後使用,否則就報錯。
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。
不允許重複宣告
let
不允許在相同作用域內,重複宣告同一個變數。因此,不能在函式內部重新宣告引數。
function func(arg) {
let arg;
}
func() // 報錯
function func(arg) {
{
let arg;
}
}
func() // 不報錯
塊級作用域
為什麼需要?
ES5 只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景:
- 第一種場景,內層變數可能會覆蓋外層變數。
- 第二種場景,用來計數的迴圈變數洩露為全域性變數。
ES6的塊級作用域
// ES6 允許塊級作用域的任意巢狀。
{{{{
{let insane = 'Hello World'}
console.log(insane); // 報錯
}}}};
// 上面程式碼使用了一個五層的塊級作用域,每一層都是一個單獨的作用域。第四層作用域無法讀取第五層作用域的內部變數。
// 塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執行函式表示式(匿名 IIFE)不再必要了。
// IIFE 寫法
(function () {
var tmp = ...;
...
}());
// 塊級作用域寫法
{
let tmp = ...;
...
}
塊級作用域與函式宣告
ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let
,在塊級作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複宣告一次函式f
function f() { console.log('I am inside!'); }
}
f();
}());
// 上面程式碼在 ES5 中執行,會得到“I am inside!”,因為在if內宣告的函式f會被提升到函式頭部,實際執行的程式碼如下。
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6 就完全不一樣了,理論上會得到“I am outside!”
。因為塊級作用域內宣告的函式類似於let
,對作用域之外沒有影響。但是,如果你真的在 ES6 瀏覽器中執行一下上面的程式碼,是會報錯的,這是為什麼呢?
考慮到環境導致的行為差異太大,應該避免在塊級作用域內宣告函式。如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。
另外,還有一個需要注意的地方。ES6 的塊級作用域必須有大括號,如果沒有大括號,JavaScript 引擎就認為不存在塊級作用域。
// 第一種寫法,報錯
if (true) let x = 1;
// 第二種寫法,不報錯
if (true) {
let x = 1;
}
// 第一種寫法沒有大括號,所以不存在塊級作用域,而let只能出現在當前作用域的頂層,所以報錯。第二種寫法有大括號,所以塊級作用域成立。
// 函式宣告也是如此,嚴格模式下,函式只能宣告在當前作用域的頂層。
const
基本使用
const
宣告一個只讀的常量。一旦宣告,常量的值就不能改變。const
宣告的變數不得改變值,這意味著,const
一旦宣告變數,就必須立即初始化,不能留到以後賦值。只宣告不賦值,就會報錯。const
的作用域與let
命令相同:只在宣告所在的塊級作用域內有效。const
命令宣告的常量也是不提升,同樣存在暫時性死區,只能在宣告的位置後面使用。const
宣告的常量,也與let
一樣不可重複宣告。
本質
const
實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const
只能保證這個指標是固定的(即總是指向另一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。
// 常量foo儲存的是一個地址,這個地址指向一個物件。不可變的只是這個地址,即不能把foo指向另一個地址,但物件本身是可變的,所以依然可以為其新增新屬性。
const foo = {};
foo.prop = 123; // 為 foo 新增一個屬性,可以成功
foo.prop // 123
// 將 foo 指向另一個物件,就會報錯
foo = {}; // TypeError: "foo" is read-only
// 常量a是一個數組,這個陣列本身是可寫的,但是如果將另一個數組賦值給a,就會報錯。
const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯
// 如果真的想將物件凍結,應該使用Object.freeze方法。
const foo = Object.freeze({});
// 常規模式時,下面一行不起作用;嚴格模式時,該行會報錯
foo.prop = 123;
除了將物件本身凍結,物件的屬性也應該凍結。下面是一個將物件徹底凍結的函式。
var constantize = (obj) => { // 對每一項進行遍歷
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6宣告變數的6種方法
ES5 只有兩種宣告變數的方法:var
命令和function
命令。ES6 除了新增let
和const
命令,後面章節還會提到,另外兩種宣告變數的方法:import
命令和class
命令。所以,ES6 一共有 6 種宣告變數的方法。
var function let const import class
頂層物件的屬性
頂層物件,在瀏覽器環境指的是window
物件,在 Node 指的是global
物件。ES5 之中,頂層物件的屬性與全域性變數是等價的。
一方面規定,為了保持相容性,var
命令和function
命令宣告的全域性變數,依舊是頂層物件的屬性;另一方面規定,let
命令、const
命令、class
命令宣告的全域性變數,不屬於頂層物件的屬性。也就是說,從 ES6 開始,全域性變數將逐步與頂層物件的屬性脫鉤。
globalThis物件
JavaScript 語言存在一個頂層物件,它提供全域性環境(即全域性作用域),所有程式碼都是在這個環境中執行。但是,頂層物件在各種實現裡面是不統一的。
很難找到一種方法,可以在所有情況下,都取到頂層物件。下面是兩種勉強可以使用的方法。
// 方法一
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
// 方法二
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
在語言標準的層面,引入globalThis
作為頂層物件。也就是說,任何環境下,globalThis
都是存在的,都可以從它拿到頂層物件,指向全域性環境下的this
。