mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript深入之执行上下文栈

mqyqingfeng opened this issue · 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,对作者也是一种鼓励。

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

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

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

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

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

@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 十分感谢回答~ 正在写的 JavaScript 专题系列也有很多会涉及 underscore 的实现方法,多多交流哈~~~

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

另外还有个问题请教

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

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

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

@qianlongo 谢谢解答

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

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

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

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

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

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

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

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

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

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

commented

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

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

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

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

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

commented

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

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

commented

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

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

var a;
var b;

函数提升编译成啥样

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

commented

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

commented

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

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

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

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

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

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

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

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

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

// checkscope()() 就相当于
var f = checkscope();
f();

checkscope 函数执行,函数执行完毕后,该函数返回一个函数名,就相当于:

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

然后再执行的这个返回的函数,就相当于:

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

原来两者的关键区别,在于,第一个是在内部已经开始执行函数,因此形成了一个调用堆栈,而第二个,是一个返回内部函数指针的一个操作,关键是没有调用执行,因此没有产生上下文,此时的上下文是全局外面的
我估计,这个地方,就会体现,this的区别了,哈,看看后面的继续

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 先出栈。

@savoygu 哈哈,你不认为我也没有办法呀😂,因为我也不知道该怎么证明 ECStack 的变化呀,有谁知道利用 Chrome 或者其他工具能表明执行上下文栈的变化吗?

此外,你可以换个角度想,尽管它出栈了,但是它依然在内存中保留了引用到的变量~

@mqyqingfeng 😄 感谢

nice

一段一段的执行代码能不能看作作用域呢

@ShumRain 一段一段的代码指的是全局代码或者函数代码或者eval代码,遇到这种代码,比如一段函数的代码,就会创建一个执行上下文,执行上下文保存了作用域链的信息,规定了其中的变量能访问到的作用域。我觉得这样说也可以啦,只是感觉有点怪怪的……

commented

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

@deot 函数提升跟变量提升不一样,函数提升是"整个提升",所以第二个例子不应该是

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

如果是这样提升的话那:

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

应该报错,但实际上输出了'abc',说明函数提升是整个提升的

@GopherJ 你这里将函数声明和函数表达式混为一谈了。关于函数提升,只有函数声明才会提升,函数表达式是不会进行函数提升的

// 函数声明可以提升
foo(); // foo
function foo() {
    console.log('foo');
}
// 函数表达式不会提升
foo(); // TypeError: undefined is not a function
var foo = function() {
    console.log('foo');
}

上述第一个例子是函数声明,可以提升;第二个例子是函数表达式,不会进行函数提升,但是 var foo 会进行变量提升,所以当调用 foo 的时候,foo 的值是 undefined,导致抛出 TypeError 异常。

其实可以从 JS 引擎的执行原理来理解声明提升的过程。JS 引擎在分析代码的时候,分为两个阶段:编译阶段和执行阶段。(注意,这里的编译和 C 语言中的将高级语言编译为机器代码的编译不是同一个概念,这里的编译可以理解为博主所说的「准备工作」的一部分)

  • 编译阶段:这个阶段只会处理声明语句,会将所有声明的变量添加为当前执行上下文变量对象(VO)的属性。如果是变量声明,其值暂且初始化为 undefined,如果是函数声明,它会在堆上开辟内存,并将函数定义放到堆上,函数变量只保存这指向函数定义的地址。
  • 执行阶段:编译结束后,JS 引擎会再次扫描代码,这个阶段主要的任务是根据执行语句,更新变量对象等。

之所以会发生声明提升,就是因为 JS 引擎在编译阶段只会处理声明语句。

如果有兴趣了解更多,欢迎阅读我最近总结的文章:JS作用域

