关于let,const,var,function,class变量提升的不同解释

197 阅读11分钟

我们在网络上搜let,const,var它们三者的区别,大部分都会提到var具有变量提升的性质, 例如,网上大部分的讲解如下

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

正常来说,你在声明一个变量前,去访问他,肯定是访问不到的,会出现 a is not defined,但是由于变量提升机制,会把 var a 这个声明提到最前面了,所以当执行到console.log(a)的时候,它能找到 a 这个变量,只是这个时候 a 还没有被赋值,所以它的值是 undefined。 而let和const不存在变量提升,只存在暂存性死区。

注意⚠️,本文章将会给用不一样观点和角度来谈他们三者的区别,看完大约耗费你十分钟,绝对会给您带来不一样的感受,请您细细看完,并点上一个珍贵的赞👍!

拉开序幕

上述代码表现更像下面的代码

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

但实际上,代码并没有被改变,上面的代码只是我们猜测的,其实Javascript引擎在执行这几行代码的时候并没有移动或是改变代码的结果。到底发生了什么呢? 这里就是发生了变量提升

变量提升

在代码的编译期间,即代码真正执行的瞬息之间,引擎会将代码块中所有的变量声明和函数声明都记录下来。这些函数声明和变量声明都会被记录在一个名为词法环境的数据结构中。词法环境是Javascript引擎中一种记录变量和函数声明的数据结构,它会被直接保存在内存中。所以,上面的console.log(a)可以正常执行。

简而言之,变量提升指的是 JavaScript 会先处理声明部分(把变量声明放到作用域的顶部),然后再处理赋值。

什么是词法环境

所谓词法环境就是一种标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,变量是对实际对象[包含函数和数组类型的对象]或基础数据类型的引用)。简单地说,词法环境是Javascript引擎用来存储变量和对象引用的地方。 想要了解更多有关词法环境的细节可以看这位博主的文章彻底搞懂作用域、执行上下文、词法环境相信很多小伙伴在初学JavaScript的时候会经常对作用域,执行上下文,词法环境等 - 掘金

词法环境的结构用伪代码表示如下:

LexicalEnvironment = {
  Identifier:  <value>,
  Identifier:  <function object>
}

通过函数声明来加强对词法环境和提升的理解

function声明提升

helloWorld(); 
function helloWorld(){
  console.log('Hello World!');
}

运行后会打印出Hello World!

我们已经知道了,函数声明会在编译阶段就会被记录在词法环境中并且保存在内存中,因此我们可以在函数进行实际声明之前对该函数进行访问。

上面函数声明保存在词法环境中像下面这样:

lexicalEnvironment = {
  helloWorld: < func >
}

所以在代码执行阶段,当Javascript引擎碰到helloWorld()这行代码,会在词法环境中寻找,然后找到这个函数并执行它。

函数表达式

注意,只有函数声明才会被直接提升,使用函数表达式声明的函数不会被提升⚠️

看下面代码:

helloWorld();  // TypeError: helloWorld is not a function
var helloWorld = function(){
  console.log('Hello World!');
}

如上,代码报错了。使用var声明的helloWorld是个变量,并不是函数,Javascript引擎只会把它当成普通的变量来处理,而不会在词法环境中给它赋值

保存在词法环境中像下面这样:

lexicalEnvironment = {
  helloWorld: undefined
}

上面的代码要想可以正常运行改写如下即可:

var helloWorld = function(){
  console.log('Hello World!');
}
helloWorld();  // 打印 'Hello World!'

看到这里相必你已经对Javascript引擎以及词法环境有一定的理解了,,很棒!😊。现在让我们回过头来看var,let和const,一起坚持下去吧,你会看到不一样的世界💪⛽️!

var变量提升

看一个使用var声明变量的例子:

console.log(a); // 打印 'undefined'
var a = 1;

如果按上面function函数声明的方式去理解,这里应该打印3,但实际上打印了undefined

请记住: 所谓的声明提升只是在编译阶段Javascript引擎将函数声明和变量声明存储在词法环境中,但不会给它们赋值😡。等到了执行阶段,真正执行到赋值那一行的时候,词法环境才会更新

但上面的代码为什么打印了undefined🤔呢?

Javascript引擎会在编译阶段将使用var声明的变量保存在词法环境中,并将它初始化为undefined。到了执行阶段,等执行到赋值那一行代码的时候,词法环境中变量的值才会被更新

所以上面代码的词法环境初始化像下面这样:

lexicalEnvironment = {
  a: undefined
}

这也解释了为什么前面使用函数表达式声明的函数执行会报错,为什么上面的代码会打印undefined。当代码执行到var a = 1;这行代码的时候,词法环境中a的值就会被更新,此时词法环境会被更新如下:

lexicalEnvironment = {
  a: 1
}

let和const变量提升

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

输出:

Uncaught ReferenceError: Cannot access 'a' before initialization

再看一个使用const声明变量的例子:

console.log(b);
const b = 1;

输出:

Uncaught ReferenceError: Cannot access 'b' before initialization

var不同,相同结构的代码换成let或是const都直接报错了。

难道使用letconst声明的变量不存在变量提升的情况么🤔?

实际上,在Javascript中所有声明的变量(var,const,let,function,class)都存在变量提升的情况😮。使用var声明的变量,在词法环境中会被初始化为undefined,但用letconst声明的变量并不会被初始化!!⚠️。

使用letconst声明的变量和var一样只有在执行到赋值那行代码的时候才会真正给他赋值,这也意味着在执行到变量声明的那行代码之前访问那个变量都会报错,这就是我们常说的暂时性死区(TDZ)

暂时性死区,简单来说就是:在变量声明之前都不能对变量进行访问。

