【译】JS的执行上下文和环境栈是什么?

简介: 这篇文章中,我将深入探讨JavaScript中的一个最基本的部分,即执行上下文(或称环境)。读过本文后,你将更加清楚地了解到解释器尝试做什么,为什么在声明某些函数/变量之前,可以使用它们以及它们的值是如何确定的。

这篇文章中,我将深入探讨JavaScript中的一个最基本的部分,即执行上下文(或称环境)。读过本文后,你将更加清楚地了解到解释器尝试做什么,为什么在声明某些函数/变量之前,可以使用它们以及它们的值是如何确定的。


执行上下文是什么?


在运行JavaScript代码时,执行环境非常重要,并可以认为是以下其中之一:


  • 全局代码 - 默认环境,你的代码第一时间在这里执行。
  • 函数代码 - 当执行流进入函数体的时候。
  • Eval代码 - eval函数内部的文本。【eval不建议使用】


你可以在网上查到大量的关于scope(作用域)的资料,本文的目的就是要让事情更加容易理解。我们把术语执行上下文视为当前代码的评估环境/范围。现在,条件充足,我们看个包含全局和函数/本地上下文评估代码的示例。


image.png


这里没什么特别的,我们有1个由紫色边框表示的全局上下文和由绿色、蓝色和橙色边框表示的3个不同的函数上下文。只有1个全局上下文,我们可以从程序的任何其它上下文访问。


你可以拥有任意数量的函数上下文,并且每个函数调用都会创建一个新的上下文,从而创建一个私有的作用域,无法从当前函数作用域外直接访问函数内部声明的任何内容。在上面的例子中,函数可以访问在其当前上下文之外声明的变量,但是外部上下文无法访问(函数)其中声明的变量/函数。为什么会这样?这段代码究竟是如何评估的?


环境栈


浏览器中的JavaScript解释器是单线程实现的。这意味着在浏览器中一次只能发生一件事情,其它动作或事件在所谓的执行栈中排队。下图是单线程栈的抽象视图:


image.png


我们知道,当浏览器首次加载脚本时,它默认进入全局执行上下文。如果在全局代码中调用一个函数,程序的顺序流就进入被调用的函数,创建一个新的执行上下文并将该上下文推送到执行栈的顶部。


如果你在当前函数中调用另外一个函数,则会发生同样的事情。代码的执行流程进入函数内部,该函数创建一个新的执行上下文,该上下文被推送到现有栈的顶部。浏览器将始终执行位于栈顶部的当前执行上下文,并且一旦函数完成当前执行上下文,它将从栈顶弹出,将控制权返回当前栈的栈顶上下文。下面的例子展示了递归函数和其程序的执行栈


(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));


image.png


上面代码只调用自身3次,将i的值递增1。每次调用函数foo时,都会创建一个新的执行上下文。一旦上下文执行完毕,它就会弹出栈并且将控制权返回它下面的上下文,直到再次到达全局上下文


关于执行栈有五个关键点:


  • 单线程
  • 同步执行
  • 1个全局上下文
  • 无限的函数上下文
  • 每个函数调用都会创建一个新的执行上下文,甚至是调用自身


执行上下文的细节


所以,我们现在知道每次调用一个函数时,都会创建一个新的执行上下文。但是,在JavaScript的解释器中,执行上下文的调用都有两个阶段:


  1. 创建阶段【调用函数时,但是在执行里面的代码之前】:
  • 创建作用域链
  • 创建变量,函数和参数
  • 确定this的值
  1. 激活/代码执行阶段:
  • 分配值,引用函数和解析/执行代码


可以将每个执行上下文在概念上标示为具有3个属性的对象:


executionContextObj = {
    'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
    'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
    'this': {}
}


活动/变量对象【AO/VO】


调用函数时,但在执行实际函数之前,会创建此executionContextObj。这被称为阶段1,即创建阶段。这里,解释器通过扫描传入的参数或参数的函数、本地函数声明和局部函数声明来创建executionContextObj。此扫描的结果将称为executionContextObj中的variableObject


以下是解释器如何评估代码的伪概述:


  1. 找些代码来调用一个函数
  2. 在执行函数代码之前,创建执行上下文
  3. 进入创建阶段
  • 初始化作用域链
  • 创建变量对象
  • 创建arguments对象,检查参数的上下文,初始化名称和值并创建引用的副本。
  • 扫描上下文以获取函数声明:
  • 对于找到的每个函数,在变量对象(或活动对象)中创建一个属性,该属性是确切的函数名称,该函数具有指向内存中函数的引用指针。
  • 如果函数名已存在,则将覆盖引用指针值。
  • 扫面上下文以获取变量声明:
  • 对于找到的每个变量声明,在变量对象(或活动对象)中创建一个属性,该属性是变量名称,并将值初始化为undefined。
  • 如果变量名称已存在于变量对象(或活动对象)中,则不执行任何操作并继续扫描(即跳过)。
  • 确定上下文中的this
  1. 激活/代码执行阶段:
  • 在上下文中运行/解释功能代码,并在代码逐行执行时分配变量值。


看下下面的例子:


function foo(i) {
    var a = 'hello';
    var b = function privateB() {
    };
    function c() {
    }
}
foo(22);


调用foo(22),创建阶段如下:


fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}


