JS全书:JavaScript Web前端开发指南
上QQ阅读APP看书,第一时间看更新

3.2 变量和变量作用域

3.2.1 变量

变量用来存储值或表达式,是存储数据的容器,示例如下。

      a = 1;
      a = a + 1; // -> 2

变量名称的命名规则与标识符类似。

3.2.2 声明变量

在JavaScript中,有多种声明变量的方式。

  • var
  • let
  • const
1. var

var声明一个变量,可以在声明变量的时候为其赋值,示例如下。

      var a = 1;

上面的代码很好理解,定义了一个名称为a,值为1的变量,我们也可以先声明变量,在必要的时候再对其进行赋值操作,示例如下。

      var a; //声明变量a
      a = 1; //把数字1赋值给变量a

声明多个变量的方式如下。

      var a;
      var b = 1;

声明多个变量时,也可以只使用一次var关键字,简写成:

      var a , b = 1;
2. 变量的作用域

ECMAScript使用的是词法作用域(Lexical scoping,又称“静态作用域”),其变量又称为“词法变量”,词法变量在变量声明时确定其有效范围,这一有效范围就是变量的作用域(scope),在作用域外,该变量不可见。

通常,作用域是一个函数,示例如下。

ES6增加了块级作用域:

在函数作用域和块级作用域内声明的变量叫作“局部变量”,除此之外的变量叫作“全局变量”。

局部变量只能在该函数或块级作用域内被访问;全局变量可以在当前文档内的任何位置访问。

