2

序言

这是一道非常简单的面试题,但是想要回答好它却不容易。

从实用性的角度出发,var, let, const 都是js中用来声明变量的关键字。其中 varlet用来定义变量, const用来声明常量。其中letconst是在ES6中新增的。

问题到这里还远远没有结束,真正的考验才刚刚开始

变量提升

这是大家看到 var 和 let 最容易想到的点。那么什么是变量提升呢?

先放一个MDN官方定义在这里

变量提升(Hoisting)被认为是Javascript中执行上下文 (特别是创建和执行阶段)工作方式的一种认识。

实际上来讲,变量提升就是指在变量声明之前访问该变量。

那么为什么会产生变量提升?因为JavaScript 虽然是动态语言,但确实是拥有静态语义的。而在 JavaScript 的早期,这个静态语义其实并没有处理得太好,就导致了变量提升的存在。

光是文字有点绕,我们先来看一段代码:

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

为什么在我们定义a之前,a就被使用了,代码却没有报错,而且结果输出了undefined?

你可以说var的默认值是undefined,但这不够严谨,我们需要从头说起。

在传统的编译语言中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析(Tokenizing/Lexing)
  • 解析/语法分析(Parsing)
  • 代码生成

比起那些编译过程只有三个步骤的语言的编译器,JavaScript引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

摘录来自: “你不知道的JavaScript”

那么我们在上面这段代码得到执行前,在代码的预编译阶段,var a 就已经由静态分析得到了。

那么随之而来就产生了第二个问题,为什么这里的a不是1而是undefined呢?

因为变量提升的本质的原因是声明是在语法分析阶段就处理的, 如下所示:

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

// 变量提升后

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

PS: 这里存在一个LHS和RHS的概念,也是一道面试题。

在预编译完之后,javascript的编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 1这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果不是,引擎会沿作用域链继续查找该变量;如果查到根作用域也没有查到,会自动声明a。

那么这样的话,为什么let会抛出异常呢?

这个问题下面会有答案

let 和 var 还有什么区别?

重复声明

除了变量提升,我们还需要探讨var和let重复声明的问题。

先来看这样一段代码:

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

// ------------

let a = 1;
console.log(a);
let a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared

const a = 1;
console.log(a);
const a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared

为什么var在这里可以声明两次?其实如果搞明白刚刚讲的编译过程,就很好解释了。这些都是在语法分析阶段处理的结果。

预编译时候的LHS(Left Hand Side)并不关心当前a的值是什么,只是想要为 =1 和 = 2 这两个赋值操作找到一个目标。就好像《让子弹飞》一样

image.png

当然了,let这种更符合一般编程习惯的做法更值得提倡。


暂时性死区

在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。

用代码解释就是:

console.log(a) // ReferenceError: a is not defined
let a

ES6标准中对let/const声明中的解释是:

The variables are created when their containing Lexical Environment is instantiated but may not be accessed inany way until the variable’s LexicalBinding is evaluated.

这其实对应了var的变量提升。当程序的控制流程在新的作用域(module function 或 block 作用域)进行实例化时,在此作用域中用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。

换句话说,var和let,const这些标识符都是在用户代码执行之前就已经由静态分析得到,并且创建在环境中,它们都是在读取一个“已经存在”的标识符名。而let之所以会抛出异常,不是因为它不存在,而是因为这个标识符被拒绝访问了。


全局属性:

我们都知道,浏览器的全局对象是 window,Node 的全局对象是 global。

var在全局环境声明变量,会在全局对象里新建一个属性,而 let 在全局环境声明变量,则不会在全局对象里新建一个属性。

var foo = 'global'
let bar = 'global'

console.log(this.foo) // global
console.log(this.bar) // undefined

那么我们随之而来就会产生一个疑问,let的全局变量到底去了哪里?
我们来运行这样一段代码:

var foo = 'global'
let bar = 'global'

function test() {}

console.dir(test)
这段代码是我在网上看到的,刚看到我就产生了一个疑问,为什么要定义一个没用的函数?
后来发现,定义这段函数只是为了引出全局变量
1646283077(1).png

我们可以看到,bar所处的位置是与window同级,也就是[[Scopes]][0]: Script这个变量对象的属性中,而[[Scopes]][1]: Global就是我们常说的全局对象。


块级作用域

块作用域由 { }包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:
• 内层变量可能覆盖外层变量
• 用来计数的循环变量泄露为全局变量

用 var 声明的变量的作用域是它当前的执行上下文,即如果是在任何函数外面,则是全局执行上下文,如果在函数里面,则是当前函数执行上下文。换句话说,var 声明的变量的作用域只能是全局或者整个函数块的。

而 let 声明的变量的作用域则是它当前所处代码块,即它的作用域既可以是全局或者整个函数块,也可以是 if、while、switch等用{}限定的代码块。

另外,var 和 let 的作用域规则都是一样的,其声明的变量只在其声明的块或子块中可用。

示例代码:

function varTest() {
  var a = 1;

  {
    var a = 2; // 函数块中,同一个变量
    console.log(a); // 2
  }

  console.log(a); // 2
}

function letTest() {
  let a = 1;

  {
    let a = 2; // 代码块中,新的变量
    console.log(a); // 2
  }

  console.log(a); // 1
}

varTest();
letTest();

从上述示例中可以看出,let 声明的变量的作用域可以比 var 声明的变量的作用域有更小的限定范围,更具灵活。

总结

Javascript存在很多有趣的语法问题,有时候简单的回答并不能很好的解释原因。我们要知其然更知其所以然


magiconch
150 声望1 粉丝

在你面前的是: 数据可视化菜鸡 Python业余选手, 国服最菜切图仔.