草庐IT

浅谈对JavaScript 中的执行上下文和执行栈的理解

梁木由的前端新气象 2023-03-28 原文
大家好,金三银四马上也快到了,总听说行情不好,面试不好面,不过好像也没什么太大关系,该换新工作就换,只要准备充分还怕它什么行情不好。笔者呢最近也有想法所以再回顾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 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(​​let​​ 和 ​​const​​)绑定,而后者只用来存储 ​​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​​ 时,函数执行上下文才会被创建。

可能你已经注意到 ​​let​​ 和 ​​const​​ 定义的变量并没有关联任何值,但 ​​var​​ 定义的变量被设成了 ​​undefined​​。

这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 ​​undefined​​(​​var​​ 情况下),或者未初始化(​​let​​ 和 ​​const​​ 情况下)。

这就是为什么你可以在声明之前访问 ​​var​​ 定义的变量(虽然是 ​​undefined​​),但是在声明之前访问 ​​let​​ 和 ​​const​​ 的变量会得到一个引用错误。

这就是我们说的变量声明提升。

执行阶段
这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

注意 — 在执行阶段,如果 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

结语

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

有关浅谈对JavaScript 中的执行上下文和执行栈的理解的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  6. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  9. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