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"]