文章内容词语‘白话’;
- 声明:定义标识符,起名字。
- 初始化:赋值
- 声明但未初始化:有名字能访问,但没给它赋值,所以它的值为undefined
变量提升
在函数作用域或全局作用域中通过关键字 var
声明的变量,无论实际是在哪里声明的,都会被当成在当前作用域顶部声明的变量,这就是我们常说的提升机制。
引用一个简单例子:
function getValue(condition){
if(condition){
var value = "blue";
return value;
}else{
//此处可以访问变量value,其值为undefined
return null;
}
//此处可以访问变量value,其值为undefined
}
无论如何,变量都会被创建,在预编译阶段,JavaScript引擎会把
getValue
函数修改成:
function getValue(condition){
//声明
var value;
if(condition){
//初始化
value = "blue";
return value;
}else{
return null;
}
}
变量
value
的声明和初始化被拆成两步执行,声明被提升至函数顶部,而初始化操作还在原处执行,所以在
else
字句中也可以被访问到,只是此时变量还未初始化,所以访问到的值是
undefined
。
块级声明
块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域(亦被称为词法作用域)存在于:
- 函数内部
- 块中(字符
和
{
之间的区域)
}
let声明
let可以把变量的作用域限制再当前代码块中,由于let声明不会被提升,所以通常将let声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。
function getValue(condition){
if(condition){
let value = "blue";
return value;
}else{
//变量value在此处不存在
return null;
}
//变量value在此处不存在
}
执行流离开
if
块,
value
立即被销毁。如果
condition
的值为
false
,就永远不会声明并初始化
value
。
禁止重声明
在同一作用域中不能用
let
重复定义已经存在的标识符,否则会抛出错误。
var count = 30;
let count = 40;//抛出语法错误
var count = 30;
//当前作用域内嵌另一个作用域(if块),在内嵌的作用域中用let声明就不会报错
if(con){
let count = 40;//只能在if块中被访问到
}
const声明
使用声明的是常量,一旦声明则不可更改。所以:每个通过
const
声明的常量必须初始化。
const
const a = 30; //有效常量
const b; //语法错误,常量未初始化
const
和
let
类似,不存在变量提升、执行到块外就会被立即销毁、重复定义会语法错误。
无论严格模式还是非严格模式,都不可为
const
定义的常量再赋值,否则就会报错。
const a = 30;
a = 40;//抛出语法错误
但是如果常量是对象,则对象中的值是可以被修改的。
const person = {
name:'111'
};
//可以修改对象属性的值
person.name='222';
//抛出语法错误
person = {
name:'222'
}
解析:const声明不允许修改绑定,但允许修改绑定的值。
原来给
person
绑定的是一个包含一个属性
name
的对象,修改属性
name
,其实是在修改这个对象包含的值,即修改绑定的值,但直接给
person
赋值,相当于改了
person
的绑定,所以会抛出错误。
临时死区(Temporal Dead Zone)
和
let
声明的变量不会被提升到作用域的顶部,如果在声明之前访问这些变量,即使是相对安全的
const
操作符也会触发引用错误。
typeof
if(con){
console.log(typeof value); //引用错误
let value = "blue";
}
由于
console.log(typeof value);
语句会抛出错误,所以
let value = "blue";
不会执行,此时
value
还位于所谓的“临时死区”。只有执行过变量声明语句
let value = "blue";
后,变量才会从TDZ中移除,然后才可以正常访问。
console.log(typeof value); //"undefined"
if(con){
let value = "blue";
}
在声明
value
的代码块外执行,此时
value
并不在TDZ中,也就是不存在
value
这个绑定,所以
typeof
返回
"undefined"
循环中的块作用域绑定
for(var i = 0; i < 10; i++){
}
console.log(i); //10
for(let j = 0; j < 10; j++){
}
console.log(j); //抛出错误
let
声明的变量值存在于
for
循环当中,一旦循环结束,在其他地方均无法访问该变量。
小问题:依次输出0-9
var funcs=[];
for(var i = 0; i < 10; i++){
funcs.push(function(){
console.log(i);
});
}
funcs.foreach(function(func){
func(); //输出10次数字10
})
为什么输出10次数字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);
}
}(i))
);
}
funcs.foreach(function(func){
func(); //输出0,1,2...9
})
为什么会这样?
IIFE表达式为接受的每一个变量
i
都创建了一个副本并存储为变量
value
,
value
的值就是相应迭代创建的函数所使用的值,所以调用每个函数得到的都是对应的循环中的值。
循环中的let声明
var funcs=[];
for(let i = 0; i < 10; i++){
funcs.push(function(){
console.log(i);
});
}
funcs.foreach(function(func){
func(); //输出0,1,2...9
})
为什么?
每次循环的时候
let
声明都会创建一个新变量
i
,并将其初始化为
i
的当前值,所以循环内部创建的每个函数都能得到属于他们自己的
i
的副本。对于
for-in
循环和
for-of
循环是一样的。
循环中的const声明
如果上述循环
let
声明改成
const
声明,在循环的第一个迭代中,
i
是0,迭代执行成功,然后执行
i++
,因为这条语句试图修改常量,所以抛出错误。
如果后续循环不会修改该变量,那就可以使用
const
声明。
可以运用在
for-in
和
for-of
循环中,因为每次迭代不会修改已有绑定,而是会创建一个新绑定。
全局块作用域绑定
当
var
被用于全局作用域的时候,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。
但是如果使用
let
和
const
,会在全局作用域下创建一个新的绑定,且不会添加为全局对象的属性。
用
var
声明的变量可能会覆盖掉已经存在的全局属性,可以应用在浏览器中跨frame或跨window访问代码。所以如果不想为全局对象创造属性,用
let
和
const
安全。
建议
默认使用
const
,只有确实需要改变变量的值时使用
let
。因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头。