序言
这是一道非常简单的面试题,但是想要回答好它却不容易。
从实用性的角度出发,var, let, const
都是js中用来声明变量的关键字。其中 var
和let
用来定义变量, const
用来声明常量。其中let
和const
是在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 这两个赋值操作找到一个目标。就好像《让子弹飞》一样
当然了,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)
这段代码是我在网上看到的,刚看到我就产生了一个疑问,为什么要定义一个没用的函数?
后来发现,定义这段函数只是为了引出全局变量
我们可以看到,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存在很多有趣的语法问题,有时候简单的回答并不能很好的解释原因。我们要知其然更知其所以然
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。