var声明与变量提升机制
使用ES5语法时,在函数作用域或全局作用域中通过var关键字声明变量。
function es5DecareVar(condition){
if(condition) {
var val = "Y";
}
return val;
}
上述函数,分别输入true和false:
es5DecareVar(true); // Y
es5DecareVar(false); // undefined
出现该现象的原因则是变量提升机制(Hoisting),由于在预编译阶段,JavaScript引擎会将es5DecareVar进行修改:
function es5DecareVar(condition){
var val;
if(condition) {
val = "Y";
}
return val;
}
将函数中的变量声明均提升至函数顶部,而初始化操作依旧在原处执行,造成了if条件外的变量尚未初始化,所以其值为undifinded。
ES6 中的块级作用域强化了对变量生命周期的控制。
块级声明
用于声明在制定块的作用域之外无法访问的变量,也被称为词法作用域,存在于 - 函数内部 - 块中(字符{}之间)
let声明
let声明的用法与var相同,使用let代替var声明变量,可将变量的作用域限制在当前代码块中,且不会被提升。
function es6DecareVar(condition){
if(condition) {
let val = "Y";
}
return val;
}
同样的函数,分别输入true和false:
es6DecareVar(true); // Y
es6DecareVar(false); // undefined
结果却和使用var一样,但其中的原因却不相同。
使用var时产生的undifinded是由于变量提升机制导致的;
而使用let时,却是由于临时死区(Temporal Dead Zone)造成的。
在解释临时死区前,先了解下ES6中另一个变量声明const。
const声明
const声明的是常量,其值一旦被设定后不可更改,因此每个通过const声明的常量必须进行初始化。
const val = "Y"; // 有效声明的常量
const invalidVal; // 无效声明的常量
val = "N"; // 常量无法修改
可见,常量的声明必须初始化,且无法更改,但使用const完成对象的声明却有些许不同。
const object = {
id: 1,
name: "A"
}
// 可以修改对象属性的值
object.name = "B";
// 抛出语法错误
object = {
id: 2,
name: "C"
}
对象常量的属性值可变更,但却无法改变对象本身的绑定关系。即const声明不允许修改绑定,但允许修改绑定的值。
具体原因引用阮一峰老师在《ECMAScript 6 入门》所述。
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
临时死区
let与const声明的变量并不会被提升到作用域的顶部,如果在声明之前访问这些变量,即使是相对安全的typeof操作符也会触发引用错误。
console.log(typeof value); // 抛出语法错误
let value = "Y";
原因是当JavaScript引擎在扫描代码发现变量声明时,会根据不同的关键字采取不同的操作。
- 遇到
var声明时,会将变量提升至作用域顶部 - 遇到
let或const声明时,将声明放到临时死区中
只有执行过变量声明语句后,变量才会从临时死区中移出,可被正常访问,否则会触发运行时错误。
块作用域在循环中的使用
var 声明由于变量在循环之外仍能访问,会造成预料之外的结果。
var list = [];
for(var i=0; i < 10; i++){
list.push(()=>{
console.log(i);
})
}
list.forEach((func)=>{
func(); // 输出10次 数字10
})
为了解决这个问题,通常的方式是在循环中使用立即调用函数表达式(IIFE),强制生成计数器变量的副本。
var list = [];
for(var i=0; i < 10; i++){
list.push(((val)=>{
return ()={
console.log(val);
}
}(i)));
}
list.forEach((func)=>{
func(); // 输出0-9
})
上面的方法在循环内部,IIFE表达式为接手的每一个变量i都创建了一个副本并存储为变量val,从而每个函数被执行后会输出0-9。
而使用ES6的let和const提供的块级绑定可轻松解决。
let在循环中的使用
上述问题使用let关键字来改写:
var list = [];
for(let i=0; i < 10; i++){
list.push(()=>{
console.log(i);
})
}
list.forEach((func)=>{
func(); // 输出0-9
})
每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于各自的i的副本。
使用for-in或for-of也会得到同样的效果。
const在循环中的使用
如果使用const关键字来改写:
var list = [];
// 完成一次迭代后抛出语法错误
for(const i=0; i < 10; i++){
list.push(()=>{
console.log(i);
})
}
原因在于变量i被声明为常量,完成一次迭代后,循环语句试图修改常量i导致语法错误。
而使用for-in或for-of则由于每次迭代不会修改已有的绑定关系,而是会创建一个新的 绑定。
var list = [],
object = {
a: 1,
b: 2,
c: 3
}
for(const key in object){
list.push(()=>{
console.log(key);
})
}
list.forEach((func)=>{
func(); // 输出1-3
})
块级绑定的最佳实践
const和let的唯一区别在于,const可以让数值、字符串和布尔变量不可变,且定义的对象始终指向同一个对象。
由于大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头,故建议的使用方式为:
- 默认使用
const - 确实需要改变变量的值时使用
let - 不使用
var
如果觉得文章能帮助你理解一些前端知识点,请点赞、关注本专栏,也可关注微信公众号。