let、const和塊級作用域
目錄:
let 與 var 的異同點比較
let | var | |
---|---|---|
定義變量 | √ | √ |
可被釋放 | √ | √ |
可被提升(Hoist) | √ | |
重複定義檢查 | √ | |
可被用於塊狀作用域 | √ |
重複定義檢查:
var foo = 'bar';
var foo = 'abc';
console.log(foo); // abc
let bar = 'foo';
let bar = 'abc' // Uncaught SyntaxError: Identifier 'bar' has already been declared
let可用於塊級作用域(Block Scope)
const arr1 = [];
for(var i = 0; i < 3; ++i){
arr1.push(() => i);
}
const arr2 = arr1.map(x => x())
const arr3 = [];
for(let i = 0; i < 3; ++i){
arr3.push(() => i);
}
const arr4 = arr3.map(x => x())
console.log('var: ' + arr2.join(', ')); //var: 3, 3, 3
console.log('let: ' + arr4.join(', ')); //let: 0, 1, 2
const 定義常量
//定義一個常量
const PI = 3.1415926;
//嘗試對該常量進行重新賦值
PI = 3.14 //Uncaught TypeError: Assignment to constant variable
ECMAScript 在對變量的引用進行讀取時,會從該變量當前所對應的內存地址所指向的內存空間中讀取內容。而當用戶改變變量的值時,引擎會重新從內存中分配一個新的內存空間以存儲新的值,並將新的內存地址與變量進行綁定。const 的原理便是在變量名與內存地址之間建立不可變的綁定,當後面的程序嘗試申請新的內存空間時,引擎便會拋出錯誤。
而對於對象、數組等稀疏的引用類型值,在內存空間中可能會被拆分成若干個段落,雖然 Google V8 在對 javaScript 運行時的內存管理中使用的是堆內存 (heap) ,而不是棧內存(stack) ,但因爲對象的屬性是可以變化的,所以爲了最快地進行內存調度,當對象的屬性被改變或添加了新的屬性時,都需要重新計算內存地址偏移值。
const foo = {
a: 1,
b: 2
}
內存偏移值(假設) | 內容 |
---|---|
0 | Object foo |
1 | Property a |
2 | Property b |
const 在這段代碼中所起到的作用是:將常量 foo 與內存偏移值爲0的內存空間綁定起來並鎖死,然而內存偏移值爲1和2的 a 和 b 屬性並沒有的得到強綁定,所以其內容依然能通過修改屬性值而被改變。
因爲const所創建的內存綁定是只綁定一處,所以默認情況下對象這種由若干內存空間片段組成的值並不會全部被鎖定。
要獲得值不可變的對象,需要配合 ES5 中的 Object.fressze() 方法,便可以得到一個首層屬性不可變的對象。但若首層屬性中存在對象,則默認情況下首層以下的對象依然可以被改變:
const obj1 = Object.freeze({
a: 1,
b: 2
})
obj1.a = 2; // Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'
const obj2 = Object.freeze({
a: {}
})
obj2.a.b = 1;
console.log(obj2); // {"a":{"b":1}}
爲了解決首層以下的對象依然可以被改變的問題,我們需要寫一個函數來實現創建一個完全的值不可變對象:
// Object.deepFreeze from MDN
// To make obj fully immutable, freeze each object in obj
// To do so, we use this function
Object.deepFreeze = function(obj) {
// Retrieve the property names defined on obj
var propNames = Object.getOwnPropertyNames(obj);
// Freeze properties before freezing self
propNames.forEach(function(name) {
var prop = obj[name];
// Freeze prop if it is an object
if (typeof prop == 'object' && prop !== null)
Object.deepFreeze(prop);
});
// Freeze self (no-op if already frozen)
return Object.freeze(obj);
}
const obj3 = Object.deepFreeze({
a: {
b: 1
}
})
obj3.a.c = 2; // Uncaught TypeError: Can't add property c, object is not extensible
const 定義的常量同樣遵循變量在作用域中的生命週期( const 也可用於塊級作用域)。
變量的生命週期
var foo // Declaration
foo = 1 // Assignment
- ECMAScript 引擎在進入一個作用域時,會掃描這個作用域內的變量(或常量)定義語句(var, let 或 const),然後在這個作用域內爲掃描得到的變量名做準備,在當前作用域中被掃描到的變量名都會進入未聲明(Undeclared)階段。
- 進入聲明語句時,var foo ,即前半句時聲明部分(Declaration),用於在 ECMAScript 引擎中產生一個變量名,但此時該變量名沒有對應的綁定和內存空間,所以“值”爲null。
- = 的作用是作爲變量的賦值語句,引擎執行至此處即爲該變量的賦值部分(Assignment),計算將要賦予變量名的值的物理長度(內存空間佔用大小),向系統申請相應大小的內存空間,然後將數據存儲到裏面去,並在變量名和內存空間之間建立綁定關係(Bingding),此時變量(或常量)纔得到了相應的值。
- 到當前作用域中的語句被執行完畢時,引擎便會檢查該作用域中被定義的變量(或常量)的被引用情況,如果引用已被全部解除,引擎便會認爲其應該被清除。
- 運行引擎會不斷檢查存在於運行時( Runtime )中的變量(或常量)的被引用情況,並重復第四步,直到程序結束。
console.log(foo); // ReferenceError
console.log(bar); // ReferenceError
let foo = 1;
const bar = 2;
當變量處於未聲明階段,在ES6的 let 和 const 中,引擎將這一種行爲視爲錯誤行爲,並拋出錯誤。
使用原則
- 一般情況下,使用const來定義值的存儲容器(常量)。
- 只有在值容器明確地被確定將會被改變時才使用let來定義(變量)。
- 不再使用var。
循環語句 for…of
它的主要用途是代替 for…in 循環語句。
const arr = [1, 2, 3];
for(const item of arr){
console.log(item); //1 2 3
}
for…in 遍歷的是數組的索引(即鍵名),而 for…of 遍歷的是數組元素值。
使用 for…in 遍歷數組存在以下問題:
- index索引爲字符串型數字,不能直接進行幾何運算。
- 遍歷順序有可能不是按照實際數組的內部順序。
- 使用 for…in 會遍歷數組所有的可枚舉屬性,包括原型。
數組類型的entries方法
返回對應的數組中每一個元素與其下標配對的一個新數組:
const arr = ['a', 'b', 'c'];
for (let item of arr.entries()) {
console.log(item);
}
// [0, "a"]
// [1, "b"]
// [2, "c"]