Skip to content

JavaScript深入之执行上下文栈 #4

Description

@mqyqingfeng

顺序执行?

如果要问到 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,对作者也是一种鼓励。

Activity

  1. izhangzw commented on May 10, 2017

    @izhangzw

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

  2. mqyqingfeng commented on May 10, 2017

    @mqyqingfeng
    OwnerAuthor

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

  3. kevinxft commented on May 13, 2017

    @kevinxft

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

  4. mqyqingfeng commented on May 13, 2017

    @mqyqingfeng
    OwnerAuthor

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

  5. zaofeng commented on May 28, 2017

    @zaofeng

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

  6. qianlongo commented on May 28, 2017

    @qianlongo

    @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
    
    
    
    
    
  7. mqyqingfeng commented on May 29, 2017

    @mqyqingfeng
    OwnerAuthor

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

  8. xumengzi commented on May 30, 2017

    @xumengzi

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

    另外还有个问题请教

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

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

  9. zaofeng commented on May 30, 2017

    @zaofeng

    @qianlongo 谢谢解答

  10. mqyqingfeng commented on May 31, 2017

    @mqyqingfeng
    OwnerAuthor

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

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

  11. mqyqingfeng commented on May 31, 2017

    @mqyqingfeng
    OwnerAuthor

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

  12. qianlongo commented on May 31, 2017

    @qianlongo

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

  13. mqyqingfeng commented on Jun 1, 2017

    @mqyqingfeng
    OwnerAuthor

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

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

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

  14. mqyqingfeng commented on Jun 1, 2017

    @mqyqingfeng
    OwnerAuthor

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

  15. 125 remaining items

  16. iambinlin commented on Dec 17, 2021

    @iambinlin

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

    函数无返回值 默认return undefined

    可以爬楼看下喔,chrome 会自动打印一个函数的执行结果。但是这仅仅是 chrome 的行为。
    image

  17. CurryPaste commented on Apr 9, 2022

    @CurryPaste

    var foo = function () {

    console.log('foo1');
    

    }

    foo(); // foo1

    var foo = function () {

    console.log('foo2');
    

    }

    foo(); // foo2

    这个例子能帮忙解释下吗,我没有太明白为什么是这个结果呢?
    @ycshill
    因为这里用的是var,所以是变量提升。
    执行顺序是

    • var foo = undefined
    • var foo = undefined
    • 给foo赋值一个函数 // foo1
    • 执行函数 foo
    • 再给foo赋值一个函数 // foo2
    • 执行 foo

    而另一个例子是函数提升

    • 先给foo赋值一个函数 // foo1
    • 再给foo赋值一个函数 // foo2
    • 执行foo
    • 执行foo

    引用一句话 函数提升和变量提升的原理一样,区别就是在于,函数提升已经创建好了函数对象,而变量提升赋值为undefined,可以理解为变量声明提升。

  18. czj2017 commented on Apr 25, 2022

    @czj2017
  19. czj2017 commented on May 3, 2022

    @czj2017
  20. wenjianmin commented on Jun 17, 2022

    @wenjianmin

    @mqyqingfeng 大神想请教

    var x=1;
    if(function f(){}){
        x+=typeof f;
    }
    console.log(x);//'1undefined'
    

    为什么这个例子的 function f(){} 不会函数声明前置呢? 该怎么解释这种现象呢
    因为if() 括号里面属于表达式,不是函数声明或者变量声明,不会进行变量提升

  21. yanlei1234 commented on Jul 12, 2022

    @yanlei1234

    想问下关于这段代码,为什么checkscope的执行上下文被弹出后f还能访问它内部的变量呢?

    function checkscope(){
        var scope = "local scope";
        function f(){
            return scope;
        }
        return f;
    }
    checkscope()();
    
  22. badmanbadman commented on Jul 12, 2022

    @badmanbadman
  23. userActor commented on Jan 13, 2023

    @userActor

    ECStack在压栈时候,那段代码执行了吧?以前一直觉得压栈是<先进后出>,如果 fn1/fn2/fn3都有console, 压栈的同时,fn1瞬间就打印了

  24. Aa13615970643 commented on Jul 29, 2023

    @Aa13615970643

    讲得有点浅

  25. ATravelerGo commented on Jul 2, 2024

    @ATravelerGo

    image 大神,这里有打印出来一个undefined,能解释一下吗 谢谢。已star
    代码一样 我运行发现没有undefined

  26. liy010 commented on Jul 2, 2024

    @liy010

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

    代码一样 我运行发现没有undefined

    你func1没有返回值,会默认返回undefined,然后被控制台打印出来

  27. YuFengjie97 commented on Dec 13, 2024

    @YuFengjie97

    最开始的例子跟执行栈有什么关系呢?
    var是声明提升,但是初始化不提升
    function是声明提升,但是表达式不提升

  28. shadowyy commented on Apr 15, 2025

    @shadowyy

    最

    说错了吧,function是声明和表达式都提升

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions