Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mqyqingfeng opened this issue Apr 23, 2017 · 150 comments
Open

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

mqyqingfeng opened this issue Apr 23, 2017 · 150 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Apr 23, 2017

顺序执行?

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

@izhangzw
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@kevinxft
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@zaofeng
Copy link

zaofeng commented May 28, 2017

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

@qianlongo
Copy link

@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




@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented May 29, 2017

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

@xumengzi
Copy link

xumengzi commented May 30, 2017

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

另外还有个问题请教

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

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

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

@zaofeng
Copy link

zaofeng commented May 30, 2017

@qianlongo 谢谢解答

@mqyqingfeng
Copy link
Owner Author

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

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

@mqyqingfeng
Copy link
Owner Author

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

@qianlongo
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

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

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

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

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

@xdwxls
Copy link

xdwxls commented Jun 7, 2017

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

@suoz
Copy link

suoz commented Jun 8, 2017

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

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

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

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

@hxyweb
Copy link

hxyweb commented Apr 9, 2021

有个疑问,如果第二段代码执行上下文栈,先pop,再push,那在f函数中打印scope应该是全局的变量;感觉应该是闭包部分的只是,有变量继续使用,执行栈还保留;

@haitaoyu
Copy link

感谢大神,豁然开朗了很多~

@banli17
Copy link

banli17 commented May 6, 2021

栈底的全局上下文也是会被弹出的,比如执行 setTimeout(fn) 里的函数时,此时栈是空的,这时会重新初始化新的全局上下文(会执行如让指针执行 window的操作),然后再将 fn 压入栈。

@bigbigDreamer
Copy link

可以更新一个ES6版本的吗?现在不用var了= =

其实没多大必要,最终的 ES6+ 还是会被babel编译为 ES5,相信你的实力可以根据原理去自己改写一下

@klw19980718
Copy link

山东菏泽曹县 牛逼 666 我滴宝贝

@klw19980718
Copy link

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

如果我没理解错的的话这应该是尾调用吧?
如果是尾调用不应该和第二段代码的执行上下文栈一样吗?
也就是

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

我看楼上已经有人提到这个问题了,但是没有发现很好的回答。

return f() 的时候 会现在checkscope执行f函数 再完成checkscope函数的整体执行
所以是 先push() 再pop 再pop
而return f 的时候 会先执行完checkscope 等调用checkscope()()的第二个括号的时候 才执行f函数
所以先pop 再push() 再pop()

尾调用checkscope会提前出栈的。

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

这里说的是 遇到一个 执行函数 会创建一个执行上下文,假如说是只有一行代码 let name = ‘Tom’ ; 难道就不会创建一个执行上下文了吗 ?
还是说只要是遇到一段可执行代码 都会创建 一个执行上下文呢 ?

@yedongze
Copy link

var foo = function() {

    console.log('foo1');

}

foo();  // foo1

function foo() {

    console.log('foo2');

}

foo(); // foo1

function的函数声明比var的变量声明优先级更高,所以var声明的会覆盖function声明的

函数声明并不会被变量声明覆盖,而会被变量赋值覆盖

var foo;

foo();  // foo2

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

foo(); // foo1

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

foo(); // foo1

@Huanghaiting
Copy link

Huanghaiting commented Jul 26, 2021

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

函数无返回值 默认return undefined

@kflizongbao
Copy link

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

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

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

我对这个出栈的结果有疑问,因为 f 函数引用了 checkscope 函数的 scope 变量,所以我不认为 checkscope 先出栈。

chorme浏览器可以查看
image

@KevinZhang19870314
Copy link

@kflizongbao 用这个模拟器试试就知道了,https://www.jsv9000.app/

@mqyqingfeng
Copy link
Owner Author

@kflizongbao 用这个模拟器试试就知道了,https://www.jsv9000.app/

@kevinxft 这个模拟器很好哎

@ycshill
Copy link

ycshill commented Dec 15, 2021

var foo = function () {

console.log('foo1');

}

foo(); // foo1

var foo = function () {

console.log('foo2');

}

foo(); // foo2

这个例子能帮忙解释下吗,我没有太明白为什么是这个结果呢?

@bigbigDreamer
Copy link

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

函数无返回值 默认return undefined

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

@CurryPaste
Copy link

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,可以理解为变量声明提升。

@czj2017
Copy link

czj2017 commented Apr 25, 2022 via email

@ghost
Copy link

ghost commented Apr 29, 2022

大佬写的真好,已star

@ghost
Copy link

ghost commented Apr 29, 2022

讲的太他妈好了

@czj2017
Copy link

czj2017 commented May 3, 2022 via email

@wenjianmin
Copy link

@mqyqingfeng 大神想请教

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

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

@yanlei1234
Copy link

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

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

@badmanbadman
Copy link

badmanbadman commented Jul 12, 2022 via email

@userActor
Copy link

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

@Aa13615970643
Copy link

讲得有点浅

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

No branches or pull requests