Blog icon indicating copy to clipboard operation
Blog copied to clipboard

JavaScript深入之执行上下文栈

Open mqyqingfeng opened this issue 7 years ago • 152 comments

顺序执行?

如果要问到 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

解答思考题

好啦,现在我们已经了解了执行上下文栈是如何处理执行上下文的,所以让我们看看上篇文章《JavaScript深入之词法作用域和动态作用域》最后的问题:

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();

是不是有些不同呢?

当然了,这样概括的回答执行上下文栈的变化不同,是不是依然有一种意犹未尽的感觉呢,为了更详细讲解两个函数执行上的区别,我们需要探究一下执行上下文到底包含了哪些内容,所以欢迎阅读下一篇《JavaScript深入之变量对象》。

下一篇文章

《JavaScript深入之变量对象》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

mqyqingfeng avatar Apr 23 '17 08:04 mqyqingfeng

文中,"当遇到一个函数代码的时候,就会创建一个执行上下文" 是不是应该是:“遇到函数执行的时候,就会创建一个执行上下文”

izhangzw avatar May 10 '17 08:05 izhangzw

嗯,是的,感谢指正。o( ̄▽ ̄)d

mqyqingfeng avatar May 10 '17 08:05 mqyqingfeng

感谢,讲的通俗易懂,就是少了个赞赏的地方,手动滑稽。

kevinxft avatar May 13 '17 04:05 kevinxft

@kevinxft 哈哈,不奢求那么多,star一下就是对我的鼓励了~ (๑•̀ㅂ•́)و✧

mqyqingfeng avatar May 13 '17 05:05 mqyqingfeng

image 大神,这里有打印出来一个undefined,能解释一下吗 谢谢。已star

zaofeng avatar May 28 '17 11:05 zaofeng

@zaofeng 函数执行结束之后,如果没有显示地返回值,默认是undefined,chrome中会把函数执行的结果打印出来(不过应该只是打印最外层的那个函数)

function fn3 () {
  return true
}

function fn2 () {
  fn3()
}

function fn1 () {
  fn2()
}

fn1() // undefined

function fn3 () {
  return true
}

function fn2 () {
  return fn3()
}

function fn1 () {
  return fn2()
}

fn1() // true




qianlongo avatar May 28 '17 15:05 qianlongo

@qianlongo 十分感谢回答~ 正在写的 JavaScript 专题系列也有很多会涉及 underscore 的实现方法,多多交流哈~~~

mqyqingfeng avatar May 29 '17 08:05 mqyqingfeng

可执行代码那块,执行上下文那里 就叫做"执行上下文(execution contexts)"。 上下文contexts多了个字母s吧?

另外还有个问题请教

function test(){
  console.log('test1');
};
test();

function test(){
  console.log('test2');
};
test();

这个的执行上下文栈是怎样模拟的呢?它有函数提升呢?

xumengzi avatar May 30 '17 02:05 xumengzi

@qianlongo 谢谢解答

zaofeng avatar May 30 '17 03:05 zaofeng

@JarvenIV 规范中的执行上下文的英文也是有 s 的,可以查看http://es5.github.io/#x10,说起来,应该首字母大写来着……

你举得例子肯定是有函数提升的,因为函数提升的原因,同名的会被后者覆盖,实际上只会执行第二次声明的函数,执行上下文栈也只会创建第二次声明的函数的执行上下文,关于覆盖的规则,下一篇文章讲变量对象也会涉及到~

mqyqingfeng avatar May 31 '17 02:05 mqyqingfeng

@JarvenIV 感谢指出,我多篇文章的可执行上下文的英文都少了 "s",o( ̄▽ ̄)d

mqyqingfeng avatar May 31 '17 04:05 mqyqingfeng

@mqyqingfeng 多多交流,正在写underscore相关的文章。

qianlongo avatar May 31 '17 11:05 qianlongo

@t2krew var foo = function (){}也是有变量提升的,只是这个变量的值是一个函数而已。 举个例子:

console.log(foo)
var foo = function(){}

就是因为有变量提升,才会打印 undefined,否则就是报错啦

mqyqingfeng avatar Jun 01 '17 02:06 mqyqingfeng

@t2krew 我只是说了有准备工作,也没有说跟这个就有关系呐😂

mqyqingfeng avatar Jun 01 '17 04:06 mqyqingfeng

@t2krew 哈哈~ 研究的时候,这种精神是十分有必要的~ o( ̄▽ ̄)d

mqyqingfeng avatar Jun 01 '17 05:06 mqyqingfeng