@mqyqingfeng 博主你好,非常感谢你分享这么有价值的心得,看完让人茅塞顿开。不过我想问几个问题。

  • 关于执行上下文入栈的时机

    你在《JavaScript深入之执行上下文栈》 一文中提到:

    当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

    也就是说,当函数调用的时候,创建了执行上下文,就会被压入栈中。但据我所知,调用栈一般都是用于函数调用时保存函数所出作用域的现场的。就是说,一开始栈是空的,当在全局作用域调用一个函数时,为了保护现场,将全局执行上下文压入栈。我也不是很确定,可能是我将执行上下文栈和函数调用栈认为是同一个栈了?如果博主有介绍这方面的资料,希望可以分享。

  • 执行上下文对象出栈后,是否立即就被销毁?

    你在 《JavaScript深入之闭包》 一文中提到:

    就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它

    按照你的意思,执行上下文只要被弹出栈,就会被销毁(如文中提到的 checkscopeContext),但是如果有其他 AO 引用了这个执行上下文的 AO 中的变量时,那么这个执行上下文的 AO 仍然保留着(销毁的只是这个执行上下文的作用域链和 this)。为什么不是整个执行上下文都被保留呢(在其 AO 被引用的情况下),如果只保留执行上下文的 AO,那 JS 引擎又是如何实现的呢,如果 checkscopeContext 被销毁,那不是也不能用 checkscopeContext.AO 去访问这个 AO 了吗?

@qianlongo @zaofeng 应该是在每一条执行语句结束时默认都会有一个返回值,如果没有是undefined;

var a= 2;//undefined
var a =3;
if(a>1){
	a = 2
} 
//2
var a = 1;
a++;
//1

感谢大神的教程。受益匪浅!已star~~

@Tate-Young 非常感谢指出~ 这里写得有问题,应该是程序结束之前始终存在

@nightn 首先非常感谢回答哈~关于第一个问题,函数调用栈的说法我也听说过,我觉得和执行上下文栈是一个东西。

为了保护现场,将全局执行上下文压入栈

这个说法我倒没有听说过,还想问你有什么资料可以分享呢~

关于第二个问题,为什么不是整个执行上下文都被保留呢,我觉得是没有必要保存那么多的东西,一些没有必要的变量可以直接销毁掉,毕竟还是要占内存。

如果只保留执行上下文的 AO,那 JS 引擎又是如何实现的呢?

说真的,我也很好奇😂

如果 checkscopeContext 被销毁,那不是也不能用 checkscopeContext.AO 去访问这个 AO 了吗?

本身也没有途径可以访问 checkscopeContext.AO 呐……

@ALL watch 我博客的各位同学,今天打扰大家了! 向大家致歉!

@mqyqingfeng 大神想请教

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

为什么这个例子的 function f(){} 不会函数声明前置呢? 该怎么解释这种现象呢

@Rainpia,console.log, 1function/undefined

image

大神,您好,关于您所说的“js不是一行一行“”执行的我有个疑问。
image
如图,关于第三行我的理解是:

  • 点操作符的优先级高与=,故a.x = undefined
  • 连等从右往左 ,先执行a={n:2}
  • 接着a.x=a,此时前者的指向的是原来的内存b:{n:1} ,后者的a指向的是新的内存 {n:2}
    故输出a {n:2} b:{n:1,x:{n:2}}

    所以此时我觉得js是一行一行执行,并进行数据更新,比如连我觉得可能会有第三方变量去缓存 a 的状态。

    但是,也就像您所提到的,变量提升啊,词法分析啊,我也觉得是js一定是以一个代码段进行预编译的,所以我就陷入了自相矛盾的境地,望大神不吝赐教,感谢,笔芯

@xuyonglin222
我认为JavaScript不是一行行执行而是一段段执行可以理解为:JavaScript的运行环境即执行上下文分为创建阶段和代码执行阶段。

  • 创建阶段,执行上下文会创建变量对象建立作用域链,以及确定 this 的指向
  • 代码执行阶段,创建完成后,才会开始执行代码,这个过程就是变量赋值函数引用,以及执行其他代码
    建议看看博主后面的几篇文章能更清晰了解JavaScript代码执行的机制。

@tsejx 不管是按行执行,还是按段执行,都会有编译和执行阶段,所问非所答

@xuyonglin222 既然你都知道了编译阶段和执行阶段,那么你所举的例子就无关乎于执行上下文的相关知识,而只是内存空间分配、变量对象的访问和表达式中运算符优先级的相关问题吧,何以通过你的例子你就得出JavaScript是一行一行执行的结论呢?

