执行上下文与执行栈 -- JavaScript

本文主要记录全局执行上下文函数执行上下文

1. 执行上下文

简而言之, 执行上下文 就是 JavaScript 代码运行的环境。

1.1 分类

在JavaScript中,有三种执行上下文类型:

  • 全局执行上下文 :这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

  • 函数执行上下文 : 每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

  • Eval 函数执行上下文 : 行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

1.2 执行上下文存储的内容

在不同的执行上下文中所存储的内容不尽相同,但是每一个执行上下文,都会存在一个 变量对象(Variable Object 简称VO这个变量对象将会存储当前执行函数的变量声明等内容。

2. 执行栈

  • 执行栈(Execution Context Stack 简称ECS), 也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文

  • JavaScript引擎第一次遇到你的脚本时,它会创建一个全局执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

  • 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

3. 全局执行上下文

所有的 JavaScript 代码在执行之前就会创建一个执行栈,而每一个执行栈的最开始的时候,都会存放一个 全局执行上下文Global Execution Context 简称GEC)。 因为执行栈是一个栈结构,那么则表示 GEC全局执行上下文) 会存在于栈底,当全部的 JavaScript 代码执行完毕之后,GEC才会出栈。

3.1 GEC(全局执行上下文) 中存储的内容

  1. 全局对象Global Object 简称 GO): 这里面存储的是在全局中声明的所有变量声明和函数声明。但是在 GO全局对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
  • 变量声明: 在 GO中存储的变量声明,在声明的时候,全部声明undefined,在真正执行的时候,才会给每一个变量进行赋值操作。
  • 函数声明: 在 GO中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中
  1. 执行代码: 就是在全局中将要执行的所有代码。

3.2 window 下的全局执行上下文

在浏览器中执行 JavaScript 代码(或者说是 V8 引擎下执行 JavaScript 代码) 的时候,GEC全局执行上下文) 中的 VO变量对象),会被赋值为 GO全局对象),这个时候 VO(变量对象)和 GO(全局对象)是同一个东西,在浏览器下就是 window 对象。所以在浏览器执行的 JavaScript 代码的时候,在全局中打印 this 的话,打印出来的是 window 对象。

var foo = 'hello world';

function bar() {
  console.log(name);
}
bar();

在这里插入图片描述
因为在浏览器中的 window 对象中,window 对象本身就绑定了很多属性,比如定时器、一些浏览器事件函数等等。但是最神奇的一点是在 window 对象中,又绑定了 window,如此循环。如果打印一下console.log(window.window.window....),并不会报错,而是继续打印出 window 对象。这是因为在 window 这个对象中,又绑定了这个 GO(全局对象),因为浏览器中的全局对象是 window,所以如此往复,window 中嵌套 window

在这里插入图片描述

4. 函数执行上下文(FEC

在整个 ECS(执行栈)中,除了最底层的 GEC(全局执行上下文),剩下的其他执行上下文就是 函数执行上下文Function Execution Context 简称FEC)。

其实还有 Eval 函数执行上下文, 本文不讲述 Eval 函数执行上下文。

JavaScript 代码中的每一个函数都会创建一个 FEC(函数执行上下文),只有在执行到该函数的时候,才会将其 FEC(函数执行上下文)存入执行栈中,当执行完后又会将该 FEC(函数执行上下文)进行销毁,即出栈。

4.1 FEC中存储的内容

FECGEC 存储的内容不尽相同:

  1. 活动对象Activation Object 简称AO): 这里面存储的是在全局中声明的所有变量声明和函数声明。但是在 AO活动对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
  • 变量声明: 在 AO中存储的变量声明,在声明的时候,全部声明undefined,在真正执行的时候,才会给每一个变量进行赋值操作。
  • 函数声明: 在 AO中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中
  1. 执行代码: 就是在全局中将要执行的所有代码。

重点:

  • GEC在浏览器下运行的时候,其 VO(变量对象)会被赋值为 GO(全局对象)
  • FEC(函数执行上下文)中的 VO(变量对象),会被赋值为 AO(活动对象)

执行下面的代码,我将该代码在浏览器中运行时整个 ECS画个草图展示一下:

var foo = 'hello world';

function bar() {
  var age = 10;
  console.log(name);
}
bar();

在这里插入图片描述

5. 经典案例

