🚩前言
相信很多JavaScript初学者在学习的时候,肯定有被
var、let、const的这三个概念搞晕过吧? 本文从底层原理为你揭开这三者之间的差别,并且给出每一个的最佳实战,相信我,看完之后你会对JavaScript的这三个概念有一个很清晰的认识!并且能够熟练地使用!
📚一、前置知识
在正式开始讲这三者之间的区别之前,我会为你简单的介绍编译原理和作用域,作为前置知识,助于我们后续对var、let、const的理解。如果想要详细学习,推荐大家去看《你不知道的JavaScript(上卷)》。
1.编译原理
简单来说,任何JavaScript代码在执行前都要进行编译。只有在编译过后才会进入执行阶段。
比如,JavaScript首先会对var a = 2,这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
所以我们需要理解,代码写完后通常有两个阶段,第一个阶段是会编译你写完的代码,待编译完了之后才会进入第二个执行阶段。
2.作用域
作用域分为全局作用域和函数作用域。
全局作用域是在整个JS代码内,函数和代码块之外声明的变量,特点是任何地方都能访问。
而函数作用域是在函数内部声明的变量,只有函数内部能访问。
好了,对这两者有概念了之后,咱们就可以继续往下了。
🔼二、var的变量提升
我们知道var变量是ES6以前,变量的唯一声明方式。
var这个变量存在一个特性,叫做变量提升,它无时无刻都在存在于我们身边,只不过我们可能没有注意到。
下面给几个例子,能够很好地让我们看到这一点。
console.log(a);
var a = 1;
大家觉得这段代码控制台中会输出什么呢?
答案是undefined。
为什么是undefined呢?
因为我们的var变量会进行变量提升:在编译时期,我们的编译器老哥会先过一遍所有的代码,找到各个有var的部分,然后将这部分给放到代码的最前面,效果如下
var a;
console.log(a);
a = 1;
这段改变后的代码就是编译器在编译时期帮我们做的事情:把遇到的所有var声明都都提前放在整段代码前面,原来的var a = 1部分只留下了a = 1这个赋值操作。
在这个过程中:咱们的编译器将遇到的所有var声明都提前放在整段代码前面,原来的部分只留下赋值操作。这个过程就是我们常说的变量提升!
好了,至此编译阶段就算完成了,接下来就该是执行阶段了,这个阶段就该轮到我们的引擎出场了,引擎负责代码的执行阶段。
我们的引擎会执行编译器在变量提升后改变后的代码,输出a,此时a虽然声明了,但是还没有定义喔,于是,自然就会输出undefined:声明了但是未定义。
接着继续执行阶段,当执行到最后一句代码a = 1之后,咱们的执行阶段就结束啦。至此编译阶段和执行阶段就都结束了。
好了,现在你已经了解了代码的运行过程和变量提升了,实际上,咱们平时写的任何一段代码都会进行变量提升。而且提升的顺序也都是按照var的书写顺序进行的。
console.log(a,b,c)
var a = 2;
var b;
var c = 4;
进行变量提升之后
//按照顺序进行提升
var a;
var b;
var c;
console.log(a, b, c); // 输出: undefined undefined undefined
a = 2;
c = 4;
🎭函数的变量提升
我们之前学习的是var在全局作用域下的变量提升,接下来我们聊聊在函数中,var是怎么进行变量提升的
function fn(){
console.log(a); //undefined
var a = 1;
}
console.log(a) //ReferenceError: a is not defined
我们可以看到,在这段代码种,输出结果分别为undefined和报错ReferenceError: a is not defined,这是为什么呢???
首先我们会进行代码编译,我们的编译器老哥走一遍代码,找找哪里有var变量,这时找到函数里有var a = 1,于是就将var变量给提到函数作用域的最前面,注意!这里不是将var变量提到全局作用域的最前面喔,因为编译器一看,你这个var变量是声明在fn函数的作用域内的,好的,那我就将这个var的提升交给fn
啦,然后,依然留下a = 1,接着就交给执行阶段的引擎去处理啦。
这个编译器在编译时期将函数内的var提升到函数作用域最前面,原来的部分只留下赋值操作的过程,我们称为函数的变量提升!
变量提升后的代码如下:
// 变量提升后的代码
function fn() {
var a; // 声明提升到函数顶部
console.log(a); //undefined
a = 1;
}
console.log(a); //ReferenceError
至此咱们的编译器就完成了编译任务啦,然后交给引擎,然后引擎就开始进行执行阶段,在第一个console.log(a)时,由于已经声明了a,只不过没赋值,于是输出undefined,在第二个console.log(a)时,在全局作用域里找不着a呀,于是就报错ReferenceError
🧱三、块级作用域let、const
之前我们提到了,在ES6之前,咱们的JavaScript只存在var这一种声明方式。
接下来让我们看看这个场景:
for(var i = 1 ; i <= 5 ; i ++)
{
console.log("我爱学前端!")
}
console.log(i);
这个场景我们会输出5遍我爱学前端,此外还会输出i为6。
让我们仔细想想,这里该出现i为6吗?
我们的目的是不是为了输出五遍”我爱学前端!“?
而这个i只是放在循环体里面的一个暂时的变量,它的目的就是帮我们控制输出五遍for循环里的内容,之后再退出for循环!
但是对于这样的值,它应该在for循环结束后就完成了它的使命,随着for循环的退出,它也应该离开。
但是在js的ES6之前,由于只有var这样的全局变量,导致了i在循环退出后,还是会输出i为6 ,这样的问题我们也称为变量污染!
其实在ES6之前就有大量开发者吐槽JS这种设置的不合理性。于是在ES6诞生之后,引入了新的变量声明方式let、const帮我们解决了这个问题!
🔹let声明
接下来,我将为你仔细介绍let声明的特点和使用场景!
🧩1.块级作用域
在ES6中,let一般是声明在代码块{}内的,声明后变量会在块级作用域内,因此let只在当前代码块{}中有效
好的,让我们对刚刚的代码进行修改,将var i = 1改为let i = 1
for(let i = 1 ; i <= 5 ; i ++)
{
console.log("我爱学前端!")
}
console.log(i);
我们会发现,此时同样会输出五遍”我爱学前端!“,此外,由于外部作用域无法访问块级变量i,因此会抛出Uncaught ReferenceError: i is not defined。我们发现i变量在for循环结束后,就完成了它的使命,不会对外部作用域进行污染,达到了我们想要的效果!
🚫2.不会被提升
在ES6中,let声明的变量不会在编译阶段被提升,存在暂时性死区(TDZ):
接下来让我们举个例子~
console.log(a); //ReferenceError: x is not defined
let a = 1;
大家会发现这段代码和之前我们在讲var变量提升的时候很像对不对,但是这里的结果并不是undefined,而是ReferenceError: x is not defined,因为let声明的变量不会在编译阶段被提升,会存在暂时性死区,暂时性死区是指从代码开头到咱们声明let的这段代码里,不允许出现对let变量的使用,否则会报错
⛔3.不能重复声明
当我们在重复声明let变量时
let name = "Alice";
let name = "Bob"; //SyntaxError: Identifier 'name' has already been declared
会出现报错SyntaxError: Identifier 'name' has already been declared,这是因为let不允许重复声明,而var可以重复声明。
所以咱们以后在写代码的时候一定要注意:
- 在使用块级作用域的时候,一定要使用
let,避免变量污染 let变量的使用不要在let的声明之前- 拒绝重复声明
let变量
🔒const声明
在ES6时,咱们也引入了const,const 用于定义不可重新赋值的变量。下面介绍const的特点。
🔄1.声明后不能重新赋值
const一旦声明之后,就不能再对其进行赋值了。
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable
如果进行重新赋值的话,会进行报错TypeError: Assignment to constant variable
🏁2.必须初始化
const在声明时,必须指定其值
const NAME; // SyntaxError: Missing initializer in const declaration
const NAME = "Alice"; // ✅
并且const在使用时,和let一样,也是用于块级作用域、存在暂时性死区的。所以我们也要选择合适的场景下使用const呢
💡在函数中使用let和const
刚刚我们提到的都是在全局作用域中使用let和const,接下来就让我们来试试在函数中使用let和const吧
💡首先是在函数中使用
let
function fn(){
console.log(a);
let a = 1;
}
此时我们发现,访问后会直接报错Uncaught ReferenceError: Cannot access 'a' before initialization,这是因为我们之前提到过的暂时性死区,我们在函数作用域内,在let声明前就对a进行了访问,即我们访问了暂时性死区,就会直接报错。
所以在函数内使用let实际上是和在全局内使用let是相似的,都需要避免暂时性死区的问题,以后的使用中,必须注意要在声明后再进行使用
//正确使用规范
function checkUser(age) {
let message; // 先声明,后赋值
if (age >= 18) {
message = "成年人"; // 块内赋值
} else {
message = "未成年人";
}
}
💡接下来,来介绍一下
const在函数内的使用
1. 未初始化(直接报错)
function getValue() {
const PI; // ❌ SyntaxError: Missing initializer
PI = 3.14;
}
2. 尝试重新赋值(报错)
function updateCount() {
const count = 0;
count = 1; // ❌ TypeError: Assignment to constant
}
3. 在块内重复声明(报错)
function checkScope() {
const x = 1;
if (true) {
const x = 2; // ✅ 允许(不同块级作用域)
const x = 3; // ❌ SyntaxError: Identifier 'x' already declared
}
}
所以我们可以看出,const在函数内的使用其实和在全局作用域内的一些注意的点是相同的,只不过在函数作用域内,咱们要注意,不要在函数作用域外直接访问函数作用域内的const声明的变量即可~
📝四、总结
本文主要介绍了var,let,const的工作原理和使用技巧,很多内容作者是通过阅读《你不知道的JavaScript》之后为大家讲解的,如果想要了解关于var、let、const更详细的知识的话,也欢迎大家去阅读原著。希望阅读完本文能对你有帮助!
🌇结尾
本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)》
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。