@tsejx a.x=a={n:2} 两个a指向的并不是同一块内存,a的状态在同一行并没有同步更新

commented

@xuyonglin222 同一时刻a只能指向一个地址。所以,

两个a指向的并不是同一块内存,a的状态在同一行并没有同步更新

这句话描述是有问题的。同一行的a不一定是同一时刻执行。同一行的a在同一时刻是指向同一地址的,从来不存在“两个a”。

        var a = { n: 1 };
        var b = a;
        a.x = a = { n: 2 };
        console.log(a);
        console.log(b);

这段代码里一共有两个对象,我们分别给这两个对象取个名字:objA = { n: 1 }; objB = { n: 2 };

  • 在执行 a.x = a = { n: 2 }ab都指向objA
  • 点运算符优先级高,所以a.x=a={n:2}这句话先执行a.x,这里a指向objA,也就相当于把原先的代码替换成了objA.x = a =objB
  • 然后执行赋值运算,从右往左,所以执行a = objBa是个变量,它现在指向objB
  • 再往左,执行赋值运算,objA.x = a,相当于objA.x = objB。此时objA被修改;

到此,已经很明白了,b始终指向objA,而a在执行a = { n: 2 }时指向了objB,所以才有这样的输出。

commented

@tsejx @xuyonglin222 关于JS是不是按行执行的,我觉得这个讨论没有意义,也得不出结果。

因为JS是要进行编译的。真正执行的并不是JS代码。

JS的编译肯定不是单纯地一行一行的编译的,包括变量提升之类的东西其实都是编译的时候决定的。平时大家说执行JS其实都是一种模糊的说法,毕竟真正执行的是JS编译之后的代码。

请问 这里说的执行上下文栈跟平时说的js的栈是一回事吗

commented

@mqyqingfeng 大神你好,小白有个问题向您求教 。为什么右边两个计数器的值没有叠加
-1

@ProDong 我试着回答一下你这个问题,错了的话请大神们纠正一下。
第一个:
第一个打印1、2、3是因为返回出来的函数c赋值给了变量d,本来a执行完过后,它的活动对象应该被清空,垃圾回收机制会将它回收,但是因为c中引用了a的变量,导致垃圾回收机制没有将a回收所以你下次再执行的时候变量b被保存下来了,也就是形成了闭包。
第二个:
直接执行a,此时活动对象为全局活动对象,a相当于在window直接执行,执行完后被垃圾回收机制给干掉了,此时a里面的变量b不存在了,所以你下次在执行a的时候b还是0,最后打印的还是1
第三个:
你仔细看第三个跟第二个是样的道理,返回出来的c函数实际上也是在window下直接执行的,执行完了过后a、c都被回收掉了

commented

@ProDong 个人看法,仅供参考,请指正。
第一个: d引用了c,c引用了b,d不被销毁,b不被销毁。
第二个:�c在a中执行,作用域在a内部。
第三个:没有变量引用a(), 也就是没有变量引用c,c()后被销毁。

@cuideqi 你的回答估计有一丁点问题:
第一个,你说d执行完后会被销毁,此时因为闭包a不会被销毁。a不被销毁所以a里面的变量b不被销毁,但是d每次执行完了会被销毁。
第二个,c在a中执行的说法不准确,c在执行的时候有三个活动对象,分别是全局活动对象、a的活动对象,和她自己执行时推入的活动对象。而这不是重点原因,重点原因是c执行完后被销毁,然后a被销毁,所以变量b没有被保存。

es2015之后,是不是有了块级执行上下文

commented
function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

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

不明白这两个的区别?为什么第一个不是

ECStack.push(<fun1> functionContext);
pop();
ECStack.push(<fun2> functionContext);
pop();
ECStack.push(<fun3> functionContext);
pop()

呢?
是因为遇到函数执行才push嘛?

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

@hzjswlgbsj太感谢了!明白了( ^ω^ )

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

f1 没有return 默认返回 undefined

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

另外还有个问题请教

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

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

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

