#var声明及变量提升机制
在 函数作用域 或 全局作用域中通过 var 声明的变量,无论实际上在哪里声明的,都会被当成在 当前作用域顶部声明的变量。这就是常说的提升机制;
function func(condition){
if(condition){
var value="xxx";
return value;
}
else{
//这里可以访问 value,其值为 undefined
return null;
}
//此处可以访问 value,其值为 undefined
}
上面的代码,在预编译阶段,JavaScript引擎会将上面的func函数修改成下面这样:
function func(condition){
var value;
if(condition){
value="xxx";
return value;
}
else{
return null;
}
}
变量 value 的声明被提升至函数顶部,而初始化操作依旧在原处执行,这就意味着在 else 子句中可以访问到该变量,且由于没有被初始化,所以其值为 undefined;
正因为这个问题,ECMAScript 6 引入了块级作用域来强化对变量声明周期的控制。
#块级声明
块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:
1,函数内部;
2,块中(花括号{}之间);
&let声明
let用法和var相同。用let来替代var来声明变量,可以把变量的作用域限制在当前代码块中。
function func(condition){
if(condition){
let value="xxx";
return value;
}
else{
//变量 value 在此处不存在
return null;
}
//变量 value 在此处不存在
}
变量 value 用let声明后,不再被提升至函数顶部。执行流离开if块,value 立刻被销毁。如果condition为false,就永远不会声明并初始化 value。
&禁止重声明
假设作用域中已经存在某个变量,此时再使用 let 关键字声明一次就会错误:
var count = 30;
// 抛出语法错误
let count = 40;
&const声明
ECMAScript 6 标准还提供了 const 关键字。使用声明的是常量,其值一旦被设定后不可更改。因此,每个通过 const 声明的常量必须进行初始化;
// 有效的常量
const maxNum = 30;
// 语法错误:常量未初始化
const minNum;
const 与 let 声明的都是块级标识符,所以常量也只在当前代码块内有效,一旦执行到块外会立即被销毁。常量同样也不会被提升至作用域顶部;
与 let 相似,在同一作用域中用 const 声明已经存在的标识符也会导致语法错误;
&用 const 声明对象:
记住,const 声明不允许修改绑定,但是允许修改值。这也就意味着用 const 声明对象后,可以修改该对象的属性值,但是不可以修改该对象的引用:
const person = {
name : 'zhangsan'
}
// 没毛病
person.name = 'lisi';
// 抛出语法错误
person = {
name : 'wangwu'
}
#循环中的块级作用域绑定
以下的代码在JavaScript中很常见:
for(var i=0;i<10;i++){
process(arr[i]);
}
console.log(i); // 打印10
由于 var 声明得到提升,变量 i 的声明提升至当前作用域顶部,在循环结束后依旧可以访问它;如果换成 let 声明变量:
for(let i=0;i<10;i++){
process(arr[i]);
}
// i 在这里不可访问,抛出一个错误
console.log(i);
&循环中的函数
var 声明让开发者在循环中创建函数变得异常困难,对于新手会造成困惑,看下面的代码:
var funcs = [];
for(var i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func();
});
预期的结果是输出数字 0 ~ 9,但是程序却一直输出 10;
这是因为循环里的每次迭代同时共享着变量 i,循环内部创建的函数全部保留了对相同变量的引用。当循环结束时候,变量 i 的值为10,所以每次调用 console.log(i) 时就会输出10;
为了解决上面的问题,可以在循环中使用 立即调用函数表达式(IIFE),以强制生成计数器变量的副本:
var funcs = [];
for(var i=0;i<10;i++){
funcs.push(function(value){
return function(){
console.log(value);
}
});
}
funcs.forEach(function(func){
func(); //依次输出 0 ~ 9
});
在循环内部,IIFE 表达式为接受的每一个变量 i 都创建了一个副本并存储为变量 value。这个变量的值就是相应迭代创建的函数所使用的值。
ECMAScript 6 中的 let 和 const 提供的块级绑定就无需这么麻烦了;
&循环中的let声明
let 声明模仿上面的 IIFE 所做的一切来简化循环过程。
var funcs = [];
for(let i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func(); //依次输出 0 ~ 9
});
&循环中的const
ES6 标准中没有明确指明不允许在循环中使用 const 声明,然而,针对不同类型的循环它会表现出不同的行为。
对于普通的 for 循环,使用 const 声明计数器,那肯定是会抛出错误的:
var funcs = [];
// 循环一次之后,抛出错误
for(const i=0;i<10;i++){
//...
}
但是在 for-in 或者 for-of 循环中使用 const 声明的行为与使用 let 是一致的:
var funcs = [];
var obj = {
a:true,
b:true,
c:true
};
// 不会产生错误
for(const key in obj){
funcs.push(function(){
console.log(key);
})
}
funcs.forEach(function(func){
func(); // 依次输出 a,b,c
})
之所以可以在 for-in 和 for-of 中使用 const 声明,是因为每次迭代不会修改已有的绑定,而是会创建一个新的绑定;
#全局作用域绑定
let 和 const 与 var 的另外一个是它们在全局作用域中的行为;
当 var 被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境下全局对象为window)的属性。这也就意味着 var 可能会在无意中覆盖一个已经存在的全局变量;
// 在浏览器中
var RegExp = 'Hello!';
console.log(window.RegExp); // 输出 Hello!
var ncz = 'Hi!';
console.log(window.ncz);
即使全局对象 RegExp 对象定义在 window上,也不能幸免于被 var 声明覆盖掉;
如果在全局作用域中使用 let 或 const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,用 let 或 const 不会覆盖全局变量,而只能遮蔽它;
// 在浏览器中
let RegExp = 'Hello!';
console.log(RegExp); // 输出 Hello!
console.log(window.RegExp === RegExp); // false
const ncz = 'Hi!';
console.log(ncz); // 输出 Hi!
console.log("ncz" in window); // false
let 声明的 RegExp 创建了一个绑定并遮蔽了全局的 RegExp 变量,结果就是 window.RegExp 和 RegExp 不相同,但不会破坏全局作用域。
同样,const 声明的 ncz 创建了一个绑定但没有创建为全局对象的属性。
如果不想为全局对象创建属性,则使用 let 或 const 要安全得多。
tips:在浏览器中跨 frame 或者跨 window 访问,依然得使用 var在全局对象下定义变量。
#块级绑定最佳实践
当更多的开发者迁移到ES6后,一种做法日益普及:默认使用 const ,只有确实需要改变变量的值时使用 let。
因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多 bug 的源头。