威尼斯官方网站JavaScript深切之实践上下文栈

JavaScript 深入之执行上下文栈

2017/05/13 · JavaScript
·
执行上下文

原文出处: 冴羽   

已离开简书,原因参见
http://www.jianshu.com/p/0f12350a6b66。

先说说栈吧! 栈的执行顺序是先进后出,后进先出!

顺序执行?

如果要问到JavaScript代码执行顺序的话,想必写过JavaScript的开发者都会有个直观的印象,那就是顺序执行,毕竟

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
 
    console.log(‘foo1’);
 
}
 
foo();  // foo2
 
function foo() {
 
    console.log(‘foo2’);
 
}
 
foo(); // foo2

打印的结果却是两个foo2。

刷过面试题的都知道这是因为JavaScript引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如第一个例子中的变量提升,和第二个例子中的函数提升。

但是本文真正想让大家思考的是:这个”一段一段”中的“段”究竟是怎么划分的呢?

到底JavaScript引擎遇到一段怎样的代码时才会做’准备工作’呢?

虽人微言轻,但也要有自己的态度。

执行上下文栈 执行全局代码和函数时,都会产生一个环境,叫做执行上下文。一开始打开页面的时候。全局环境(全局执行上下文)就会入栈。当执行到函数时,该函数也会产生一个执行环境,并入栈。(如果函数里包含另一个函数,同理)等该函数执行完了,就将该函数的执行环境弹出栈,返回全局上下文环境。

可执行代码

这就要说到JavaScript的可执行代码(executable code)的类型有哪些了?

其实很简单,就三种,全局代码、函数代码、eval代码。

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

文章可以在我的 Github
https://github.com/mqyqingfeng/Blog
查看

威尼斯官方网站 1

执行上下文栈

接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?

所以js引擎创建了执行上下文栈(Execution context
stack,ECS)来管理执行上下文

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

1
    ECStack = [];

试想当JavaScript开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,让我们用globalContext表示它,并且只有当整个应用程序结束的时候,ECStack才会被清空,所以ECStack最底部永远有个globalContext:

ECStack = [ globalContext ];

1
2
3
    ECStack = [
        globalContext
    ];

现在JavaScript遇到下面的这段代码了:

function fun3() { console.log(‘fun3’) } function fun2() { fun3(); }
function fun1() { fun2(); } fun1();

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 伪代码
 
// 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();

1
2
3
4
5
6
7
8
9
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()();

1
2
3
4
5
6
7
8
9
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();

1
2
3
4
ECStack.push(checkscope> functionContext);
ECStack.push(f> functionContext);
ECStack.pop();
ECStack.pop();

让我们模拟第二段代码:

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

1
2
3
4
ECStack.push(checkscope> functionContext);
ECStack.pop();
ECStack.push(f> functionContext);
ECStack.pop();

是不是有些不同呢?

当然,如果觉得这样粗略的回答执行上下文栈的变化,依然显得不够详细,那就让我们去探究一下执行上下文到底包含了哪些内容,欢迎期待下一篇《JavaScript深入之变量对象》

举个例子

深入系列

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念,与罗列它们的用法不同,这个系列更注重通过写demo,捋过程、模拟实现,结合ES规范等方法来讲解。

所有文章和demo都可以在github上找到。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本系列:

  1. JavaScirpt 深入之从原型到原型链
  2. JavaScript
    深入之词法作用域和动态作用域

    1 赞 1 收藏
    评论

威尼斯官方网站 2

威尼斯官方网站 3

当加载<script>中的代码时,全局执行环境入栈

威尼斯官方网站 4

当执行到 10 行的时候,sayHello执行环境入栈

威尼斯官方网站 5

sayHello中还有一个sayWorld环境,执行到该环境(第 8
行)的时候,sayWorld执行环境入栈

威尼斯官方网站 6

sayWorld执行完毕后,就弹栈,回到sayHello执行环境,再sayHello执行完毕后,弹栈。回到全局执行环境。关闭浏览器的时候,全局执行环境也弹出栈!

整个入栈弹栈过程

威尼斯官方网站 7

执行上下文 什么是执行上下文。个人理解为代码的执行环境。不同的代码有不同的执行环境。
一个执行上下文的生命周期分为两阶段。创建阶段和代码执行阶段
创建阶段是为该环境的代码执行先做好准备。

创建阶段:包括创建变量对象、建立作用域链?、确定this的指向

1、创建变量对象:
① 建立arguments对象和参数的赋值。

②函数声明:也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
③变量声明:
检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

**举个例子:

**

威尼斯官方网站 8

威尼斯官方网站 9

这里作用域链和this的指向先不讲

然后就是执行阶段了。
执行的时候,若代码需要获取变量或函数或对象,就会到变量对象中去取!
若执行到代码赋值(如var eye = 2)就更行变量对象中同名变量的变量值!
在基本变量未执行赋值的时候读取变量,会取得undefined!

先这样吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注