Introduction
var
, let
and const
是JavaScript中三种定义变量的方式,它们之间有什么区别呢?这是面试中常见的一道题,今天我们来一文说透它。let
和const
区别不大,主要是const
声明的变量是常量,不可修改,而let
声明的变量是可修改的。所以我们重点放在var
和let
上。
Initialization
声明变量的同时为其赋值叫做初始化。
var
和let
声明的变量都可以不赋值,此时变量的值为undefined
。const
声明的变量必须赋值,否则会报错。
var
和let
声明的变量可以不赋值,此时变量的值为undefined
。
1 | var num; // num is undefined |
const
声明的变量必须赋值,否则会报错。
1 | const a; // SyntaxError: Missing initializer in const declaration |
Hoisting
Hoisting
这个词中文译为提升,就是将变量的声明提升到其作用域的顶部,注意提升的是声明,而不是赋值。
var
声明的变量会被提升至其作用域顶部。let
和const
声明的变量不会被提升。(注意这个说法有争议,详见MDN)- 提升只针对变量声明,不包括赋值。
如果var是在全局作用域声明的,那么它会被提升到全局作用域的顶部。
1 | console.log(name); // undefined |
以上代码等价于:
1 | var name; // `var`声明的变量会被提升到其作用域顶部。 |
如果var是在函数作用域声明的,那么它会被提升到函数作用域的顶部。
1 | function printName() { |
以上代码等价于:
1 | function printName() { |
let
和const
声明的变量不会被提升。
对于let
和const
,它们不会被提升,所以下面代码会报错。
1 | console.log(num); // ReferenceError: Cannot access 'num' before initialization |
前面说过,关于let
和const
是否被提升有争议。
- 一种说法是
let
和const
不会被提升,所以在声明之前访问会报错。 - 另一种说法是
let
和const
会被提升,但是在声明之前访问会抛出Temporal Dead Zone
错误。
比如下面的代码:
这段代码会报错,但是如果我们把{}
内的const x = 2;
注释掉,那么代码就不会报错。如果const x = 2
没有被提升的话,那么console.log(x)
应该可以访问到全局的x
,而不会报错。换句话说:因为const x = 2
被提升了,所以console.log(x)
访问的是提升后的x
,而此时x
还没有被初始化,所以报错。
1 | const x = 1; |
提升只针对变量声明,不包括赋值。
下面的代码会报错,因为x = 1是赋值,并不是声明,所以不会提升。(注意:如果变量声明前没有加var
, let
或const
,那么其实产生的是一个意外的全局变量。)
1 | console.log(x); // ReferenceError: x is not defined |
如果有同名函数和变量,那么提升后,变量位于函数之前(或者说函数会覆盖变量)。
以下代码中有一个同名的函数和变量。
1 | console.log(foo); // [Function: foo], not undefined. |
提升后代码如下:
1 | var foo; |
面试题
看几道面试题,以下几段代码输出什么?
- 第一题
1 | a = 2; |
解决var提升的问题很简单,就是按照提升规则将代码重写一下,上面的代码等价于如下代码,结果一目了然。
1 | var a; |
- 第二题
1 | var a = true; |
只要函数内部有var
声明的变量,那么所有全局声明的var变量都会被忽略,以上代码提升后等价于如下代码(注意function也有提升),函数内部的var永远会覆盖全局的var。
1 | var a = true; |
- 第三题
1 | function fn() { |
还是那句话,此类题目的解法就是按照提升规则把代码重新写一遍,以上代码提升后等价于如下代码:
1 | function fn() { |
所以输出结果是function
和string
。
Scope
var
声明的变量有只两种作用域:全局作用域和函数作用域。(没有块级作用域)let
和const
声明的变量有三种作用域:全局作用域,函数作用域和块级作用域。var
声明的全局变量会挂载到window
对象上,而let
和const
不会。let
和const
有临时性死区,而var
没有。
面试题
第一题
以下代码输出什么?
1 | let x = 1; |
答案:1,因为let
有块级作用域,所以let x = 2
只在{}
内有效。
第二题
以下代码输出什么?
1 | var x = 1; |
答案:2,因为var
没有块级作用域,所以var x = 2
会覆盖外部的var x = 1
。
第三题
以下代码输出什么?
1 | let name = 'zdd'; |
答案:ReferenceError: Cannot access ‘name’ before initialization。因为let
有块级作用域,所以console.log(name);
访问的是let name = 'Philip';
之前的name
,而此时name
还没有被初始化,此时name
处于暂时性死区中,所以报错。
第四题
以下代码输出什么?
1 | ; |
答案:ReferenceError: foo is not defined。因为foo
是在块级作用域内声明的,所以在外部无法访问。但是如果我们把'use strict';
去掉,那么代码就可以正常运行。因为在非严格模式下,函数声明会被提升到全局作用域。
第五题
以下代码输出什么?
1 | (() => { |
答案:1 undefined
2。因为catch
中的x
是一个新的变量,不是外部的x
,所以x = 1
只会改变catch
中的x
,而不会改变外部的x
。而y = 2
不是catch
的参数,只是在catch
中赋值的,所以会改变外部的y
。
Temporal Dead Zone
TDZ即Temporal Dead Zone
- 中文名暂时性死区,是指let
和const
声明的变量在其作用域开始到变量声明之间的这段区域。在暂时性死区内无法访问变量,访问会报错。
1 | function foo() { |
对于以上代码,常量b的暂时性死区开始于函数的第一行,终止于b的声明,而console.log(b);
这句恰恰在暂时性死区内访问了b,所以会报错。
面试题
以下代码输出什么?
1 | function foo() { |
答案:ReferenceError: Cannot access 'bar' before initialization
因为console.log(typeof bar);
这句在bar
的暂时性死区内访问了bar
,所以会报错。可以看到,即使强如typeof
这种几乎不会报错的操作符也无法规避暂时性死区。
如果我们把const bar = 1;
去掉,那么代码就不会报错。typeof
操作符对于没有声明的变量不会报错,而是返回undefined
。
1 | function foo() { |
Redeclaration
var
声明的变量可以被重复声明,后声明的覆盖先声明的。let
和const
声明的变量不可以被重复声明。
面试题
看几道面试题,以下几段代码输出什么?
- 第一题
1 | var a = 1; |
答案:3 3 1, 这个题主要考察两个知识点:
var
声明的变量没有块级作用域。var
声明的变量可以被重复声明,后声明的会覆盖先声明的。
所以var a = 3
会覆盖外部的var a = 2
,但是var a = 2
不会覆盖最外面的var a = 1
。因为var
有函数作用域。
以上代码提升后等价于如下代码:
1 | var a; |
注意:面试题中凡事用{}
包裹var
的都是障眼法,var
没有块级作用域。
第二题
以下代码输出2, 1。因为let有块级作用域。let a = 2
只在{}
内有效。
1 | function foo() { |
Accidental Global Variables
如果我们声明变量的时候忘记了写var
, let
或者const
,那么这个变量就是所谓的Accidental Global Variables
,意思是意外的全局变量
。
1 | function f1() { |
面试题
以下代码输出什么?
1 | for (var i = 0; i < 10; i++) { |
答案:3 3 3
因为var
没有块级作用域,所以setTimeout
内的i
都是指向同一个i
,而setTimeout
是异步的,其回调函数代码需要先进入宏任务队列,待for
循环结束后才能执行,此时i
已经是3了。关于这道题的详细解释,请看这篇。
Best Practices
如今ES6已经普及,对于业务代码来说,基本不需要使用
var
了,var
目前只有JS框架或者底层工具库才会使用。对于
let
和const
,优先使用const
,只有在需要修改变量的情况下才使用let
。经典for循环使用
let
,因为循环变量会被修改。1
2
3for (let i = 0; i < 5; i++) {
console.log(i);
}for...in
和for...of
使用const
,因为循环变量不会被修改。
for ... of
1
2
3
4const arr = [1, 2, 3];
for (const item of arr) {
console.log(item);
}for ... in
1
2
3
4const obj = {a: 1, b: 2};
for (const key in obj) {
console.log(key);
}