通过函数声明创建的函数都会被提升

callstack 2018-11-14 14_46_32
javascript是单线程的,只有一个调用栈。
当函数被执行时,v8创建一个栈帧被压入调用栈中,该函数return之后,栈帧被弹出,直到调用栈清空后

@Dsying 这个工具是什么呀

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

你理解错了,setTimeout的第二个参数delay,是进入任务队列的时间,而不是setTimeout第一个参数function的执行时间,该function具体执行时间视当前call statck中的同步任务而定,同步任务没有全部执行完毕之前,该function只能待在异步任务队列中,只有当同步任务全部执行完毕,异步任务队列中的任务才会按照入队顺序依次进入call stack,setTimeout的function只有在最理想状态下(call stack为空)才会到点就执行,
image

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

while(Date.now() - now < 99) {}
while(Date.now() - now < 150) {}
while(Date.now() - now < 201) {}
这三种情况你分别试试就知道为什么了,
< 99 时 你两个定时器还都没进队列 所以时间差在100左右
< 150时 第一个在100ms左右进入队列,150ms 调用栈空了,第一个定时器执行,200ms的时候第二个定时器进入队列,此时调用栈也为空,随即进入调用栈执行,两者相差50ms左右,
< 201 时 第一个在100ms左右进入队列,第二个在200ms进入队列,但调用栈要在201ms被清空,所以201ms时,第一个定时器先进入,第二个定时器再进入,两者时间差极短

而你的例子恰好是第3种情况

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

while(Date.now() - now < 99) {}
while(Date.now() - now < 150) {}
while(Date.now() - now < 201) {}
这三种情况你分别试试就知道为什么了,
< 99 时 你两个定时器还都没进队列 所以时间差在100左右
< 150时 第一个在100ms左右进入队列,150ms 调用栈空了,第一个定时器执行,200ms的时候第二个定时器进入队列,此时调用栈也为空,随即进入调用栈执行,两者相差50ms左右,
< 201 时 第一个在100ms左右进入队列,第二个在200ms进入队列,但调用栈要在201ms被清空,所以201ms时,第一个定时器先进入,第二个定时器再进入,两者时间差极短

而你的例子恰好是第3种情况

非常感谢大神回答,对setTimeout的第二个参数的问题,没有异议,但对于function何时出队列,进入执行上下文栈,我还是持怀疑态度,大神如果有资料,也请不吝赐教,我把这个改一下,由于主线程执行了500ms(姑且认为有个主线程),此处两个setTimeout的时间都到了,按照大神的意思也就是时间都到了,调用栈被清空了,此时两个setTimeout都要出队,按照队列先进先出的数据结构特点,此时应该是按顺序打印,但实际是100ms那个先打印,所以,本人猜测其实这两个setTimeout都已经到时间出队入栈了(栈结构式后进先出,所以才会根据时间顺序,入栈并不是直接执行函数,要等待栈顶函数处理完毕才会从栈顶取新的函数),各位欢迎拍砖,我也想彻底弄明白,感谢各位~

setTimeout(()=>{
  console.log(1)  
}, 200); 
setTimeout(()=>{
  console.log(2);
},100);  
var now = Date.now();
 while(Date.now() - now < 500) {}

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

while(Date.now() - now < 99) {}
while(Date.now() - now < 150) {}
while(Date.now() - now < 201) {}
这三种情况你分别试试就知道为什么了,
< 99 时 你两个定时器还都没进队列 所以时间差在100左右
< 150时 第一个在100ms左右进入队列,150ms 调用栈空了,第一个定时器执行,200ms的时候第二个定时器进入队列,此时调用栈也为空,随即进入调用栈执行,两者相差50ms左右,
< 201 时 第一个在100ms左右进入队列,第二个在200ms进入队列,但调用栈要在201ms被清空,所以201ms时,第一个定时器先进入,第二个定时器再进入,两者时间差极短
而你的例子恰好是第3种情况

