var、let、const与闭包
1 var声明和变量提升机制
在函数作用域或全局作用域中通过关键字 var 声明的变量,无论实际上在哪里声明,都会被当成在当前作用域顶部声明的变量,这就是提升机制。以下列代码为例:
function getValue(condition) {
if (condition) {
var value = "blue"
console.log("value1:", value)
} else {
console.log("value2:", value)
}
console.log("value3:", value)
}
getValue(false) //"value2:", undefined "value3:", undefined
getValue(true) //"value1:", "blue" "value3:", "blue"
运行后发现当 condition 的值为 false 时,变量 value 依然被创建了,只是值为 undefined。
这是因为,在预编译阶段,JavaScript 引擎会将变量 value 的声明提升至函数顶部,而初始化操作依旧留在原处执行,所以 else 子句也能访问到 value 变量,且由于此时变量尚未初始化,所以其值为 undefined,如下所示:
function getValue(condition) {
var value
if (condition) {
value = "blue"
console.log("value1:", value)
} else {
console.log("value2:", value)
}
console.log("value3:", value)
}
getValue(false) //"value2:", undefined "value3:", undefined
getValue(true) //"value1:", "blue" "value3:", "blue"
2 块级声明
块级声明用于声明在指定块的作用域之外无法访问的变量,块级作用域存在于函数内部和块中(字符 { 和 } 之间的区域)。
2.1 let 声明
let 声明和 var 声明在用法上相同,区别在于用 let 声明的变量,不会被提升,变量的作用域被限制在当前代码块中。由于 let 声明不会被提升,因此通常将 let 声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。
function getValue(condition) {
if (condition) {
let value = "blue"
//此处可以正常访问变量value
} else {
//变量value在此处不存在,访问会报错
}
//变量value在此处不存在,访问会报错
}
变量 value 改由关键字 let 进行声明后,不再被提升至函数顶部。执行流离开 if 块,value 立刻被销毁。如果 condition 的值为 false,就永远不会声明并初始化 value。
2.2 const 声明
使用 const 声明的是常量,其值一旦被设定后不可以再更改。因此,每个通过 const 声明的常量必须进行初始化。如下:
const maxNum = 30
const name //语法错误:常量未初始化
maxNum = 5 //语法错误:常量值不可以更改
当用 const 声明对象时,const 声明不允许修改绑定,但允许修改值。这也意味着用 const 声明对象后,可以修改该对象的属性值。如下:
const person = {
name:"Jack"
}
person.name = "Rose" //可以修改对象属性的值
//抛出语法错误
person = {
name:"Rose"
}
const 与 let 声明的都是块级标识符,所以常量也只在当前代码块内有效,一旦执行到块外会立即被销毁。常量同样也不会被提升。如下:
if (condition) {
const maxNum = 30
}
//此处无法访问 maxNum
2.3 临时死区(Temporal Dead Zone)
前文提到,由于 let 声明不会被提升,因此通常将 let 声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。假设我们不将 let 声明语句放在封闭代码块的顶部,如下:
if (condition) {
console.log(typeof value) //引用错误!
let value = "blue"
}
由于 console.log(typeof value)
语句会抛出错误,因此 let value = "blue"
不会执行。此时的 value 位于 ”临时死区“ 。临时死去这一概念没有在官方标准中明确提到,但人们常用它来描述 let 和 const 的不提升效果。
JavaScript 引擎在扫描代码发现变量声明时,若遇到 var 声明就将其提升至作用域顶部,若遇到 let 和 const 声明就将其放到临时死区中。访问临时死区中的变量会触发运行时错误,只有执行过变量声明语句后,变量才会从临时死区中移出,然后才能正常访问。
但是,在 let 声明的作用域之外对该变量使用 typeof 则不会报错,如下:
console.log(typeof value) //"undefined"
if (condition) {
let value = "blue"
}
typeof 是在声明变量的代码块外执行的,此时 value 不在临时死区中,即此时不存在 value 这个绑定,所以 typeof 操作最终返回 “undefined”。
3 闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
如下一段代码:
function foo() {
let a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() //2
函数 bar() 的词法作用域能够访问 foo() 的内部作用域,bar() 函数本身作为 foo() 的返回值。foo() 执行后,返回值 bar() 函数被赋值给变量 baz 。调用 baz() 后,发现成功输出变量 a 的值 2。
可变量 a 明明只在 foo() 函数的内部作用域中存在,foo() 函数在执行完成后,变量 a 也应该随之销毁,为什么调用 baz() 函数依旧可以访问变量 a 并得到其值呢?
调用 baz() 实际上只是通过不同的标识符引用调用了 foo() 内部的函数 bar() ,而 bar() 之所以能在外部调用时依旧正常访问变量 a ,是因为 bar() 所在的 foo() 的整个函数内部作用域形成了一个闭包。
4 循环中的闭包
一个非常基础的 for 循环语句,如下所示:
for (var i=1; i<=5; i++) {
console.log( i );
}
//执行结果:依次输出数字1-5
现在我们期望这个循环语句能分别输出数字1-5,每间隔1秒输出一次,一次一个数字,所以使用 setTimeout() 进行了改写,如下所示:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//执行结果:每秒一次,输出五次6
问题出现了,上述代码的执行结果并未如期实现,而是每隔1秒输出一次,输出了五次数字6。
为什么会出现这种情况呢?
首先,由于 setTimeout() 的作用,timer() 是一个回调函数,在循环结束后才会被执行;其次,循环的终止条件是 i<=5
,所以循环结束后 i 的最终值是6;最后,由于变量 i 是用 var 声明的,变量 i 的作用域被提升,整个循环过程中实际上只有一个 i 。
为了解决这个问题,可以用到前文提到的块级声明 let ,在 for 循环中用 let 声明一个变量 j ,这样每一次循环都会有一个新的变量 j 记录当前循环中 i 的值,虽然用 let 声明的 j 会随着当前循环的结束而销毁,但每一次循环都形成了一个包含当前变量 j 的闭包。
因此,当循环中的回调函数被依次调用时,都能在各自闭包中访问变量 j ,得到对应的 i 值。代码如下所示:
for (var i=1; i<=5; i++) {
let j = i
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
//执行结果:每秒依次分别输出数字1-5