比较下面两段代码,试述两段代码的不同之处。

// 第一个
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()();

答案:两段代码执行结果都是 local scope 。但是这两个代码在执行栈的执行过程则是截然不同。

5.1 代码一的解析

  1. 首先创建一个GEC(全局执行上下文),压入到 ECS(执行栈)的栈底;将全局中的 变量声明函数声明 都绑定到 GO(全局对象)上。

在这里插入图片描述

  1. 开始执行 GEC ,给变量 scope 赋值 global scope;当执行到 checkscope() 调用 checkscope 的函数的时候,会给checkscope函数创建一个函数执行上下文FEC),将其压入 ECS(执行栈)。

在这里插入图片描述
2. 执行 checkscope 函数 FEC(函数执行上下文)中的代码,给 变量 scope 赋值 local scope;当执行到 return f() 的时候,因为又调用了函数 f,所以又要创建一个函数 f 的执行上下文,压入到 ECS(执行栈)中。

在这里插入图片描述

  1. 当函数 f 全部执行完毕之后,那么他的执行上下文将会出栈。

在这里插入图片描述

  1. 当函数 checkscope 全部执行完毕之后,他的执行上下文将会出栈。

在这里插入图片描述

  1. ECS(执行栈)中只剩下 GEC(全局执行上下文),没有任何其他的代码需要去执行的时候, GEC(全局执行上下文)也会出栈,将 ECS(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。

5.2 代码二的解析

  1. 首先创建一个 GEC (全局执行上下文),压入 ECS(执行栈)的栈底;将全局中所有 变量声明函数声明 都绑定在 GO(全局对象)中。

注意:在代码执行中我写的是执行 checkscope(),并不是执行checkscope()(),因为执行函数是只有一个小括号才表示执行第二个小括号表示的是,执行 checkscope() 后返回的函数再去执行。

在这里插入图片描述

  1. 开始执行 GEC(全局执行上下文),给 变量 scope 赋值 global scope;当执行到 checkscope() 调用 checkscope 函数的时候,会给 checkscope 函数创建一个FEC(函数执行上下文),压入 ECS(执行栈)中。

注意:红色圈起来的部分,代码中只是将 f 这个函数返回了,并没有去执行,所以下一步并不会给 f 函数创建一个执行上下文。

在这里插入图片描述

  1. 经过第二步之后,checkscope 所有的代码都已经执行完毕,注意 return 返回的是 f 函数,并不是f(),只有函数调用的时候才会创建 FEC(函数执行上下文)。所以这里将 checkscope 函数执行完毕之后,就会将 checkscope 的执行上下文销毁。

在这里插入图片描述

  1. 这时候,checkscope 函数最后返回的函数 f,会被调用 —— 因为 checkscope()() => f()。这里才对 f 函数进行了调用,所以又会创建一个函数 f 的执行上下文。

在这里插入图片描述

  1. 函数 f 执行完毕之后,就会将 f 的执行上下文进行销毁。

在这里插入图片描述

  1. ECS(执行栈)中只剩下 GEC(全局执行上下文),没有任何其他的代码需要去执行的时候, GEC(全局执行上下文)也会出栈,将 ECS(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。

The execution context, lexical environment, variable environment
在这里插入图片描述

6. 几道面试题

6.1 例题1

var n = 100

function foo() {
  n = 200
}
foo()
console.log(n) 

// 200

6.2 例题2

function foo() {
  console.log(n) //undefined
  var n = 200
  console.log(n) //200
}

var n = 100
foo()
// undefined
// 200

6.3 例题3

var n = 100;

function foo1() {
    console.log(n) //100
}

function foo2() {
    var n = 200
    console.log(n) //200
    foo1()
}

foo2()
console.log(n) //100
// 200
// 100
// 100

6.4 例题4

var a = 100

function foo() {
  console.log(a) //undefined 局部变量
  return
  var a = 200
}

foo()
// undefined

6.5 例题5

先说一个 bug,代码里不要这么写。


这是JavaScript的bug
function foo() {
  m = 100
}

foo()
console.log(m) //可以在外部访问到m,注意:m前没用var声明
// 100

看题:

function foo() {
  var a = b = 10
  // => 可以转成下面的两行代码
  // var a = 10
  // b = 10
}

foo()

// console.log(a) //ReferenceError: a is not defined
console.log(b) // 10

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值