ES5只有全局作用域和函数作用域,没有块级作用域,这带来了一些不合理的场景。
场景一,内层变量可能会覆盖外层变量。
var tmp = new Date()
function f() {
console.log(tmp)
if (false) {
var tmp = 'hello world';
}
}
f() // undefined
场景二,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i])
}
console.log(i) // 5
为了解决以上问题,ES6提出了块级作用域。何为块级作用域呢,简单来说,就是一个{ }
就是一个块级作用域。
让我们来看一个例子:
var list = []
for (var i = 0; i < 5; i++) {
list[i] = function() {
console.log(i)
}
}
list[2]() // 5
如果变量使用的是var声明,每一次打印出来的都会是5,而不是当前值,这是为什么呢?其实上面代码相当于下面这样:
var list = []
// for循环只是为数组添加了function元素,但是并没有为function里面的i绑定当前的循环值
var i = 0;
list[0] = function() {
console.log(i)
}
i++; // 1
list[1] = function() {
console.log(i)
}
i++; // 2
list[2] = function() {
console.log(i)
}
i++; // 3
list[3] = function() {
console.log(i)
}
i++; // 4
list[4] = function() {
console.log(i)
}
i++; // 5
// 注意这个时候i已经变成了5
list = [
function() { console.log(i) },
function() { console.log(i) },
function() { console.log(i) },
function() { console.log(i) },
function() { console.log(i) }
]
// 函数执行是没有传参的,调用的是全局的i
list[2]() // 5
以上可见,list数组里的每个函数调用的都是全局的i
,而非当前循环的i
,而又因为JS是从上到下执行的,所以最终打印的就是所有循环结束后的i
。
那么应该如何来解决这个问题呢?其实很简单,回到我们的开头,ES5只有全局和函数2个作用域,而这种情况是因为函数读取了全局变量导致的,那我们在它读取全局变量之前覆盖它,或者给一个局部(函数)变量不就行?是的,核心思想就是这样,不过有几种不同的实现方式。
方法一,ES5的问题依然由ES5解决,这个时候就需要用到鼎鼎有名的闭包了。
var list = []
for (var i = 0; i < 5; i++) {
(function(temp) {
list[i] = function() {
console.log(temp)
}
})(i)
// }(i)) 也一样
}
list[2]() // 2
其实很简单,就是在list[i]
被赋值之前,在它的外层包裹一个新的函数,这个函数是一个立即执行函数IIFE,它会创建一个局部(函数)变量temp
,而list[i]
这个时候改为调用temp
,所以结果就可以跟当前的循环值对应起来。
方法二,使用ES6的新特性,let关键字创建一个块级作用域。
var list = []
for (let i = 0; i < 5; i++) {
list[i] = function() {
console.log(i)
}
}
list[2]() // 2
上面代码相当于下面这样:
var list = []
{
let i = 0;
list[i] = function() { console.log(i) }
}
{
let i = 1;
list[i] = function() { console.log(i) }
}
{
let i = 2;
list[i] = function() { console.log(i) }
}
{
let i = 3;
list[i] = function() { console.log(i) }
}
{
let i = 4;
list[i] = function() { console.log(i) }
}
list[2]() // 2
它就是每次循环都创建了一个新的变量,而这个变量处于块级作用域之内,所以不会跟其他的混乱。