@mqyqingfeng 博主 作用域和执行上下文是两个概念,方便记忆,应该如何如何理解区分呢? 作用域基于函数,执行上下文基于对象?

xdwxls avatar Jun 07 '17 14:06 xdwxls

上面说了当执行一段代码的时候,会进行一个“准备工作”,比如第一个例子中的变量提升,和第二个例子中的函数提升。

同时又说了当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution contexts)"。

那么所说的是指,由于JS是一段一段执行,执行上下文就是我们所理解的“段”。

建议将第一句话更为“当执行一段代码时,会进行一个‘准备工作’,这个工作不仅包含了预编译阶段的‘变量提升、函数提升’等,还包含了执行阶段~”

suoz avatar Jun 08 '17 01:06 suoz

@suoz 这里写的让人误解了,第一个例子中的变量提升和第二个例子的函数提升,是全局执行上下文做的准备工作,当执行函数的时候,又会创建一个执行上下文,做的是这个函数内部的准备工作,“准备工作”不是一个专业词汇,并不严谨,只是一个概括描述,我更想用这个词表示“预编译阶段”,侧重表达正是执行上下文做了“准备工作”,但如果要说到专业名词,执行上下文的话,其实还是横跨了两个阶段的。

mqyqingfeng avatar Jun 08 '17 02:06 mqyqingfeng

@mqyqingfeng 嘻嘻 回复赶上光速 看到下一篇文章有讲到 理解了 谢谢你的解答~

suoz avatar Jun 08 '17 02:06 suoz

@JarvenIV 现在我觉得 execution context 这个单词确实应该用单数,感谢指出~

mqyqingfeng avatar Jun 08 '17 12:06 mqyqingfeng

如果又一篇关于变量和函数提升的文章就更好了! 比如

var a = 1;
var b = 2;
fun c (){}
fun d(){}
//提升之后

var a;
var b;

函数提升编译成啥样

snow1101 avatar Jun 14 '17 03:06 snow1101

@snow1101 下一篇文章《JavaScript深入之变量对象》就讲到了提升的规则~

mqyqingfeng avatar Jun 14 '17 05:06 mqyqingfeng

关于执行上下文,这篇文章讲到执行的一些具体细节

kkkisme avatar Jul 05 '17 06:07 kkkisme

关于变量提升,函数提升转化后会不会好理解些 第一个例子中的变量提升转化后

var foo;
foo = function () {
    console.log('foo1');
}
foo();  // foo1

foo = function () {
    console.log('foo2');
}
foo(); // foo2

第二个例子中的函数提升转化后

var foo;
foo = function () {
    console.log('foo1');
}
foo = function () {
    console.log('foo2');
}
foo();  // foo2
foo(); // foo2

deot avatar Jul 25 '17 08:07 deot

@deot 感谢补充,下一篇文章《JavaScript深入之变量对象》就会讲到如何进行变量提升的,这里的目的在于让大家认识到 JavaScript 并非一行一行执行代码~

mqyqingfeng avatar Jul 28 '17 02:07 mqyqingfeng

请问什么是全局代码、eval代码?只知道你讲的函数代码指的是函数……除了函数运行时有执行上下文,应该还有其他情况也有执行上下文吧?

ishowman avatar Aug 31 '17 07:08 ishowman

@ishowman 全局代码就是指在函数外面的代码,eval 代码是指在 eval 函数中写的代码,比如:

eval("x=10;y=20;document.write(x*y)")

mqyqingfeng avatar Sep 01 '17 05:09 mqyqingfeng

读了文章再读了下讨论有感而发:

  1. 关于提升,函数有函数声明和函数表达式之分,提升在二者之间是有区别的。函数表达式是不会被提升的。
  2. @zaofeng 的问题,我觉得楼上解释的有点问题。其实就是牵扯到JS的语法。你在浏览器输入var a = 2;会输出一个undefined的。在js的语法规则中,每个语句都是有一个结果的,var语句默认是undefined。 这个结果通常是不能被获取的,当然,办法还是有的: var a,b; a=eval('if(true){b=42}');a; // 42

cbbfcd avatar Nov 13 '17 02:11 cbbfcd

@cbbfcd 感谢补充哈~ ( ̄▽ ̄)~*

mqyqingfeng avatar Nov 15 '17 10:11 mqyqingfeng

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

这段代码为什么会是这个执行结果?

ECStack.push( functionContext); ECStack.pop(); ECStack.push( functionContext); ECStack.pop();

jasonzhangdong avatar Nov 19 '17 15:11 jasonzhangdong