乐趣区

关于前端:浅谈对JavaScript-中的执行上下文和执行栈的理解

大家好,金三银四马上也快到了,总据说行情不好,面试不好面,不过如同也没什么太大关系,该换新工作就换,只有准备充分还怕它什么行情不好。笔者呢最近也有想法所以再回顾 JavaScript 常识时,又看到了 JavaScript 的 执行上下文

那么这篇文章呢一小部分内容是我本人的一些了解。

大部分内容来自[译] 了解 JavaScript 中的执行上下文和执行栈

原文地址:Understanding Execution Context and Execution Stack in Javascript

例题

大家先来看一道较为简单的题,看下是否能看进去后果

var a = 10;
function fn(b) {
  b = 20;
  console.log(a, b);
}
function fn1() {
  a = 100;
  fn(a);
}
fn(200); // 输入后果
fn1(); // 输入后果

大家能够看进去输入后果是什么吗?

如果你曾经算进去的话,那么阐明你对执行上下文还是有一些了解的,欢送持续往下看加深印象

如果你没算进去或者输入后果与你算的不相符,那也先不要焦急,先看下边内容,看完后再回来算

执行上下文

概念

大家都晓得,JavaScript 代码的在运行的时候都是自上而下按程序执行的,然而呢理论并非是一行一行的执行,那大家有没有理解过它在执行代码的时候做过哪些筹备,做过哪些事件,比方代码解析、调配内容都是在哪解决的,那这个中央呢就是 执行上下文,是筹备工作的所在环境

执行上下文类型

执行上下文呢有三种类型,别离是

  • 全局执行上下文
  • 函数执行上下文
  • 还有就是 eval 函数执行上下文

那么咱们持续,执行上下文呢是在代码编译阶段创立的,来看看执行上下文的生命周期

执行上下文生命周期

  • 创立阶段
  • 执行阶段

创立阶段

执行上下文的创立阶段具体做了什么事呢,又分为三局部

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = {...},
  VariableEnvironment = {...},
}
确定 this 指向

在全局执行上下文中,this 指向的是全局对象

在函数执行上下文中,this 指向取决于该函数是如何被调用的

看下这个 demo

const obj = {fn: function(){console.log(this)
  }
}
​
obj.fn(); //fn: f();
​
const func = obj.fn;
​
func(); // Window
词法环境

官网的 ES6 文档把词法环境定义为

词法环境 是一种标准类型,基于 ECMAScript 代码的词法嵌套构造来定义 标识符 和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的援用 内部 词法环境的空值组成。

简略来说 词法环境 是一种持有 标识符—变量映射 的构造。(这里的 标识符 指的是变量 / 函数的名字,而 变量 是对理论对象 [蕴含函数类型对象] 或原始数据的援用)。

当初,在词法环境的 外部 有两个组件:(1) 环境记录器 和 (2) 一个 外部环境的援用

  1. 环境记录器 是存储变量和函数申明的理论地位。
  2. 外部环境的援用 意味着它能够拜访其父级词法环境(作用域)。

词法环境 有两种类型:

  • 全局环境 (在全局执行上下文中)是没有外部环境援用的词法环境。全局环境的外部环境援用是 null。它领有内建的 Object/Array/ 等、在环境记录器内的原型函数(关联全局对象,比方 window 对象)还有任何用户定义的全局变量,并且 this 的值指向全局对象。
  • 函数环境 中,函数外部用户定义的变量存储在 环境记录器 中。并且援用的外部环境可能是全局环境,或者任何蕴含此外部函数的内部函数。

环境记录器 也有两种类型(如上!):

  1. 申明式环境记录器 存储变量、函数和参数。
  2. 对象环境记录器 用来定义呈现在 全局上下文 中的变量和函数的关系。

简而言之,

  • 全局环境 中,环境记录器是对象环境记录器。
  • 函数环境 中,环境记录器是申明式环境记录器。

