Javascript中的var和let
引子
我们先来看一个常见的例子,
function func(){
for (var i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i)
},1000)
}
}
func()
输出的结果并不是我们预期的 0-9,而是输出了十个 10。而当我们把其中的 var
改成 let
,结果就成了 0-9。
原因
var
使用的是函数作用域,即 for
循环中的 var i
实际上在整个 func
函数中都有效。而 setTimeout
中的匿名函数(十个)都由于闭包的原因能够访问到同一个 i
,因为他们是延迟执行的,所以等到 console.log(i)
被执行的时候, i
已经是 10 了。
在以前我们面对这种情况常用的方法是通过另外一个函数来将 i
装进自己的函数作用域中:
function helper(i){
setTimeout(function(){
console.log(i)
},1000)
}
function func(){
for (var i = 0; i < 10; i++) {
helper(i)
}
}
func()
这样虽然 setTimeout
中的匿名函数仍然是延时调用的,但是 helper
是即时调用的( helper(0), helper(1), ...
),且其参数 i
是其函数作用域持有的(而非 func
持有),这样每个匿名函数通过闭包访问到的 i
都是独立的,所以能够得到 0-9
。
为什么 let 不同
let
是 ES6 引入的新特性,和 var
最大的不同在于,它是块级作用域的,所以相当于 for
的每次循环的 i
都是独立的,它们在语句块(所在的循环体)结束时就失效了(当然由于闭包的原因仍然能被 setTimeout
的匿名函数访问),匿名函数分别持有每个作用域的 i
而不是同一个。
具体的差别
上面虽然给了解释,但是仍然会有些模糊的感觉。下面来明确一下块级作用域和函数作用域的行为到底有什么不同:
不同的作用域范围
顾名思义,函数作用域在所在的函数都有用,而块级作用域则只在自己所在的代码块有用,举个例子:
function func(){
if(true){
var iAmVar = 'var'
let iAmLet = 'let'
}
console.log(iAmVar) // 'var'
console.log(iAmLet) // 访问不到
}
func()
变量提升的不同行为
var
和 let
的变量提升行为也不同,首先了解什么叫变量提升,即
var k = 'test'
function func(){
console.log(k) // undefined
var k = 'in'
}
func()
即函数中的所有 var
的函数声明都会被提前到函数的头部,对于 var
来说,上面的代码相当于
var k = 'test'
function func(){
var k
console.log(k) // undefined
k = 'in'
}
func()
而 let
的表现更为严格,同样的,let 的声明也会被提前,但是如果在这种情况下试图访问尚未初始化的 k
,会抛出一个 ReferenceError
。我们称这种特性为\”临时死区\”,这样能够避免一些奇奇怪怪的失误。
另外,由于作用域的原因, let
的声明提前在代码块中就能生效。
验证上面的想法
所以,如何验证 for
循环每次执行时的 i
都不是同一个呢,也非常简单,我们把i
的声明放到 for
的外面:
function func(){
let i
for ( i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i)
},1000)
}
}
func()
再次得到了 10 个 10,和用 var
时一样(因为作用域都是 func
函数内)。
总结
let
的行为显得比 var
更加符合预期,规则也更为严格,所以我们应该尽可能的使用 let
和 const
来替代 var
,这样代码就不会出现开头那种奇怪的行为。