在我们前面理解了作用域之后,“作用域链”这个概念就产生了。那么作用域链是什么意思,它又是怎么形成的,跟哪些概念有关系,这就是我接下来几章想和大家探讨的内容:执行上下文、变量对象和作用域链。根据顺序我们也可以看出来,想要理解作用域链,执行上下文是我们碰到的第一个坎。
这一章我们就来讨论一下到底什么是执行上下文。
1. 定义
当 JS 引擎开始执行预编译生成的代码时,就会进入到一个执行上下文(Executable Code - 简称 EC)。
在 ECMA 标准规范里并没有从技术角度去定义 EC 的具体类型和结构,这个是在实现 ECMAScript 引擎时需要考虑的问题。
但是在逻辑上,我们可以将活动的执行上下文看成一个栈结构。栈底部永远是全局上下文(global context),而顶部就是当前活动的执行上下文。执行到当前代码时,上下文入栈,执行完毕后,上下文出栈。
2. 可执行代码有几种
前面说到当引擎执行到可执行代码的时候,就会将当前上下文压入上下文栈中。那么可执行的代码又分为几种?
在这里,我们先假设定义执行上下文栈是一个数组:
代码语言:javascript复制EC = [];
第一种可执行代码 -- 全局代码: 全局类型代码是在加载外部的 js 文件或者本地 标签中的代码。 注意,在全局代码中,并不包含定义在全局环境 function 内的代码。
程序启动后进入初始化全局环境:
代码语言:javascript复制EC = [
globalContext
];
第二种可执行代码 -- 函数代码: 当定义的函数被执行时,就进入了函数代码,当前函数上下文被压入 EC 栈中。 注意,在函数代码中,也不包含定义在该函数内部环境 function 内的代码。
例如:
代码语言:javascript复制var a = 10;
function foo () {
var b = 20;
foo();
}
foo();
这个例子中的 EC 是什么样子的呢?
代码语言:javascript复制// 初始化
EC = [
globalContext
];
// 第一次调用 foo 函数
EC = [
<foo> functionContext,
globalContext
];
// 在 foo 内递归调用自己
EC = [
<foo> functionContext - recursively,
<foo> functionContext,
globalContext
];
// 继续递归调用自己
EC = [
......
<foo> functionContext - recursively2,
<foo> functionContext - recursively,
<foo> functionContext,
globalContext
];
// 递归会不断调用下去,因为没有结束条件,所以这是一个死循环
// 所以,EC 只会不断增加新的上下文,但是却不会退出
只有每次 return 的时候,才会退出当前执行上下文,相应上下文会从栈中弹出,栈指针会自动移动位置。
注意,当函数没有明确指明 return 什么的时候,默认 return undefined 。
如果有抛出的异常没有被截获的话,也有可能从一个或多个执行上下文中退出。当所有代码执行完以后,EC 中只会包含全局上下文(global context),当程序退出以后,全局上下文也会退出。
第三种可执行代码 -- eval 代码: eval 函数在调用的时候会产生上下文。 例如:
代码语言:javascript复制eval('var a = 10');
(function foo () {
eval('var b = 20');
}());
alert(a); // 10
alert(b); // ReferenceError,b is not defined
这个例子中 EC 的变化如下:
代码语言:javascript复制// 初始化
EC = [
globalContext
];
// eval('var a = 10');
EC = [
evalContext,
globalContext
];
// eval 执行完毕
EC = [
globalContext
];
// 立即执行函数 foo
EC = [
<foo> functionContext,
globalContext
];
// eval('var b = 20');
EC = [
evalContext,
<foo> functionContext,
globalContext
];
// eval 执行完毕
EC = [
<foo> functionContext,
globalContext
];
// foo 执行完毕
EC = [
globalContext
];
这就是一个典型的逻辑调用上下文栈。
在 setTimeout 和 setInterval 函数中的第一个参数也可以传入代码字符串,但是这个一般不会这么去用,所以这里也就不讨论了。
3. 结论
执行上下文环境是我们了解变量对象和作用域链的基础,大家一定要好好理解(其实也并不难),下一节我们来讨论变量对象,相信会让大家有一定的收获。