非常感谢大神回答,对setTimeout的第二个参数的问题,没有异议,但对于function何时出队列,进入执行上下文栈,我还是持怀疑态度,大神如果有资料,也请不吝赐教,我把这个改一下,由于主线程执行了500ms(姑且认为有个主线程),此处两个setTimeout的时间都到了,按照大神的意思也就是时间都到了,调用栈被清空了,此时两个setTimeout都要出队,按照队列先进先出的数据结构特点,此时应该是按顺序打印,但实际是100ms那个先打印,所以,本人猜测其实这两个setTimeout都已经到时间出队入栈了(栈结构式后进先出,所以才会根据时间顺序,入栈并不是直接执行函数,要等待栈顶函数处理完毕才会从栈顶取新的函数),各位欢迎拍砖,我也想彻底弄明白,感谢各位~

setTimeout(()=>{
  console.log(1)  
}, 200); 
setTimeout(()=>{
  console.log(2);
},100);  
var now = Date.now();
 while(Date.now() - now < 500) {}

你还是没有明白setTimeout的第二个参数是什么含义,进入队列不是按照你setTimeout书写的顺序,而是根据第二个参数delay,100ms先于200ms的定时器先进入任务队列,所以出列的时候100ms的定时器对应的任务函数先入栈,输出顺序是2,1

ECStack 最底部永远有个 globalContext

hi,想请教一下,这里的ECStack是不是可以理解为执行栈或调用栈?但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

“这时候只有检测到调用栈为空的时候,才会把队列的事件放到栈中执行”,消息队列不会去检测执行上下文栈是否为空的,只要满足触发条件就会将函数压入执行上下文栈,而不会去管执行上下文栈是否为空,比如setTimout,时间一到,消息从消息队列出队,然后callback压入执行上下文栈,如果此时执行上下文栈有未执行完的函数,那么这个压入栈的函数也只能在执行上下文栈最上面等待出栈,所以setTimeout会有延迟的情况,上个例子
setTimeout(()=>{console.time()},100); setTimeout(()=>{console.timeEnd()},200); var now = Date.now(); while(Date.now() - now < 500) {}
代码中的setTimeout只会将函数压入栈,不会去管执行上下文栈是否有未处理完的函数,所以此处的执行时间就不是预期的相差100左右的数值了,是多少大家执行以下试试?

你理解错了,setTimeout的第二个参数delay,是进入任务队列的时间,而不是setTimeout第一个参数function的执行时间,该function具体执行时间视当前call statck中的同步任务而定,同步任务没有全部执行完毕之前,该function只能待在异步任务队列中,只有当同步任务全部执行完毕,异步任务队列中的任务才会按照入队顺序依次进入call stack,setTimeout的function只有在最理想状态下(call stack为空)才会到点就执行,
image

@Dsying 老哥你这什么工具这么神奇

wechatimg23
大神,我这里输出的是foo1,foo2呀,

wechatimg23
大神,我这里输出的是foo1,foo2呀,

函数声明和函数表达式 了解下

有个疑问,思考题里的这个:

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

return f(); 算不算是尾调用呢?如果是的话,checkscope 应该会被先弹出执行上下文栈吧?

@guanyingjie
命令式和声明式的代码在执行时是有区别的

您好,小白想请教一下

console.log(a())

function a() {
  ...
}

这样的情况下执行上下文栈的顺序是怎么样的?

还有另一个问题
如果

function a() {
  console.log('111')
}
a()

function a() {
  console.log('222')
}
a()

等同于

function a() {
  console.log('111')
}

function a() {
  console.log('222')
}

a() // 222
a() // 222

的话,那么在进入执行上下文(假设就在全局下)这个阶段VO中a 赋值为 function() {}这一步就完成了函数的覆盖吗?

@hanqizheng 函数提升之后,找到a()执行function a() { console.log('222') },那么此时输出222,再次执行a()并且输出222,结束。
可以结合楼上那位朋友@Dsying 的动态演示图来分析。

请教下 作用域链 和 执行上下文的顺序。

var a = [1];
function f(a){
a[100] = 3
a = [1,2,3];
}
f(a)
console.log(a);

为啥输出的不是[1,2,3]呢