当执行到变量声明的那一行的时候,但是仍然没有赋值,那么使用let声明的变量就会被初始化为undefined;使用const声明的变量就会报错; 看实际的例子:

let a;
console.log(a); // 输出 undefined
a = 5;

在代码编译阶段,Javascript引擎会把变量a存储在词法环境中,并把a保持在未初始化的状态。此时词法环境像下面这样:

lexicalEnvironment = {
  a: <uninitialized>
}

此时如果尝试访问变量a或是b,Javascript引擎会在词法环境中找到该变量,但此时变量处于未初始化的状态,因此会抛出一个引用错误。

然后在执行阶段,Javascript引擎执行到赋值(专业点叫词法绑定)那一行的时候,会评估被赋值的值,如果没有被赋值,只是简单的声明,此时就会给let声明的变量赋值为undefined;此时词法环境像下面这样:

lexicalEnvironment = {
  a: undefined
}

当执行到a = 5这一行的时候,词法环境再次更新:

lexicalEnvironment = {
  a: 5
}

再看下使用const声明代码的情况:

let a;
console.log(a);
a = 5;
const b;
console.log(b);

输出:

Uncaught SyntaxError: Missing initializer in const declaration

上面代码直接报错,a的值也没有打印,直接报错,其实是代码在编译阶段就已经报错了,压根没执行到console.log(a);这一行代码。 原因

javaScript 的代码执行分为两个主要阶段:

  1. 编译阶段:这是解析和检查代码的阶段,包括变量提升(Hoisting)、语法检查等。错误会在这个阶段被捕获,代码如果存在错误,后续的执行会被停止。
  2. 执行阶段:这是 JavaScript 实际执行代码的阶段。在这个阶段,变量和函数会按照编写的顺序执行。

在这段代码中,const b; 这一行因为缺少初始化导致了语法错误,导致 JavaScript 引擎在 编译阶段 就抛出了错误。因此,console.log(a) 这一行实际上并不会执行,代码会在遇到错误时提前中断

这里我们可以得出一个结论,let声明的变量如果没有赋值,Javascript引擎执行到赋值操作的时候它会被初始化一个值为**undefined,而被const声明的变量不会被初始化,必须要给它一个值,否则会在编译阶段就会出错。

let 和 const不存在变量提升证明

我们看这串代码

let a = 1 
{ 
console.log(a) 
let a = 2 
} //ReferenceError
    • { console.log(a); let a = 2; }这个代码块中,当执行到console.log(a)时,JavaScript 引擎会先在当前块级作用域中寻找变量a的声明。因为已经遇到了let a = 2这个声明(虽然还没有执行到赋值这一步),所以它会认为这个块级作用域中有一个a,并且由于let的暂时性死区(TDZ)特性,在变量声明语句之前访问变量是不被允许的,会直接抛出ReferenceError,而不会去访问外部作用域中的a
  1. 如果有变量提升的话,那么打印出的应该是undefined

注意: 在函数中,只要是能在变量声明之后引用该变量就不会报错。

什么意思呢?看如下代码:

function foo () {
  console.log(a);
}
let a = 20;
foo(); // 打印 20

但下面代码就会报错:

function foo () {
  console.log(a);
}
foo();
let a = 20; // 报错: Uncaught ReferenceError: Cannot access 'a' before initialization

这里报错的原因需要结合Javascript中的执行上下文和执行栈才能理解,因为此时全局执行上下文中词法环境中保存的变量a处于未初始化的状态,调用foo函数,创建了一个函数执行上下文,然后函数foo执行过程对全局执行上下文的变量a进行访问,但a还处于未初始化的状态(此时let a = 20还没有执行)。因此报错。

class声明提升

letconst类似,使用class声明的类也会被提升,然后这个类声明会被保存在词法环境中但处于未初始化的状态,直到执行到变量赋值那一行代码,才会被初始化。另外,class声明的类一样存在暂时性死区(TDZ) 。看例子:

let peter = new Person('zhangsan', 25); 
console.log(peter);
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

打印:

Uncaught ReferenceError: Cannot access 'Person' before initialization

改写如下就可以正常运行了:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('zhangsan', 25); 
console.log(peter);
// Person { name: 'zhangsan', age: 25 }

上面代码在编译阶段,词法环境像这样:

lexicalEnvironment = {
  Person: <uninitialized>
}

然后执行到class声明的那一行代码,此时词法环境像下面这样:

lexicalEnvironment = {
  Person: <Person object>
}

注意: 使用构造函数实例化对象并不会报错:

let peter = new Person('zhangsan', 25);
console.log(peter);
function Person(name, age) {

    this.name = name;
    this.age = age;
}
// Person { name: 'Peter', age: 25 }

上面代码正常运行。

类表达式

和函数表达式一样,类表达式也一样会被提升,比如:

let peter = new Person('zhangsan', 25);
console.log(peter);
let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

报错:

Uncaught ReferenceError: Cannot access 'Person' before initialization

要想正常运行,改写如下即可:

let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

也就是说不管是函数表达式还是类表达式遵循的规则和变量声明是一样的。

结论

  • var:存在变量提升,在编译阶段会被初始化为undefined
  • let: 不存在变量提升,存在暂时性死区(TDZ),编译阶段不会被初始化为undefined,执行阶段,如果没赋值,则初始化为undefined
  • const: 不存在变量提升,存在暂时性死区(TDZ),如果没有赋值,编译阶段就会报错;
  • function:存在变量提升,在变量声明之前可以访问并执行;
  • class: 存在变量提升,存在暂时性死区(TDZ);

以上都是个人学习得出的结论,如果有错误,请各位大佬在评论区指出,我会细心学习并且改正!🙏