全局变量其实是global对象(浏览器环境下,global对象指的是window对象)的属性,可以通过window['变量名]访问。

· 作用域链

当代码在一个执行环境中执行时,会创建变量对象(又称“活动对象”,activation object,包含形参、函数声明、变量声明)的一个作用域链,作用域链的最前端,始终都是当前执行的代码所在执行环境的变量对象(如果执行环境是函数,变量对象以arguments初始化),作用域链的下一个对象来自包含(外部)的执行环境,再下一个则来自下一个包含执行环境,这样一直追溯到全局执行环境global。

· 执行环境

执行环境又称“执行上下文”,JavaScript在执行代码时,会创建一个执行环境,该执行环境会成为当前的执行环境,每个执行环境包含3部分:

  • 词法环境,即作用域链。
  • 变量环境,即声明的变量。
  • this绑定。

也就是说,在执行代码时,其执行环境就已经对词法环境、变量环境和this进行了初始化操作。

· 声明提前(var hoisting)

我们先看以下一段代码。

      console.log(a); // > undefined
      console.log(b); // > Uncaught ReferenceError: b is not defined
      var a = 1;
      console.log(a); // > 1

查看变量的生命周期,变量的生命周期可以理解为3部分。

① 声明阶段,为变量创建存储空间。

② 初始化阶段,变量值被初始化为undefined。

③ 赋值阶段,执行赋值操作。

声明提前是指在进入变量的作用域时,立即完成变量的声明阶段和初始化阶段。

因此,上述代码可以看成:

其中,声明阶段就位于执行环境创建时,因此,在尚未执行代码前,声明的变量其值均为undefined。

3. let与const

ES6新增了两个定义变量的关键字——let与const,用来取代var。

· let

let的用法与var类似,但let声明的变量具有块级作用域,即let声明的变量只在当前代码块内有效,示例如下。

上述代码中,分别使用var和let在一个代码块内声明一个变量,之后在代码块外访问这两个变量,使用var声明的变量可以正常访问,使用let声明的变量则报错,这说明let声明的变量只在当前代码块内有效。

· 暂时性死区(temporal dead zone)

let和const声明的变量拥有暂时性死区(TDZ),即在进入它的作用域后,变量无法被访问,直到声明结束,示例如下。

上述代码中,分别使用var和let以及非关键字的方式声明一个变量,并在变量声明前,尝试访问这个变量,使用var声明的变量返回undefined,使用let和非关键字的方式声明的变量抛出ReferenceError,这表明使用let声明的变量不存在声明提前。

之后,对a、b、c三个变量进行赋值操作,变量a和c顺利完成赋值,并返回相应值,但对使用let声明的变量b进行赋值时,抛出了异常,对照变量c,这表明变量b此时存在但不能进行赋值操作,也就是说变量b此时完成了声明阶段,但不能被正常访问。

下面使用let声明变量b,此后,再次访问变量b时,尽管没有对变量b进行赋值操作,但依然可以获取变量b的值,这表明在声明结束之前,变量b已经完成了声明阶段、初始化阶段和赋值阶段。

综上,使用let声明的变量在进入其作用域后,立即完成变量的声明阶段和初始化阶段(如果有赋值操作,也会完成赋值阶段),但在变量声明结束前,无法对变量进行操作。

使用let声明的变量在进入其作用域后,直到声明结束前的这块语法区间称为“暂时性死区”(TDZ)。

声明结束前不能访问,意味着死区并不是基于空间的,而是基于时间的,由以下示例可以看出。

· const

const的用法和特性与let基本相同,不同之处在于,const声明的是一个只读常量,被const声明的变量不能被重新声明或赋值。换句话说,它将不能再被改变(对于引用类型的数据,其地址指向不可修改,属性可修改),示例如下。

对于引用类型的数据,即便是将其地址指向修改为自身的地址也会抛出异常,示例如下。

      const arr  = [];
      const arr2 = arr;
      arr = arr2; //抛出异常> Uncaught TypeError: Assignment to constant variable

这个示例之后,声明了一个变量arr,并将arr中存储的堆中的地址赋值给另一个变量arr2,此时arr2和arr中存放的地址指向堆中的同一个对象,之后,将arr的地址修改为arr2的地址,即将arr的地址修改为自身的地址,此时,控制台抛出异常,这表示被const声明的数组或对象的地址指向不可修改。

4. 重复声明

在相同作用域内,let和const声明的变量不允许有重复声明,示例如下。

3.2.3 非声明变量

非声明变量是指不使用关键字声明的变量,示例如下。

      b = 1; //严格模式下会抛出ReferenceError: b is not defined
      console.log(window.b); // > 1

非声明变量会被挂载到global对象(浏览器环境下,global对象指的是window对象)的属性上,因此可以通过window访问。

· var声明变量和非声明变量的区别

示例如下。

删除情况,示例如下。

      console.log(typeof a); // > string
      console.log(typeof b); // > undefined

delete操作符可以删除一个对象的属性,但如果属性是一个不可配置(non-configurable)属性,删除时则会返回false(严格模式下删除一个不可配置的变量会抛出异常)。

这就表示使用var声明的变量是不可配置的,我们可以使用getOwnPropertyDescriptor方法来获取描述属性特性的对象,以此验证这一点,示例如下。

      Object.getOwnPropertyDescriptor(window, "a");
      // -> {value: "a", writable: true, enumerable: true, configurable: false}

Object.getOwnPropertyDescriptor(window, "b"); // -> {value: "b", writable: true, enumerable: true, configurable: true}

两者的根本区别在于关键字var声明的变量是不可配置的,不能通过delete操作符删除。

需要注意的是,configurable值一旦为false,描述属性特性的对象就不能被修改,因此不能通过修改属性描述符使var声明的变量能被delete删除,但反过来,可以使非声明变量也不能被delete删除,示例如下。

现在,你已经学会了多种声明变量的方式,但不同的声明方式应该在什么情况下使用呢?

建议是,尽量使用const,我们已经知道,let和const声明的变量是不能被重复声明的,但let所声明的变量可以被重新赋值,const可以避免意外情况(例如,没有使用关键字声明变量)下对变量进行赋值操作,导致程序出现错误,而如果想要改变变量,就使用let声明变量。

在ES6中,避免使用var。

练习

  • 使用不同的方式声明一个变量。