一文帮你搞懂var、let、const!

379 阅读10分钟

🚩前言

相信很多JavaScript初学者在学习的时候,肯定有被varletconst的这三个概念搞晕过吧? 本文从底层原理为你揭开这三者之间的差别,并且给出每一个的最佳实战,相信我,看完之后你会对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时,咱们也引入了constconst 用于定义不可重新赋值的变量。下面介绍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

刚刚我们提到的都是在全局作用域中使用letconst,接下来就让我们来试试在函数中使用letconst

💡首先是在函数中使用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》之后为大家讲解的,如果想要了解关于varletconst更详细的知识的话,也欢迎大家去阅读原著。希望阅读完本文能对你有帮助!



🌇结尾

本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。