本文首发于个人网站:let关键字:加强版的var关键字
你好,今天大叔想和你唠扯唠扯 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
这个运算符来做个骚操作了 —— 先用 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
关键字声明的全局变量不会污染全局对象。不信咱可以来试试嘛~ 还是刚才那个场景,在一个 HTML 页面中定义 JavaScript 代码,仅仅把 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
声明的变量是允许重复声明的。可能这会儿你又会问了,这我也知道啊,有啥子问题吗?
问题肯定是有滴,要是没有大叔花这么多口舌和你在这儿叨逼叨干啥啊~ 大叔还是给你带入一个场景,比方说你定义了一个 JS 文件是需要被其他小伙伴导入使用滴,那你在这个文件里面声明的变量在人家那分分钟被重新声明了,你内心是个啥感受?
当然了,大叔就是举个栗子,你也别太当真啦~ 总而言之,就是说咱们在真实开发时对变量的命名肯定是有规划的,不能随意就被重新声明使用,这样会让命名空间很乱很乱滴。
你可能有想问了,这个问题要怎么解决呢?答案其实很简单,就是使用 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
。
暂时性死区(TDZ)
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
- 第三处打印的结果是 b
对于这样的结果,大叔估计你应该会明白,毕竟都是刚唠过的内容。接下来,你得认真的看了,因为大叔要和你来唠扯有关暂时性死区的概念了~
所谓的暂时性死区,就是说使用 let
关键字声明的变量直到执行定义语句时才会被初始化。也就是说,从代码从顶部开始执行直到变量的定义语句执行,这个过程中这个变量都是不能被访问的,而这个过程就被叫做暂时性死区。
具体到上面这段代码的话,实际上暂时性死区的开始和结束位置就像下面这段代码标注的一样婶儿:
if (true) {
// 暂时性死区开始
console.log(a); // 报错,ReferenceError: Cannot access 'a' before initialization
let a;
// 暂时性死区结束
console.log(a); // 输出undefined
a = "a";
console.log(a); // 输出a
}
捞到这会儿,大叔相信你应该可以明白啥子是暂时性死区了。其实啊,一些新的概念也没啥难理解的,主要是你理解的角度和方式的问题。
typeof
运算符也不再安全
总体上来说,let
关键字要比 var
关键字严格了许多,导致我们开发时遇到的问题相应会减少许多。但 let
就没有任何问题了吗?答案显然不是滴,大叔一直信奉一句话:任何技术都没有最优,只有最适合。
ES6 新增的 let
关键字也是如此,就比方说刚才咱们捞的暂时性死区的内容,其实就有问题。啥问题呢?你还记得 JS 里面有个运算符叫做 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
关键字所有大叔想和你唠扯的内容都唠扯完了,也希望能对你有所帮助。最后再说一句:我是不想成熟的大叔,为前端学习不再枯燥、困难和迷茫而努力。你觉得这样学习前端技术有趣吗?有什么感受、想法,和好的建议可以在下面给大叔留言哦~