正如你所见,创建阶段处理定义属性的名称,而不是为它们赋值,但正式参数/参数除外创建阶段完成后,执行流程进入函数,激活/代码执行阶段在函数执行完毕后如下所示:


fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}
复制代码


“提升”一词


你可以在网上找到很多定义JavaScript术语-提升的资源,解释变量和函数声明是否被提升到其功能范围的顶部。但是,没有人详细解释为什么会发生这种情况,在掌握了关于解释器如何创建活动对象的新知识点,就很容易理解为什么了。看下下面的代码例子:


(function() {
    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined
    var foo = 'hello',
        bar = function() {
            return 'world';
        };
    function foo() {
        return 'hello';
    }
}());


我们现在可以回答下面这些问题了:


  • 为什么我们可以在声明foo前访问它?
  • 如果我们遵循创建阶段,我们就知道在激活/代码执行阶段之前就已经创建了变量。因此,当函数开始执行时,已经在活动对象中定义了foo

  • Foo被声明了两次,为什么foo显示为函数而不是undefinedstring呢?
  • 即使foo被声明了两次,我们从创建阶段中就知道到达变量之前在活动对象上已经创建了函数,并且如果活动对象上已经存在属性名称,我们就会绕过了声明。
  • 因此,首先在活动对象上创建函数foo()的引用,并且当解释器到达var foo时,我们已经看到名称foo存在,因此代码什么都不做并且继续。


  • 为什么bar是undefined
  • bar实际上是一个具有函数赋值的变量,我们知道变量是在创建阶段创建的,但它们是使用undefined值初始化的。


总结


希望到现在,你已经很好地掌握了JavaScript解释器是如何评估你的代码。理解执行上下文和环境栈可以让你了解代码的评估和你预期不同值的原因。


相关文章
|
Web App开发 JavaScript 前端开发
如何确保 Math 对象的方法在不同的 JavaScript 环境中具有一致的精度?
【10月更文挑战第29天】通过遵循标准和最佳实践、采用固定精度计算、进行全面的测试与验证、避免隐式类型转换以及持续关注和更新等方法,可以在很大程度上确保Math对象的方法在不同的JavaScript环境中具有一致的精度,从而提高代码的可靠性和可移植性。
|
8月前
|
JavaScript Ubuntu Linux
如何在阿里云的linux上搭建Node.js编程环境?
本指南介绍如何在阿里云Linux服务器(Ubuntu/CentOS)上搭建Node.js环境,包含两种安装方式:包管理器快速安装和NVM多版本管理。同时覆盖全局npm工具配置、应用部署示例(如Express服务)、PM2持久化运行、阿里云安全组设置及外部访问验证等步骤,助你完成开发与生产环境的搭建。
|
存储 JavaScript 前端开发
深入理解 JavaScript 执行上下文与 this 绑定机制
JavaScript 代码执行时,会为每段可执行代码创建对应的执行上下文,其中包含三个重要属性:变量对象、作用域链、和 this。本文深入剖析了执行上下文的生命周期以及 this 在不同情况下的指向规则。通过解析全局上下文和函数上下文中的 this,我们详细讲解了 this 的运行期绑定特性,并展示了如何通过调用方式影响 this 的绑定对象。同时,文中对箭头函数 this 的特殊性以及四条判断 this 绑定的规则进行了总结,帮助开发者更清晰地理解 JavaScript 中的 this 行为。
1785 8
深入理解 JavaScript 执行上下文与 this 绑定机制
|
11月前
|
机器学习/深度学习 JavaScript Cloud Native
Node.js作为一种快速、可扩展的服务器端运行时环境
Node.js作为一种快速、可扩展的服务器端运行时环境
198 8
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
1618 1
|
自然语言处理 JavaScript 前端开发
如何在 JavaScript 中创建执行上下文
在JavaScript中,每当执行一段代码时,都会创建一个执行上下文。它首先进行变量、函数声明的创建和内存分配(即变量环境和词法环境的建立),接着进入代码执行阶段,处理具体逻辑。
|
Web App开发 JavaScript 前端开发
探索Deno:新一代JavaScript/TypeScript运行时环境
【10月更文挑战第25天】Deno 是一个新兴的 JavaScript/TypeScript 运行时环境,由 Node.js 创始人 Ryan Dahl 发起。本文介绍了 Deno 的核心特性,如安全性、现代化、性能和 TypeScript 支持,以及开发技巧和实用工具。Deno 通过解决 Node.js 的设计问题,提供了更好的开发体验,未来有望进一步集成 WebAssembly,拓展其生态系统。
|
SQL JavaScript 数据库
sqlite在Windows环境下安装、使用、node.js连接
sqlite在Windows环境下安装、使用、node.js连接
|
存储 自然语言处理 JavaScript
如何在 JavaScript 中创建执行上下文
在JavaScript中,作用域链是一套用于查找变量和函数的机制,由当前执行上下文的变量对象和所有外层执行上下文的变量对象组成。它包括全局作用域、函数作用域和块级作用域。作用域链的工作原理是从内向外逐层查找变量,直至全局作用域。闭包通过作用域链记住其词法作用域,即使在外部作用域之外执行也能访问内部变量。作用域链有助于变量隔离、模块化和数据隐藏,提高代码的可维护性和可读性。
|
JavaScript Serverless Linux
函数计算产品使用问题之遇到Node.js环境下的请求日志没有正常输出时,该如何排查
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。

热门文章

最新文章