上一篇我们讲到在全局环境下的代码段中,执行上下文环境中有如何数据:
• 变量、函数表达式——变量声明,默认赋值为undefined;
• this——赋值;
• 函数声明——赋值;
如果在函数中,除了以上数据之外,还会有其他数据。先看以下代码:
以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。至于“自由变量”和“作用域”是后面要专门拿出来讲述的重点,这里就先点到为止。用一个例子说明一下:
好了,总结完了函数的附加内容,我们就此要全面总结一下上下文环境的数据内容。
全局代码的上下文环境数据内容为:
普通变量(包括函数表达式), 如: var a = 10; | 声明(默认赋值为undefined) |
函数声明, 如: function fn() { } | 赋值 |
this | 赋值 |
如果代码段是函数体,那么在此基础上需要附加:
参数 | 赋值 | |
arguments | 赋值 | |
自由变量的取值作用域 | 赋值 |
给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
了解了执行上下文环境中的数据信息,你就不用再去死记硬背那些可恶的面试题了。理解了就不用背诵!
讲完了上下文环境,又来了新的问题——在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?下一节将通过“执行上下文栈”来解释这个问题。
不过别着急,在解释“执行上下文栈”之前,还需要把this说一下,this还是挺重要的。
说完this,接着说执行上下文栈。
注:
变量对象
我们知道变量和执行环境有着密切的关系:
var a = 10; // 全局上下文中的变量
(function () {
var b = 20; // function上下文中的局部变量
})();
alert(a); // 10
alert(b); // 全局变量 "b" 没有声明
而且我们也知道在JS里没有块级作用域这一说法,ES规范指出独立作用域只能通过函数(function)代码类型的执行环境创建。也就是说,像for循环并不能创建一个局部环境:
for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); // 尽管循环已经结束但变量k依然在当前作用域
既然变量与执行环境相关,那变量自己应该知道它的数据存放在哪里,并知道如何访问。这就引出了“变量对象”这个概念。
每个执行环境都有一个与之关联的变量对象,这个对象存储着在环境中定义的以下内容:
1. 函数的形参
2. var声明的变量
3. 函数声明(不包括函数表达式)
举例来说,用一个普通对象来表示变量对象,它是执行环境的一个属性:
执行环境 = {
变量对象:{
//环境中的数据
}
};
例如:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
对应的变量对象为:
// 全局执行环境的变量对象
全局环境的变量对象= {
a: 10,
test: 指向test()函数
};
// test函数执行环境的变量对象
test函数环境的变量对象 = {
x: 30,
b: 20
};
那么,不同执行环境中的变量对象的初始化是怎样的呢?下面详细看一下:
❶全局环境中的变量对象
先看下全局对象的明确定义:
全局对象 是在进入任何执行环境之前就已经创建了的对象。
这个对象只存在一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
全局对象初始创建阶段,将Math、String等作为自身属性,初始化如下:
globla = {
Math:
String:
...
...
window:globla //引用自身
};
在这里,变量对象就是全局对象自己。
❷函数环境中的变量对象
在函数执行环境中,“活动对象” 扮演着变量对象这个角色。活动对象是在进入函数执行环境时创建的,它通过函数的arguments属性初始化:
活动对象 = {
arguments: //是个对象,包括callee、length等属性
};
理解了变量对象的初始化之后,下面就是关于变量对象的核心了。
环境中的代码,被分为两个阶段来处理:进入执行环境 、执行代码。变量对象的修改变化与这两个阶段紧密相关。
这2个阶段的处理是一般行为,和环境的类型无关(即,在全局环境和函数环境中的表现是一样的)。
①进入环境
当进入执行环境时(代码执行之前),变量对象已包含下列属性(上面有提到):
①函数的所有形参(如果是在函数执行环境中。因为全局环境没有形参。)
————由 形参名称 和 对应值 组成,作为变量对象的属性。如果没有传递对应的参数,将undefined作为对应值。
②所有函数声明(注意是声明,函数表达式不算。)
————由 函数名 和 对应值(函数对象)组成,作为变量对象的属性。如果变量对象已经存在同名的属性,则覆盖这个属性。
③所有变量声明(由var声明的变量)
————由 变量名 和 对应值(undefined) 组成,作为变量对象的属性。如果变量名与已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。
————注意:此时的对应值是undefined。
让我们来看一个例子:
function test(a, b) {
alert(c); //undefined
alert(d); //function d() {}
alert(e); //undefined
alert(x); //出错
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); //
我们考虑当进入带有参数10的test函数环境时(代码执行之前),活动对象表现如下:
活动对象(test) = {
a: 10,
b: undefined,
c: undefined,
d:指向函数d,
e: undefined
};
注意,活动对象里不包含函数x。这是因为x是一个函数表达式而不是函数声明,函数表达式不会影响变量对象(在这里是活动对象)。函数_e同样是函数表达式,但是我们注意到它分配给了变量e,所以可以通过名称e来访问。
在这之后,将进入处理代码的第二个阶段:执行代码。
②执行代码
这个阶段内,变量/活动对象已经拥有了属性(不过,并不是所有属性都有值,就像上面那个例子,大部分属性的值还是系统默认的undefined)。
继续上面那个例子,活动对象在“执行代码”这个阶段被修改如下:
活动对象(test) = {
a: 10,
b: undefined, //没有相应该参数传入,undefined
c: 10, //之前是undefined
d: 指向函数d,
e: 指向函数表达式_e //之前是undefined
};
注意此时,函数表达式_e保存到了已声明的变量e上,但函数表达式”x”本身不存在于活动对象中,也就是说,如果尝试调用函数”x”,无论在函数定义之前或之后,都会出现 “x is not defined”的错误。
理解了以上内容之后,再来看一个例子:
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
为什么第一个alert(x)的值是function,而且它还是在x声明之前访问的x?为什么不是10或20呢?
现在我们知道,函数声明是在进入环境时填入活动对象的,同一时间,还有一个变量声明’x’,但是正如前面所说,变量声明在顺序上跟在函数声明和形参声明之后。即,在进入环境阶段,变量声明不会干扰变量对象中已经存在的同名函数或形参声明。所以,就这个例子来说,在进入环境时,变量对象的结构如下:
变量对象 = {
x:指向函数x
//如果function x没有已经声明的话,这时的x应该是undefined
};
紧接着,在代码执行阶段,变量对象作如下修改:
变量对象['x'] = 10;
变量对象['x'] = 20;
//可以在第二、三个alert看到这个结果
再看一个例子:
if (true) {
var a = 1;
} else {
var b = 2;
}
//变量是在进入环境阶段放入变量对象的,虽然else部分永远不会执行,
//但是不管怎样,变量b仍然存在于变量对象中。
alert(a); //1
alert(b); //undefined,不是b未声明,而是b的值是undefined
另外,关于var声明变量和不用var声明:
大叔的译文中指出:任何时候,变量只能通过var关键字才能声明。
像a = 10;这仅仅是给全局对象创建了一个新属性(但它不是变量)。它之所以能成为全局对象的属性,完全是因为全局对象===全局变量对象。看例子:
alert(a); // undefined
alert(b); // "b" 没有声明,出错
b = 10;
var a = 20;
进入环境阶段:
变量对象 = {
a: undefined
};
可以看到,因为b不是一个变量,所以在这个阶段根本就没有b,b将只在代码执行阶段才会出现,但在这里,还未执行到那就出错了。
还有一个要注意的:var声明的变量,相对于属性(如a = 10;或window.a = 10;),变量的[[Configurable]]特性值为false,即不能通过delete删除,而属性则可以。