留神 — 对于 函数环境 申明式环境记录器 还蕴含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}
​
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Global or outer function environment reference>
  }
}
变量环境

它同样是一个词法环境,其环境记录器持有 变量申明语句 在执行上下文中创立的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着下面定义的词法环境的所有属性。

在 ES6 中,词法环境 组件和 变量环境 的一个不同就是前者被用来存储函数申明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

咱们看点样例代码来了解下面的概念:

let a = 20;
const b = 30;
var c;
​
function multiply(e, f) {
 var g = 20;
 return e * f * g;
}
​
c = multiply(20, 30);

执行上下文看起来像这样:

GlobalExectionContext = {
​
  ThisBinding: <Global Object>,
​
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },
​
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}
​
FunctionExectionContext = {
  ThisBinding: <Global Object>,
​
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
​
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

留神 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创立。

可能你曾经留神到 letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined

这是因为在创立阶段时,引擎查看代码找出变量和函数申明,尽管函数申明齐全存储在环境中,然而变量最后设置为 undefinedvar 状况下),或者未初始化(letconst 状况下)。

这就是为什么你能够在申明之前拜访 var 定义的变量(尽管是 undefined),然而在申明之前拜访 letconst 的变量会失去一个援用谬误。

这就是咱们说的变量申明晋升。

执行阶段

这是整篇文章中最简略的局部。在此阶段,实现对所有这些变量的调配,最初执行代码。

留神 — 在执行阶段,如果 JavaScript 引擎不能在源码中申明的理论地位找到 let 变量的值,它会被赋值为 undefined

执行栈

那根据上述执行上下文的了解,那咱们晓得在执行代码中会有很多的执行上下文,那么执行上下文是怎么确定执行程序的。

执行上下文寄存的地位就是在执行上下文栈,也叫调用栈。具备 LIFO(Last In First Out 后进先出,也就是先进后出)的个性。

那咱们来看下之前的例题,来剖析下

var a = 10;
function fn(b) {
  b = 20;
  console.log(a, b);
}
function fn1() {
  a = 100;
  fn(a);
}
fn(200); // 输入后果
fn1(); // 输入后果
  1. 首先进入全局执行环境,创立全局执行上下文环境并退出栈中
  2. fn()函数被调用,进入对应的函数执行环境,创立函数执行环境并退出栈
  3. 执行 console.log(a, b); 代码
  4. console.log(a, b); 代码出栈
  5. fn()函数执行结束后出栈
  6. fn1()函数被调用,进入对应的函数执行环境,创立函数执行环境并退出栈
  7. 持续 fn()函数被调用,进入对应的函数执行环境,创立函数执行环境并退出栈
  8. 执行 console.log(a, b); 代码
  9. console.log(a, b); 代码出栈
  10. fn()函数执行结束后出栈
  11. fn1()函数出栈
  12. 全局执行上下文出栈

题解

那咱们再来剖析下例题的答案

var a = 10;
function fn(b) {
  b = 20;
  console.log(a, b);
}
fn(200);

在执行 fn 函数时,此 fn 流动对象为

AO : {
  a: 10,
  b: 20,
  arguments: {0 : 20, length:0} 
}

所以此时输入后果为 10,20

持续看

var a = 10;
function fn(b) {
  b = 20;
  console.log(a, b);
}
function fn1() {
  a = 100;
  fn(a);
}
fn1();

在执行 fn1 函数时,此 fn1 流动对象为

AO : {
  a: 100,
  fn: reference to function fn(){}
  arguments: {length: 0} 
}

在继续执行 fn 函数时,此 fn 流动对象为

AO : {
  a: 100,
  b: 20,
  arguments: {0 : 20, length:0} 
}

所以此时输入后果为 100,20

结语

如果感觉此文的大屏数据交互方式对你帮忙的话,请不吝点个赞🥺🥺🥺,反对一下

退出移动版