你好,今天大叔想和你聊一聊 ES6 新增的关键字 —— let
。说 let
的具体用法之前,大叔想先和你说说大叔自己对 let
的感受 —— let
其实就是加强版的 var
。为什么这么说呢?别急,且听大叔慢慢道来。
首先,let
和 var
的作用是一样的,都是用来声明变量的。看到这儿,你可能会想到个问题,既然作用一样,为什么还要再搞个什么新特性出来?
想要回答这个问题,就要说到 let
和 var
的不同之处。比如说 var
声明的全局变量会自动添加到顶级对象中作为属性,而 let
就不会。再比如说 var
允许声明提升或者重复声明,而 let
就不允许这样做。当然,它们之间的不同可不止这些,大叔也只是举个例子而已。
如果你还没了解过 ES6 的内容,看到这儿可能有点懵。没关系啊~ 别往心里去,因为接下来大叔就是要和你聊一聊 let
的具体用法。
弄明白 let
和 var
第一点不同之前,大叔要先和你聊一聊 var
关键字的一些用法。为什么?!var
你要是都弄不明白,你还想弄明白 let
,那就是一个美丽的扯!
首先,咱们知道声明一个全局变量,是既可以使用 var
进行声明,也可以不使用 var
进行声明的。比如说像下面这段代码一样:
var a = 'a'
console.log(a)
b = 'b'
console.log(b)
上面这段代码不用大叔多说,想必你也知道打印的结果是个什么 —— a 和 b。别急,这才是个开始,咱得慢慢来~
接下来,大叔要先用 delete
删除上面的两个变量 a
和 b
,然后再分别打印这两个变量的值。你想一下这个时候应该打印的结果是声明?对啦!变量 a
的值会正常输出 a,但变量 b
会报错 b is not defined
。那为什么会是这样的结果呢?
大叔觉得你应该知道 delete
运算符的作用是用来删除对象的属性,但是 delete
是无法删除变量的。对啦!你想的没错,这就说明上面声明的 a
是变量但不是对象的属性,而是 b
是对象的属性但不是变量。
大叔这话说的有点绕,给你带入一个场景吧。比如上面这段代码是在一个 HTML 页面中定义的 JavaScript 代码,那 a
就是一个全局变量,b
就是作为 window
对象的一个属性。所以,delete
运算符可以删除 b
,但不能删除 a
。
那也就是说使用 var
关键字声明的是变量,不使用 var
关键字声明的是 window
对象的属性。聊到这儿,大叔还得来个骚操作,咱们再看一段代码:
var a = 'a'
console.log(window.a)
var b = 'b'
console.log(window.b)
这段代码如果按照上面的结论,打印的结果就应该是 undefined 和 b。但是~ 你真实运行一下这段代码,就应该知道实际上打印的结果是 a 和 b!
这怎么和上面的结论不一样呢?!是不是又有点懵?哈哈~ 别先急着懵,这个问题实际上是 JavaScript 的作者 Brendan Eich 当年在设计 JavaScript 这门语言时的一个小失误:在全局作用域中声明的变量同时会被作为属性添加到顶级对象中。
可能聊到这儿,你会满屏的吐槽弹幕:这尼玛谁不知道?!但大叔真正想和你聊的就是这一点,这个小小的失误,就导致了使用 var
关键字声明的全局变量会污染全局对象的问题。
而 ES6 新增的 let
就很好地弥补了这个问题!也就是说,使用 let
关键字声明的全局变量不会污染全局对象。不信咱可以来试试嘛~ 还是刚才那个场景,仅仅把 var
改成 let
:
let a = 'a'
console.log(a)
console.log(window.a)
这段代码实际的运行结果就是 a 和 undefined。事实证明 let
有效地解决了 var
的问题,所以你知道为什么 ES6 要新增一个关键字来完成和 var
一样的事儿了吧?!
但是,let
就这么一点点和 var
的区别吗?答案肯定不是的。咱们继续聊 var
关键字,使用 var
声明的变量是允许重复声明的,像下面这段代码:
var a = 'a'
var a = 'aa'
console.log(a)
这段代码最终打印的结果是 aa,原因就在于 var
声明的变量是允许重复声明的。可能这会儿你又会问“这我也知道啊,有什么问题吗?”
问题肯定是有的,大叔还是给你带入一个场景,比如说你定义了一个 JavaScript 文件是需要被其他小伙伴导入使用的,那你在这个文件里面声明的变量在人家那分分钟被重新声明了,你内心是个什么感受?
当然,大叔只是举个例子~ 总而言之,就是说咱们在真实开发时对变量的命名肯定是有规划的,不能随意就被重新声明使用,这样会让命名空间很乱的。
你可能又想问,这个问题要怎么解决呢?答案其实很简单,就是使用 ES6 新增的这个 let
关键字。因为 let
关键字声明的变量是不允许被重复声明,否则会报错。不信你看看下面这段代码:
let a = 'a'
let a = 'aa'
console.log(a)
仅仅只是把 var
改成 let
,这个结果就报错了,报错的内容是:SyntaxError: Identifier 'a' has already been declared
,大概的意思就是变量 a
已经被声明过了。
所以,你看,let
可不是仅仅那么一点点的区别呢!
这会儿你是不是又想问 let
和 var
之间还有没有其他区别啊?大叔也不藏着掖着了,干脆一口气都和你说了吧!你知道使用 var
关键字声明的变量是允许声明提前的吗?什么?不知道!没事儿,这个简单,来看段代码:
console.log(a)
var a = 'a'
你运行一下这段代码,看看打印的结果是什么?没错~ 结果就是 undefined。为什么不是报错呢?原因就是使用 var
关键字声明的变量允许声明提前。还是说人话吧,也就是说,上面这段代码和下面这段代码本质上是没区别的:
var a
console.log(a)
a = 'a'
这样写你可能就明白了为啥打印的结果是 undefined 不是报错了吧!但是,咱们又得聊一聊 let
了,因为 let
声明的变量就不允许声明提前。不信的话还是给你看段代码先:
console.log(a)
let a = 'a'
这段代码运行之后打印的结果就是报错,报错的内容是:ReferenceError: Cannot access 'c' before initialization
,大概的意思就是无法在声明变量 c
之前访问变量 c
。
let
是不是挺屌的吧?!那你想不想知道 let
声明的变量又为啥不允许声明提前呢?嘿嘿~ 这是因为使用 let
声明变量的过程中存在一个叫做暂时性死区(Temporal dead zone,简称 TDZ)的概念。
是不是觉得挺高深的?哈哈~ 其实没啥高深的,大叔就给你弄明白这个事儿。规矩不变,咱还是先看段代码再说:
if (true) {
console.log(a)
let a;
console.log(a)
a = "a";
console.log(a)
}
大叔想先问问你这段代码里面三处打印的结果分别是什么?你得认真的想一想哈~ 这可都是大叔刚和你说过的内容。
- 第一处打印的结果是报错,报错内容就是
ReferenceError: Cannot access 'c' before initialization
- 第二处打印的结果是 undefined
- 第三处打印的结果是 a
对于这样的结果,大叔估计你应该会明白,毕竟都是刚说过的内容。接下来,你得认真的看了,因为大叔要和你来聊一聊有关暂时性死区的概念。
所谓的暂时性死区,就是说使用 let
关键字声明的变量直到执行定义语句时才会被初始化。也就是说,代码从顶部开始执行直到变量的定义语句,这个过程中这个变量都是不能被访问的,而这个过程就叫做暂时性死区。
具体到上面这段代码的话,实际上暂时性死区的开始和结束位置就像下面这段代码标注的这样:
if (true) {
// 暂时性死区开始
console.log(a); // 报错,ReferenceError: Cannot access 'a' before initialization
let a;
// 暂时性死区结束
console.log(a); // 输出undefined
a = "a";
console.log(a); // 输出a
}
聊到这会儿,大叔相信你应该可以明白什么是暂时性死区。其实,一些新的概念也没啥难理解的,主要是你理解的角度和方式的问题。
总体上来说,let
关键字要比 var
关键字严格了许多,导致我们开发时遇到的问题相应会减少许多。但 let
就没有任何问题了吗?答案显然不是的,大叔一直信奉一句话:任何技术都没有最优,只有最适合。
ES6 新增的 let
关键字也是如此,比如说刚才咱们讲过暂时性死区就有问题。什么问题呢?你还记得 JavaScript 里面有个运算符叫做 typeof
吧,就是用来判断原始数据类型的。这个运算符在 let
出现之前相对是比较安全的,说白了就是不容易报错。但let
出现之后就不一定了,比如说你把它用在刚才说的暂时性死区里面,它就会报错了:
if (true) {
console.log(typeof c)
let c;
}
这段代码最终打印的结果同样是报错,报错内容同样是:ReferenceError: Cannot access 'c' before initialization
。
关于 let
关键字咱们聊到这儿,其实基本上已经聊完了。但是,let
还有一个最重要的特性叫做块级作用域。聊到作用域想必你应该知道在 ES5 中存在两个:全局作用域和函数作用域,但在 ES6 中又新增了一个块级作用域。
想弄明白什么是块级作用域,咱就得从为什么需要块级作用域说起。规矩不变,还是先看段代码:
var a = "a"
function fn() {
console.log(a)
if (false) {
var a = "b"
}
}
fn()
你觉得这段代码运行之后打印的结果应该是什么?是 a?是 b?还是... ...?其实结果是 undefined。当然,这个结果不难得出,你运行一下就能看到。关键在于,为什么是这么个结果?!
因为就在于 ES5 只有全局作用域和函数作用域,上面这段代码的结果产生的原因就在于局部变量覆盖了全局变量。当然,还有比这更麻烦的问题,比如说下面这段代码:
for (var i = 0; i < 5; i++) {
console.log("循环内:" + i)
}
console.log("循环外:" + i)
是不是无比的熟悉?!不就是个 for
循环嘛!关键在哪?关键在于 for
循环结束之后,你会发现依旧能访问到变量 i
。这说明什么?说明变量 i
现在是一个全局变量。当然,你可能会说这没啥问题,毕竟之前一直不都是这个样子。
但是,大叔要和你说的是,现在不一样了,现在有块级作用域啦!什么是块级作用域?还是看段代码先:
if (true) {
let b = "b"
}
console.log(b)
这段代码运行之后打印的结果是报错,报错的内容是:SyntaxError: Lexical declaration cannot appear in a single-statement context
。
这说明什么?这就说明现在你使用 let
声明的变量在全局作用域中访问不到了,原因就是因为使用 let
声明的变量具有块级作用域。
接下来你的问题可能就是这个块级作用域在哪呢吧?其实这个块级作用域就是在花括号({}
)里面。比如说咱们现在把上面那个 for
循环的代码用 let
改造一下再看看:
for (let i = 0; i < 5; i++) {
console.log("循环内:" + i)
}
console.log("循环外:" + i)
改造完的这段代码运行之后的结果就是在循环结束后的打印结果是报错,报错内容大叔就不说了,因为都一个样。
弄明白了什么是块级作用域,接下来大叔就得和你聊一聊需要注意的事儿了。在使用 let
关键字声明块级作用域的变量时可必须在这对 {}
里面,不然同样也会报错的。
比如说咱们经常在使用 if
语句时爱把 {}
省略,但是如果 if
语句里面是使用 let
声明变量的话就不行了。不信来看段代码吧:
if (true) let c = 'c'
这段代码的运行结果同样是报错,而且报错内容都是一样的。可是不能忘记哦~
好了,弄明白什么是块级作用域,你是不是想问问这块级作用域有什么用处?大叔都想你心里面去了,嘿嘿~
你知道匿名自调函数吧?还记得怎么写一个匿名自调函数吗?
(function(){
var msg = 'this is IIFE.'
console.log(msg)
})()
还记得匿名自调函数的作用不?就是为了定义的变量和函数不污染全局命名空间?!有了 let
,有了块级作用域,上面这段匿名自调函数就可以写成这样的:
{
let msg = 'this is IIFE.'
console.log(msg)
}
简化了不少吧?!
好了,聊到这儿,ES6 新增的 let
关键字所有大叔想和你聊的都聊完了,也希望能对你有所帮助。最后再说一句:我是不想成熟的大叔,为前端学习不再枯燥、困难和迷茫而努力。你觉得这样学习前端技术有趣吗?有什么感受、想法,和好的建议可以在下面给大叔留言哦~