Higanbana

深入理解闭包(二)——变量对象

执行一个函数之前,JavaScript引擎会进行准备工作,这个准备工作指的就是执行上下文,也叫执行上下文环境,也叫执行环境。
下面是一个执行上下文的周期图,其中变量对象(Variable Object)是重点之一,只有理解了它,我们才能知道一段代码的执行过程中先做什么,后做什么,我们今天就探讨一下变量对象在执行上下文生命周期中都经历了怎样的变化:

创建阶段

  1. 建立arguments对象(参数对象,当某个函数接收参数的时候,会将参数封装成arguments对象)。
  2. 检查当前上下文的函数声明,也就是使用function声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在的内存地址的引用。如果函数名的属性已存在,那么该属性将会被新的引用所覆盖。
  3. 检查当前上下文的变量声明,也就是使用var声明的变量,对于每一个变量声明,变量对象会以变量名建立一个属性,属性值为undefined。函数的声明比变量优先级要高,并且定义过程不会被变量覆盖,除非是赋值。

执行阶段

执行阶段没有什么难以理解的,有两点需要知道一下:

  1. 代码执行时按照顺序,该赋值时赋值,该调用时调用,这个一看后面的栗子就明白了。
  2. 变量对象转变为了活动对象(Active Object),其实这俩是一个东西,只是所处的时期不同罢了。

栗子们

栗子1:

1
console.log(a); //a is not defined

上面代码执行环境中根本没有定义变量a

1
2
console.log(a); //undefined
var a = 10;

虽然同样没有打印a的值,但是跟之前不同的是,这里执行环境中查找到了变量a,但是在打印时它还没有进行赋值,因为查找变量a是在执行上下文创建阶段完成的,并定义为undefined,然后才进入执行阶段,但是打印a的语句在前面,赋值语句在后面,所以打印出来的是a的初始值undefined,这也就体现了前面说的执行阶段是按照顺序执行相关赋值、打印、函数调用等代码。我们可以把上面代码等效成下面的形式:

1
2
3
var a;
console.log(a);
a = 10;

栗子2:

1
2
3
4
5
console.log(test); //function test(){ return 1;}
console.log(test()); //1 test是函数,test()是函数调用之后返回的值
function test() {
return 1;
}

上面代码在创建阶段检查到的是函数声明,因此可以顺利打印函数。

1
2
3
4
5
console.log(test); //undefined
console.log(test()); //test is not a function
var test = function () {
return 1;
}

这段代码也是要打印函数,但是却得到了截然不同的效果,因为这里的test其实是一个变量声明,只不过给它赋值的是一个函数,但是在创建阶段它的初始值仍是undefined。

栗子3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function test(x,y) {
console.log(arguments); //{ '0': 1, '1': 2 }
console.log(a); //undefined
console.log(b); //function func(){ return 2; }
console.log(b()); //2
console.log(c); //unundefined
var a = 10;
console.log(a); //10
function b() {
return 1;
}
function b() {
return 2;
}
var b;
var c=function () {
return 3;
}
console.log(c); //function func(){ return 2; }
console.log(c()); //3
}
test(1,2);

分析:

  • 检查到函数调用时传入了参数(1,2),以参数为属性值封装成arguments对象{ ‘0’: 1, ‘1’: 2 }。
  • 检查函数声明,先检查到第一个b函数,便以b为属性名,b函数的内存地址引用为属性值建立一个属性。但紧接着又检查到一个b函数,属性值将会被后面的b函数覆盖,因此打印b函数时得到的是后面声明的b函数。
  • 检查变量声明,得到变量a,b,c,由于函数b的存在,b属性已经被创建了,并且这里的变量b并没有赋值,因此它不会覆盖属性b的值,而是跳过。不过另外的变量a和c都顺利创建属性a和c,属性值为undefined。
  • 按顺序执行代码,第一次打印a时还没有经过赋值,因此输出undefined,第二次打印a时已经赋值,因此输出10,其他同理,不再解释。

如果按照执行顺序把栗子3代码做个等效的话,是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test(x,y) {
function b() {
return 2;
}
var a;
var c;
console.log(arguments);
console.log(a);
console.log(b);
console.log(b());
console.log(c);
a = 10;
console.log(a);
c=function () {
return 3;
}
console.log(c);
console.log(c());
}
test(1,2);

结语

现在我猜你对于一段代码的执行过程应该已经了解得差不多了,你不要觉得这跟闭包看起来没有关系就忽略这里的知识点,其实你对基础的东西理解透彻,对后面难点的理解是潜移默化的,不仅仅是闭包,很多难点在夯实的基础面前都是赤身裸体的,很容易就能看得一清二楚。今天关于变量对象就说到这里,下一次我们会进一步探查执行上下文的奥秘。