Javascript深入浅出之执行上下文

当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。

如果你刚看《高程》,看到这句话估计会似是而非的理解。其实我当时也是,直到最近撸第二遍,也是偶然看到大叔的系列文章,有心研究了下,加上自己的理解,促成了这几篇博文。今天我们先来聊聊执行上下文与活动对象。

执行上下文

执行上下文可能不太容易理解,其实它就是上面写到的执行环境,代码就是在相对应的执行环境中运行的。所以我们可以这样理解,每当调用一个函数,就会相应地为这个函数划出一块地,该函数所有的代码活动都被限制在这块地上。

好,这里我们可以引出两个问题:这块地是什么?只有函数调用才能创建EC吗?

这块地是什么?

当然就是内存了。无论是js还是其他代码,代码的运行所需空间都是由内存来分配的。
内存分堆(heap)和栈(stack),在这里,执行上下文组成了 执行上下文栈 来调控代码的进行,执行上下文栈的顶端就是正在使用的上下文。我们来用图示看一下:

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1,
b = 2;
function foo() {
a = 3;
bar();
alert(a + b);
}
function bar() {
b = 3;
}
foo();

ECStack Losted

当程序结束时,全局上下文出栈并销毁。

相信大家也注意到了上面出现了很多次的 全局上下文 ,它可以很好的回答我们的第二个问题:不是! 那么,问题就来了。。。

什么代码可以创建EC?

全局代码

函数代码

Eval代码

全局代码

全局代码就是 <script> 标签中的,或者是通过外部加载的js文件中的代码,它不包含任何函数体内的代码。
全局代码创建的EC就是我们上文中所提的全局上下文,它是程序刚开始,代码尚未执行时就产生的上下文,它永远处在执行上下文栈的底部。

函数代码

function 函数中的代码段就是函数代码,它也不包括内部函数代码。

函数如果递归调用自身,每调用一次都会创建一个新的执行上下文。如果函数体内有 return ,执行到它的时候会立刻退出当前执行上下文。

Eval代码

Eval代码和函数代码类似,执行到它的时候也会创建一个新的执行上下文。但不同的是,它有一个调用上下文(calling context),同样会压入ECStack。eval函数中如果进行变量、函数声明,将会体现到调用上下文中。

不过eval会使阅读性、安全性、性能下降,不建议大家使用。

变量对象

代码执行的时候访问的变量都是从变量对象中取到的。

变量对象(Variable Object)其实是执行上下文的一个属性,执行上下文还有 this作用域 属性。它们介绍起来篇幅很大,所以我把它放到了后面的文章。

变量对象分为两类:
1.全局上下文变量对象
2.函数上下文变量对象

全局上下文中的变量对象

全局上下文变量对象就是全局对象。

全局对象本身包含很多属性如Math、String、Date、parseInt等等,大家不妨用 console.log(this) 在控制台中查看一下,还有,在全局作用域中声明的变量和函数也会成为它的属性。

当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window。

函数上下文中的变量对象

函数上下文中存在的是活动对象,它可以当做变量对象使用,其中包含变量、函数声明、形参、arguments。

我们来配合一个例子看一下:

1
2
3
4
5
6
7
8
var a = 10;
function foo(b) {
var c = 20;
c += b;
};
foo(30)

对应的变量对象是:

1
2
3
4
5
6
7
8
9
10
11
12
// 全局上下文的变量对象(不包含自带属性)
VO(globalContext) = {
a: 10,
foo: <reference to function>
}
VO(foo functionContext) = {
// arguments其实是一个类数组对象,可以通过下标来访问实参,它同时包含callee、length、properties-indexes三个属性
arguments: <ArgO>
b: 30, // 形参
c: 20 // 变量
}

我们想一个问题,变量对象是初始化时就是这样吗?其实不是这样,执行上下文中的代码分两个阶段,不同阶段值不同,这也是js的很重要的一个特性。

执行上下文的两个阶段

我们透过这段代码在不同时期的变量对象看一下。

1
2
3
4
5
6
7
8
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call

进入执行上下文

这个阶段处于代码执行之前,此时执行上下文的VO已经包含函数声明、形参、变量、arguments。其实这个阶段就是声明提升的阶段

此时AO对象数据为:

类型 赋值与否
变量 undefined
函数声明 对函数的引用
形参 赋值
arguments 赋值

建议大家配合 Javascript深入浅出之声明与提升 中的 提升规则 一起看看。

对应变量对象:

1
2
3
4
5
6
7
8
// 活动对象
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};

代码执行

此时AO对象数据为:

类型 赋值与否
变量 赋值
函数声明 对函数的引用
形参 赋值
arguments 赋值

在这个阶段中,AO被改成了

1
2
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;

到了这里,相信大家都对函数调用阶段有个很多本质的理解,配合本文也可以更好地理解声明提升,也为后面理解闭包打好基础。

热评文章