在ECMAScript中,一个变量(或常量)的生命周期(Life Cycle)模式是固定的,由两种因素决定,分别是作用域和对其的引用。
我们可以先用ES2015 标准之前的代码来讲解一遍变量的生命周期,以便理解。
在这段代码中,可以看到在一个由匿名函数产生的作用域内定义了一个变量foo,但是我们不对它做任何处理,在脱离这个作用域后该变量便不再存在。
绝大部分ECMAScript运行引擎对垃圾数据的收集方式都是基于对变量(或常量)的引用进行统计,当一个变量的引用被全部解除时,引擎便会将其认定为应该被清除的。简单表示便是: 一个事物当为人所需要时,便永生不朽;但若被抛弃时,便悄然离去 。
如果想要把这个foo变量的生命周期延长,最为常用的办法便是 闭包(Closure) 。因为变量的生命周期由对其的引用所决定,而闭包的原理便是利用高阶函数来产生能够穿透作用域的引用。
这里成功地利用闭包将被定义在outter函数所形成作用域内的常量innerVariable所在的作用域引出来,从而在外部的作用域中能读取到它的值。即便没有将其值读取,只要对其保留引用的函数依然存在,这个常量就不会被清除。
回归正题,在ECMAScript中,变量(或常量)的生命周期是从程序进入定义语句所在的作用域开始的,即便是在定义语句之前。以下面这句代码为例。
我们不妨将这一句拆分为两句,以便更好理解后面的内容。
1.ECMAScript引擎在进入一个作用域时,会先扫描这个作用域内的变量(或常量)定义语句(var,let或const),然后在这个作用域内为扫描得到的变量名做准备,在当前作用域中被扫描到的变量名都会进入 未声明阶段(Undeclared)阶段 。
2.进入声明语句时,var foo,即前半句是 声明部分(Declaration) ,用于在ECMAScript引擎中产生一个变量名,但此时该变量名没有对应的绑定和内存空间,所以“值”为null。
3.= 的作用是作为变量的赋值语句,引擎执行至此处即为该变量的 赋值部分(Assignment) ,计算将要赋予变量名的值的物理长度(内存空间占用大小),向系统申请相应大小的内存空间,然后将数据存储到里面去,并在变量名和内存空间之间建立 绑定(Binding) 关系,此时变量(或常量)才得到了相应的值。
4.到当前作用域中的语句被执行完毕时,引擎便会检查该作用域中被定义的变量(或常量)的被引用情况,如果引用已被全部解除,引擎便会认为其应该被清除。
5.运行引擎会不断检查存在于运行时(Runtime)中的变量(或变量)的被引用情况,并重复第四步,直至程序结束。
需要注意的是,在ES2015 标准之前,我们可能会遇到下面这种情况。
就如上面所说的,一旦引擎进入一个作用域时,会先扫描该作用域内的定义语句。在这段代码中,我们可以看到在外层作用域中定义了变量foo,而在内层的作用域中,我们在对变量foo读取代码后又对其再次“定义”。此时我们便可以发现在第二次定义语句之前对foo的引用所获得的值竟然是undefined,因为此时变量foo处于 未声明阶段 ,即使它在上一层作用域已被定义。
但实际上,这并不符合正常的编程逻辑,所以在ES2015 的let和const 中,引擎将这种行为视为错误行为,并抛出错误。
ES2015 标准中(一般情况下为严格模式)不允许变量(或常量)在被定义之前被其他语句所读取,以免出现逻辑性错误。