变量对象的创建,依次经历了以下几个过程。
- 在chrome浏览器中,变量对象会首先获得函数的参数变量与其值,在firefox中,是直接将参数对象arguments保存在变量对象中。
- 依次获取当前上下文中所有的函数声明,也就是使用function关键字声明的函数。在变量对象中会以函数名建立一个属性,属性值为指向该函数所在的内存地址引用。如果函数名的属性已经存在,那么该属性的值会被新的引用覆盖。
- 依次获取当前上下文中的变量声明。也就是使用var关键字声明的变量。每找到一个变量声明,在变量对象中就会以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
ES6支持新的变量声明方式let/const,他们的规则与var完全不同,它们是在上下文的执行阶段开始执行,它们的出现避免了变量提升带来的一系列问题,因此这里暂时先不详细介绍他们,会在之后的ES6相关章节为大家详细解读。
知道了上面的规则,我们来思考一个问题,当我们执行以下的代码时,具体的执行过程是什么?
var a = 30;
首先上下文的创建阶段会先确认变量对象,而变量对象的创建过程则是先获取变量名并赋值为undefined。因此第一步则是:
var a = undefined;
上下文的创建阶段完毕,开始进入执行阶段,在执行阶段中需要完成变量赋值的工作,因此第二步则是:
a = 30;
我们需要注意的是,这两步分别是在上下文的创建阶段与执行阶段完成。因此 `var a = undefined` 这一步其实是提前到了比较早的地方去执行,我们可以通过一个简单的例子来证明。
console.log(a); // a 这个时候输出结果为undefined
var a = 30;
结合之前的理解,这个例子的实际执行顺序为:
// 创建阶段
var a = undefined;
// 执行阶段
console.log(a);
a = 30;
这种现象,我们称之为变量提升(Hoisting)。
从上面的规则中我们还可以看出,在变量对象的创建过程中,函数声明的执行优先级会比变量声明的优先级更高一点,而且同名的函数会覆盖函数与变量,但是同名的变量并不会覆盖函数。
但是在上下文的执行阶段,同名的函数会被变量重新赋值。
var a = 20;
function fn() { console.log('fn') };
function fn() { console.log('cover fn.') };
function a() { console.log('cover a.') };
console.log(a);
fn();
var fn = 'I want cover function named fn.';
console.log(fn);
// 20
// cover fn.
// I want cover function named fn.
上面例子的执行顺序其实为:
// 创建阶段
function fn() { console.log('fn') };
function fn() { console.log('cover fn.') };
function a() { console.log('cover a.') };
var a = undefined;
var fn = undefined;
// 执行阶段
a = 20;
console.log(a);
fn();
fn = 'I want cover function named fn.';
console.log(fn);
根据输出结果可以证明,在创建阶段,函数fn覆盖了函数fn,但是变量fn并没有覆盖函数fn。而在执行阶段,a与fn的重新赋值导致了他们发生了变化。