let 、var、const

960 阅读10分钟

1.var

在 ES6 之前我们都是通过 var 关键字定义 JavaScript 变量。ES6 才新增了 let 和 const 关键字

var num = 1

在全局作用域下使用 var 声明一个变量,默认它是挂载在顶层对象 window 对象下(Node 是 global)

var num = 1
console.log(window.num) // 1

用 var 声明的变量的作用域是它当前的执行上下文,可以是函数也可以是全局

var x = 1 // 声明在全局作用域下

function foo() {
    var x = 2 // 声明在 foo 函数作用域下
    console.log(x) // 2
}
foo()
console.log(x) // 1

如果在 foo 没有声明 x ,而是赋值,则赋值的是 foo 外层作用域下的 x 

var x = 1 // 声明在全局作用域下

function foo() {

    x = 2 // 赋值

    console.log(x) // 2

}

foo()

console.log(x) // 2

如果赋值给未声明的变量,该变量会被隐式地创建为全局变量(它将成为顶层对象的属性)

a = 2
console.log(window.a) // 2
function foo(){
    b = 3
}
foo()
console.log(window.b) // 3

var 缺陷一:所有未声明直接赋值的变量都会自动挂在顶层对象下,造成全局环境变量不可控、混乱

变量提升(hoisted)

使用var声明的变量存在变量提升的情况

console.log(b) // undefined
var b = 3
注意,提升仅仅是变量声明,不会影响其值的初始化,可以与隐式的理解为:
var b
console.log(b) // undefined
b = 3

作用域规则

var 声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问,包含它的代码块对此没有什么影响,所以多次声明同一个变量并不会报错:

var x = 1
var x = 2

这种作用域规则可能会引发一些错误

