JavaScript 深入之 执行上下文栈
顺序执行?
如果要问到 JavaScript 代码执行顺序的话,想必写过 JavaScript 的开发者都会有个直观的印象,那就是顺序执行,毕竟:
var foo = function () { console.log('foo1'); } foo(); // foo1 var foo = function () { console.log('foo2'); } foo(); // foo2
然而去看这段代码:
function foo() { console.log('foo1'); } foo(); // foo2 function foo() { console.log('foo2'); } foo(); // foo2
打印的结果却是两个 foo2
。
刷过面试题的都知道这是因为 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如第一个例子中的变量提升,和第二个例子中的函数提升。
但是本文真正想让大家思考的是:这个“一段一段”中的“段”究竟是怎么划分的呢?到底 JavaScript 引擎遇到一段怎样的代码时才会做 准备工作 呢?
可执行代码
这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了?
其实很简单,就三种,全局代码、函数代码、eval 代码。举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。
执行上下文栈
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?
所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文,为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
ECStack = [];
试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
ECStack = [ globalContext ];
现在 JavaScript 遇到下面的这段代码了:
function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:
// 伪代码 // fun1() ECStack.push(<fun1> functionContext); // fun1中竟然调用了fun2,还要创建fun2的执行上下文 ECStack.push(<fun2> functionContext); // 擦,fun2还调用了fun3! ECStack.push(<fun3> functionContext); // fun3执行完毕 ECStack.pop(); // fun2执行完毕 ECStack.pop(); // fun1执行完毕 ECStack.pop(); // javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
解答思考题
好啦,现在我们已经了解了执行上下文栈是如何处理执行上下文的,最后的问题:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?答案就是执行上下文栈的变化不一样。让我们模拟第一段代码:
ECStack.push(<checkscope> functionContext); ECStack.push(<f> functionContext); ECStack.pop(); ECStack.pop();
让我们模拟第二段代码:
ECStack.push(<checkscope> functionContext); ECStack.pop(); ECStack.push(<f> functionContext); ECStack.pop();
是不是有些不同呢?当然了,这样概括的回答执行上下文栈的变化不同,是不是依然有一种意犹未尽的感觉呢,为了更详细讲解两个函数执行上的区别,我们需要探究一下执行上下文到底包含了哪些内容。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: JavaScript 深入之变量对象
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
@mqyqingfeng 多多交流,正在写underscore相关的文章。
@t2krew
var foo = function (){}
也是有变量提升的,只是这个变量的值是一个函数而已。举个例子:
就是因为有变量提升,才会打印
undefined
,否则就是报错啦@t2krew 我只是说了有准备工作,也没有说跟这个就有关系呐
@JarvenIV 感谢指出,我多篇文章的可执行上下文的英文都少了 "s",o( ̄▽ ̄)d
@JarvenIV 规范中的执行上下文的英文也是有 s 的,可以查看http://es5.github.io/,说起来,应该首字母大写来着……
你举得例子肯定是有函数提升的,因为函数提升的原因,同名的会被后者覆盖,实际上只会执行第二次声明的函数,执行上下文栈也只会创建第二次声明的函数的执行上下文,关于覆盖的规则,下一篇文章讲变量对象也会涉及到~
@qianlongo 谢谢解答
可执行代码那块,执行上下文那里
就叫做"执行上下文(execution contexts)"。
上下文contexts多了个字母s吧?
另外还有个问题请教
这个的执行上下文栈是怎样模拟的呢?它有函数提升呢?
@qianlongo 十分感谢回答~ 正在写的 JavaScript 专题系列也有很多会涉及 underscore 的实现方法,多多交流哈~~~
@zaofeng 函数执行结束之后,如果没有显示地返回值,默认是undefined,chrome中会把函数执行的结果打印出来(不过应该只是打印最外层的那个函数)
大神,这里有打印出来一个undefined,能解释一下吗 谢谢。已star
@kevinxft 哈哈,不奢求那么多,star一下就是对我的鼓励了~ (๑•̀ㅂ•́)و✧
感谢,讲的通俗易懂,就是少了个赞赏的地方,手动滑稽。
嗯,是的,感谢指正。o( ̄▽ ̄)d
文中,"当遇到一个函数代码的时候,就会创建一个执行上下文"
是不是应该是:“遇到函数执行的时候,就会创建一个执行上下文”