面试题之 let 简析

1,888 阅读5分钟

为什么会有 let ?

ES6 已经出现很长时间了,但是作为一个初学者,仍然要仔细深入的理解这些点,接下来我会写一个 ES6 语法系列,深入讲解 ES6 语法产生的背景与用法,希望能给大家带来帮助。

块级作用域

大家都清楚,在 let 声明方式出现之前,我们声明一个变量只能通过 var 来定义。

var a = 1;
var b = 'zhangsan';
var c = {name: 1, age: 2};
var d = function(){}
var e = [];

一切都很正常,直到有一天,我们写出了这样的代码:

for(var i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    });
}

我们期望使用这种代码,得到如下结果:

0
1
2
3
4
5
6
7
8
9

但事实却很打脸,得到的结果如下:

这和我们的期望结果不太一样,为什么会得到这样的结果呢? 在此可以说明一下,虽然有些跑题。

原因有二:

一个原因是: var 定义的变量不受块级作用域的限制。

另一个原因是:JavaScript 引擎的事件循环机制在起作用。for循环的同步任务执行完毕之后,才会将从事件队列中取出回调函数,放到调用栈中执行:即setTimeout的回调函数。所以当同步任务执行完之后, i 的值已经变为了 10,此时,10 个定时器的回调开始执行,打印出 10 个 10。

那么,聪明的同学开始想办法了,利用 JS 中的闭包特性,实现期望的结果:

for(var i = 0; i < 10; i++){
    (function(t){
		setTimeout(function(){
        	console.log(t);
    	});
    })(i);
}

可以看到,代码不易理解,书写也很麻烦。

于是 ES6 中新出现了 let 声明方式, 通过 let 声明的变量有了块级作用域的限制。 举个例子,先从 var 声明开始:

{
    var a = 1;
}
console.log(a);

由于 var 没有块级作用域的约束,所以我们在块级作用域以外访问 a 变量的话,仍然是能够访问到的。因此,上面的定时器例子会打印出 10 个 10。

接下来,我们将 var 改为 let 进行声明:

{
    let a = 1;
}
console.log(a);

大家可以猜测一下输出结果是什么?

事实上会报错的:

原因在于,按照 let 的声明特点, let 定义的变量只能在声明时所在的作用域中访问到,所以 a 被限制在了块级作用域中,在块级作用域外访问 a 的话,由于外层作用域并未定义 a 变量,所以会报上述错误。

既然 let 有了块级作用域的约束,我们就可以用 let 来改写上面的定时器例子:

for(let i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    });
}

可以看到,使用let 我们就能够正常将 i 值约束在每一个循环块作用域中了,这比 ES5 中利用闭包要容易理解多了。

重复声明的隐患

以前我们用 var 声明变量的时候,可以重复进行同名变量的声明,比如

var a = 1;
var a = 2;
var a = 3;
console.log(a);

以上代码,没有报错,并且 a 的值得到了篡改,以最后一次赋值为准。

大家可能觉得没有问题。

我们再举一个例子:

假设 A、B、C 三个同学需要共同完成一个页面功能。

A同学写了一个 JS 文件 A.js,A同学再这个文件中定义了一个name变量,赋值为章三,他想在页面上将这个名字打印出来。

var name = '章三'

B同学写了另一个 JS 文件 B.js,也定义了一个变量,也叫 name,赋值为 '李四',他也想在页面上将这个名字打印出来。

var name = '李四';

A 同学告诉 C 同学,取出 name 属性,打印出来就可以了。

B 同学告诉 C 同学,取出 name 属性,打印出来就可以了。

可惜 C 同学不是一个细心的同学,他没有意识到两个变量重名了,于是他写了一个 html 文件,引入了 A.js 和 B.js,然后将 name 属性打印出来。

<script src="./A.js"></script>
<script src="./B.js"></script>

<body>
    <div id="name"> </div>
    <script>
        document.querySelector('#name').innerHTML = name;
    </script>
</body>

然后 C 同学将页面发给 A 同学和 B 同学,让他们看一下结果对不对。

结果 A 同学一看,发现打印出来的名字不是章三,而是 李四,他就怒气冲冲地去质问 C 同学,C同学说,我就是按照你告诉我的方式去打印的呀。

故事进行到这里,矛盾出现了:

同一个作用域下定义多个重名变量,JavaScript 引擎不会报错,但是会为程序的正确性带来隐患。

幸运的是,let 的出现很好地解决了这个问题:

同一个作用域下,let 声明的变量不能和已经声明的变量重名,否则引擎会报错。

let a = 1;
let a = 2;

或者

var a = 1;
let a = 2;

所以,let 为我们带来了另一个好处,防止我们定义重名变量。

变量声明不再提升

仍然以一段代码为例:

console.log(a);
var a = 1;

大家猜测一下,输出结果是什么?

程序运行不会报错,但是输出结果不是我们期望的。

输出结果是 undefined。

所以,这会带来一个问题,我们在声明一个变量之前,万一使用了这个变量,就会得到意料之外的结果,进而造成程序运行错误,如果能有一种机制能够强制我们在使用变量之前,必须先声明该变量就好了。

这就是 let 的第三个特点:使用一个 let 声明的变量之前,必须先声明。

console.log(a);
let a = 1;

如上代码,a 变量的声明放在了使用之后,我们看下运行结果:

编译器给出了报错提示,这促使我们在早期就能发现问题。

结语

以上就是 ES6 的 let 使用总结,虽然是一个很小的点,但是我们也要认清它出现的背景,为了解决什么问题而生。

之后,会为大家带来 ES6 其他语法的剖析,敬请期待~~~