function sumArr(arrList) {
    var sum = 0;
    for (var i = 0; i < arrList.length; i++) {
        var arr = arrList[i];
        for (var i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
    return sum;
}

这里很容易看出一些问题,里层的 for 循环会覆盖变量 i,因为所有 i 都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。

var 缺陷二:允许多次声明同一变量而不报错,造成代码不容易维护

捕获变量怪异之处

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

i 是全局变量,全局只有一个变量i ,所有i都用同一个引用,前一个i会被后面的i覆盖掉, for 循环结束时, i=10 ,所以 a 【6】( ) 也为 10 ,并且 a 的所有元素里面的 i 都为 10

2.let

let 与 var 的写法一致,不同的是它使用的是块作用域

let a = 1

块作用域变量在包含它们的块或 for 循环之外是不能访问的

{
    let x = 1
}
console.log(x) // Uncaught ReferenceError: x is not defined

所以:

var a = [];
for (let i = 0; i < 10; i++) { // let有自己的块作用域,每一次循环的 i 其实都是一个新的变量
  a[i] = function () {
    console.log(i);
  };
} // JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算
a[6](); // 6

同时, let 解决了 var 的两个缺陷:

使用 let 在全局作用域下声明的变量也不是顶层对象的属性*

let b = 2
window.b // undefined

不允许同一块中重复声明

let x = 1
let x = 2
// Uncaught SyntaxError: Identifier 'x' has already been declared

如果在不同块中是可以声明的

{
    let x = 1
    {
        let x = 2
    }
}

这种在一个嵌套作用域中声明同一个变量名称的行为称做 屏蔽 ,它可以完美解决上面的 sumArr 问题:

function sumArr(arrList) {
    let sum = 0;
    for (let i = 0; i < arrList.length; i++) {
        var arr = arrList[i];
        for (let i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
    return sum;
}

此时将得到正确的结果,因为内层循环的 i 可以屏蔽掉外层循环的 i 

通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下

3.var 与 let 的区别

(1)作用域

用 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 声明的变量的作用域有更小的限定范围,更具灵活。

(2)重复声明

var 允许在同一作用域中重复声明,而 let 不允许在同一作用域中重复声明,否则将抛出异常。

var 相关示例代码:

var a = 1;
var a = 2;

console.log(a) // 2

function test() {
  var a = 3;
  var a = 4;
  console.log(a) // 4
}

test()

let 相关示例代码:

if(false) {
  let a = 1;
  let a = 2; // SyntaxError: Identifier 'a' has already been declared
}
switch(index) {
  case 0:
    let a = 1;
  break;

  default:
    let a = 2; // SyntaxError: Identifier 'a' has already been declared
    break;
}

从上述示例中可以看出,let 声明的重复性检查是发生在词法分析阶段,也就是在代码正式开始执行之前就会进行检查。

(4)变量提升与暂存死区

var 声明变量存在变量提升,如何理解变量提升呢?

要解释清楚这个,就要涉及到执行上下文和变量对象。

在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会创建并进入一个新的执行环境,而这个执行环境被称之为执行上下文。因此执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。

执行上下文可以理解为一个抽象的对象。

Variable object:变量对象,用于存储被定义在执行上下文中的变量 (variables) 和函数声明 (function declarations) 。

Scope chain:作用域链,是一个对象列表 (list of objects) ,用以检索上下文代码中出现的标识符 (identifiers)

thisValue:this 指针,是一个与执行上下文相关的特殊对象,也被称之为上下文对象。 一个执行上下文的生命周期可以分为三个阶段:创建、执行、释放。

而所有使用 var 声明的变量都会在执行上下文的创建阶段时作为变量对象的属性被创建并初始化,这样才能保证在执行阶段能通过标识符在变量对象里找到对应变量进行赋值操作等。

而用 var 声明的变量构建变量对象时进行的操作如下:

  • 由名称和对应值(undefined)组成一个变量对象的属性被创建(创建并初始化)
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。 上述过程就是我们所谓的“变量提升”,这也就能解释为什么变量可以在声明之前使用,因为使用是在执行阶段,而在此之前的创建阶段就已经将声明的变量添加到了变量对象中,所以执行阶段通过标识符可以在变量对象中查找到,也就不会报错。

示例代码:

console.log(a) // undefined

var a = 1;

console.log(a) // 1

let 声明变量存在暂存死区,如何理解暂存死区呢?

其实 let 也存在与 var 类似的“变量提升”过程,但与 var 不同的是其在执行上下文的创建阶段,只会创建变量而不会被初始化(undefined),并且 ES6 规定了其初始化过程是在执行上下文的执行阶段(即直到它们的定义被执行时才初始化),使用未被初始化的变量将会报错。

在变量初始化前访问该变量会导致 ReferenceError,因此从进入作用域创建变量,到变量开始可被访问的一段时间(过程),就称为暂存死区(Temporal Dead Zone)。

示例代码 1:

console.log(bar); // undefined
console.log(foo); // ReferenceError: foo is not defined

var bar = 1;
let foo = 2;

示例代码 2:

var foo = 33;
{
  let foo = (foo + 55); // ReferenceError: foo is not defined
}

注:首先,需要分清变量的创建、初始化、赋值是三个不同的过程。另外,从 ES5 开始用词法环境(Lexical Environment)替代了 ES3 中的变量对象(Variable object)来管理静态作用域,但作用是相同的。为了方便理解,上述讲解中仍保留使用变量对象来进行描述。

小结

  • var 声明的变量在执行上下文创建阶段就会被「创建」和「初始化」,因此对于执行阶段来说,可以在声明之前使用。 对于var而言,当进入var变量的作用域时,会立即为它创建存储空间,并对它进行初始化,赋值为undefined,当函数加载到变量声明语句时,会根据语句对变量赋值。
  • 而let和const却不一样,当进入let变量的作用域时,会立即给它创建存储空间,但是不会对它进行初始化。 let 声明的变量在执行上下文创建阶段只会被「创建」而不会被「初始化」,因此对于执行阶段来说,如果在其定义执行前使用,相当于使用了未被初始化的变量,会报错。

4.let 与 const 异同

const 与 let 很类似,都具有上面提到的 let 的特性,唯一区别就在于 const 声明的是一个只读变量,声明之后不允许改变其值。因此,const 一旦声明必须初始化,否则会报错。

示例代码:

let a;
const b = "constant"

a = "variable"
b = 'change' // TypeError: Assignment to constant variable

如何理解声明之后不允许改变其值?

其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动(即栈内存在的值和地址)。

JavaScript 的数据类型分为两类:原始值类型和对象(Object类型)。

对于原始值类型(undefined、null、true/false、number、string),值就保存在变量指向的那个内存地址(在栈中),因此 const 声明的原始值类型变量等同于常量。

对于对象类型(object,array,function等),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是不可修改的,至于指针指向的数据结构是无法保证其不能被修改的(在堆中)。

示例代码:

const obj = {
  value: 1
}

obj.value = 2

console.log(obj) // { value: 2 }

obj = {} // TypeError: Assignment to constant variable
const user = {
    name: "sisterAn",
    age: num,
}
user = {
    name: "pingzi",
    age: num
} // Uncaught TypeError: Assignment to constant variable.

// 下面这些都是运行成功的
user.name = "Hello"
user.name = "Kitty"
user.name = "Cat"
user.age--

其它 const 与 let 相同,例如:

· 作用域相同,只在声明所在的块级作用域内有效

· 常量也是不提升,同样存在暂时性死区