执行上下文仅仅是准备工作吗(变量提升和函数提升这一块)?感觉不太通顺,执行上下文描述的是一个名词,而变量提升、函数提升这种准备工作更像是一个动作。执行上下文到底是一个区域范围,还是就是博主说的准备工作,求解

@qq781245153

var a = [1];
function f(a){ 
  a[100] = 3; // 因为f(a)是引用方式传参,即f函数内部对参数a的修改就是对原数组a的修改。
  a = [1,2,3]; // 这里仅仅是把引用参数变量a指向新数组[1,2,3],并不会覆盖原数组a。
}
f(a);
console.log(a); // 结果a肯定不是[1,2,3] 而是 [1, empty × 99, 3]

文中2道题的理解
Clearives/clearives.github.io#17

写的可真好啊,我刚想了解执行上下文有哪些内容下篇文章就是了。自己最近一段时间来也一直在学javascript,有些概念觉得已经明白了,但是要自己写总结性笔记时发现笔记结构实在太差了。

有个疑问,思考题里的这个:

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

return f(); 算不算是尾调用呢?如果是的话,checkscope 应该会被先弹出执行上下文栈吧?

我也在想这个问题.

commented

个人感觉第二个函数执行上下文入栈出栈应该是这样的:

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

@mqyqingfeng 所谓的“准备工作”是不是就是代码的编译阶段呀,编译阶段过后的代码执行阶段才是创建“执行上下文环境”吧?“准备工作”应该不包含“执行环境”的创建吧?这点有点晦涩,不知道这么理解是否正确

ECStack.push( functionContext);
ECStack.pop();
ECStack.push( functionContext);
ECStack.pop();
这个里面checkscope函数结束后f函数怎么还能找到。。checkscope函数出栈后不是清空了吗?

commented

大佬想到个问题,如果执行代码中遇到不可执行的代码,那执行上下文栈会发生什么变化,当前执行的这个函数会直接从栈中弹出么@mqyqingfeng

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 先出栈。
我是这么理解的:checkscope的变量对象并没有被销毁。

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

这样说的话,是不是可以理解为可执行代码那里的三个分类应该是这样的?

可执行代码

  • 全局的可执行代码(表达式,赋值语句等)
  • 函数的调用
  • eval的调用

这样理解对吗?

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 先出栈。

我是这么理解的:checkscope的变量对象并没有被销毁。

你可以看一下这个, 就是上下文对象里有作用域链,里面包含所有父级变量,所以 f 函数的执行上下文也包含了 checkscope 里面的变量
https://pjf.name/blogs/what-is-execution-context-and-stack-in-javascript.html

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 先出栈。
我是这么理解的:checkscope的变量对象并没有被销毁。

你可以看一下这个, 就是上下文对象里有作用域链,里面包含所有父级变量,所以 f 函数的执行上下文也包含了 checkscope 里面的变量
https://pjf.name/blogs/what-is-execution-context-and-stack-in-javascript.html

出栈只是执行上下文被销毁,而不代表checkScope的变量被销毁吧,这里因为产生了闭包,所以scope变量不会被销毁,我有一个不成熟的猜测,如果checkscope里由别的变量会在checkscope执行完后销毁.例如这样:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; var aaa = 'test'; // 这个变量会在checkscope调用完后销毁,而scope变量不会 function f(){ return scope; } return f; } checkscope()();
不知道我这样理解对不对.

commented

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

这样说的话,是不是可以理解为可执行代码那里的三个分类应该是这样的?

可执行代码

  • 全局的可执行代码(表达式,赋值语句等)
  • 函数的调用
  • eval的调用

这样理解对吗?
我的理解是执行上下文是对应声明时的,而执行上下文栈是对应调用时

文中: "当执行一段代码的时候,会进行一个'准备工作',比如第一个例子中的变量提升,和第二个例子中的函数提升。" 这里关于应变量提升的概念,mdn更准确的描述是,在"编译阶段被放入内存",所以更加准确的描述,是不是应该为"编译"而不是"执行"?

文中,开篇举的例子貌似跟上下文栈关系不大,倒是跟变量对象关系挺大的。因为变量对象中涉及了变量声明提升和函数声明提升的原理。