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。
练习
- 使用不同的方式声明一个变量。