JS 函数执行上下文 – 变量环境与词法环境在作用域中的区别

变量环境主要处理var和函数声明,在执行上下文创建时完成初始化,导致变量提升;词法环境则管理let、const及块级作用域,通过外部环境引用构建作用域链,并支持闭包。

js 函数执行上下文 - 变量环境与词法环境在作用域中的区别

JS函数执行上下文中的变量环境和词法环境,说到底,它们都是执行上下文的内部组件,但各自关注的侧面和行为逻辑有所不同。最核心的区别在于:变量环境(Variable Environment)主要负责处理var声明和函数声明的变量绑定,它的快照在执行上下文创建阶段就基本固定了;而词法环境(Lexical Environment)则是一个更动态、更精确的内部机制,它负责追踪letconst声明的变量,以及更细粒度的作用域链,它的内容在代码执行过程中会根据块级作用域的进入和退出而变化。

解决方案

要理解JS函数执行上下文中的变量环境与词法环境在作用域中的区别,我们得先从执行上下文本身说起。每次当JavaScript代码运行时,无论是全局代码、函数调用还是eval,都会创建一个执行上下文。这个上下文是一个抽象的概念,它包含了当前代码运行所需的所有信息。其中,变量环境和词法环境就是它最重要的两个内部组件,它们共同决定了我们代码中变量和函数的可见性与生命周期。

在我看来,变量环境更像是一个“老派”的记录员,它在函数被调用、执行上下文创建的那一刻,就一股脑地把所有var声明的变量(初始化为undefined)和完整的函数声明(可以直接使用)都记录下来。你可以把它想象成一个初稿,一个大体的框架。它的特点是,一旦创建,就相对稳定,不会随着代码块的进入和退出而频繁变化。这就是为什么var会有“变量提升”的现象,因为它的声明在变量环境里早就被处理了。

而词法环境,则显得更为“现代”和精细。它也有一个“环境记录器”(Environment Record),但这个记录器不仅包含函数参数,更重要的是,它负责管理letconst声明的变量。这些变量在声明前是无法访问的,这就是所谓的“暂时性死区”(Temporal Dead Zone, TDZ)。词法环境的另一个关键部分是它的“外部环境引用”(Outer Environment Reference),这个引用指向了外层(父级)的词法环境,从而构成了我们所说的“作用域链”。每次进入一个新的块级作用域(比如if语句、for循环或简单的{}),都会创建一个新的词法环境,并将其外部环境引用指向当前的词法环境,形成一个临时的链条。当块级作用域执行完毕,这个临时的词法环境就会被销毁,或者说不再活跃。

简单来说,变量环境是执行上下文创建时对var和函数声明的一个初始、粗粒度的扫描和绑定;而词法环境则是代码执行过程中,对所有变量(包括let, const, 函数参数,以及最终也会包含var变量的实际值)和作用域链进行动态、精确管理的核心机制。在ES6之后,随着letconst的引入,词法环境的重要性被大大提升,因为它才是实现块级作用域的真正幕后功臣。

JavaScript中函数执行上下文的创建过程是怎样的?

当我们谈论JavaScript中函数执行上下文的创建,其实是在描述一个分阶段的过程,这并非一蹴而就,而是经过精心设计的。我个人觉得,理解这个过程,能帮助我们更好地把握JS的变量提升、作用域以及闭包等核心概念。

首先,当JavaScript引擎发现一个函数即将被调用时,它会为这个函数创建一个全新的执行上下文。这个创建过程大致可以分为两个阶段:

创建阶段 (Creation Phase)

确定this绑定(This Binding):根据函数的调用方式(是作为方法调用、普通函数调用、构造函数调用还是通过call/apply/bind调用),this的值会被确定下来。创建词法环境组件(LexicalEnvironment Component)环境记录器(Environment Record):这个记录器会扫描函数体内的所有letconst声明的变量和函数声明。对于letconst,它们会被标记为“未初始化”(uninitialized),处于暂时性死区(TDZ),直到实际执行到声明语句时才会被初始化。对于函数声明,它们会被完全地存储在这个记录器中,可以直接使用(这就是函数声明提升)。外部环境引用(Outer Environment Reference):这个引用会指向创建当前函数的那个执行上下文的词法环境。这正是作用域链的基础。创建变量环境组件(VariableEnvironment Component)环境记录器(Environment Record):这个记录器专门处理var声明的变量和函数声明。所有var声明的变量都会被初始化为undefined(这就是var变量提升,但值为undefined),而函数声明则会被完全地存储下来。外部环境引用(Outer Environment Reference):在ES6之前,它通常和词法环境的外部环境引用指向同一个地方。在ES6之后,为了兼容var的旧行为,它仍然扮演着一个角色,但最终其内容会与词法环境的环境记录器合并。

执行阶段 (Execution Phase)

代码开始逐行执行。在执行过程中,变量的赋值操作会更新词法环境或变量环境中的值。当遇到letconst的声明语句时,它们会从“未初始化”状态变为实际的值。如果遇到新的函数调用,会重复上述的创建和执行过程,形成一个执行上下文

值得一提的是,在ES6及以后,最初创建时,词法环境和变量环境的环境记录器内容是有些重叠的。但随着代码的执行,特别是遇到块级作用域,词法环境会变得更加活跃和精细,它会为每个块级作用域创建新的环境记录器,而变量环境则相对保持不变,继续持有var声明的变量。这种分层处理,我觉得是JS引擎为了在引入块级作用域的同时,还能保持对旧var行为的兼容性所做出的巧妙设计。

为什么说变量环境主要处理var和函数声明,而词法环境则更关注letconst

这个区分是理解JavaScript作用域和变量提升机制的关键所在,我个人觉得,这是ES6对语言核心机制的一次重要重塑。

变量环境(Variable Environment):它的核心职责,或者说历史使命,就是处理那些“老旧”的var声明和函数声明。在执行上下文的创建阶段,JavaScript引擎会扫描当前作用域内的所有var声明的变量,并将它们添加到变量环境的环境记录器中,并初始化为undefined。同时,所有函数声明也会被完整地放置在这里,这也就是我们常说的“变量提升”和“函数提升”的根本原因。你可以把它看作是JS在早期版本中,为了实现变量和函数的“预处理”而设立的一个专门区域。它在上下文创建时就完成了大部分工作,后续代码执行时,只是去更新它里面的值。正因为这种“提前处理”的特性,var才没有块级作用域的概念,因为它只关心函数作用域或全局作用域。

function exampleVar() {  console.log(a); // undefined,因为a被提升了  var a = 10;  console.log(a); // 10}exampleVar();

在这个例子中,aexampleVar的变量环境创建时就被添加到其中,并初始化为undefined

词法环境(Lexical Environment):相较于变量环境,词法环境是更现代、更灵活的机制,它主要关注ES6引入的letconst声明。它的环境记录器在执行上下文创建时,也会记录letconst声明的变量,但与var不同的是,它们会被标记为“未初始化”。这意味着在代码执行到声明语句之前,这些变量是不可访问的,试图访问会导致ReferenceError,这就是“暂时性死区”(TDZ)。

function exampleLet() {  // console.log(b); // ReferenceError: Cannot access 'b' before initialization  let b = 20;  console.log(b); // 20}exampleLet();

这里的b虽然在词法环境创建时就被记录了,但它处于未初始化状态,直到let b = 20;执行才变得可用。

更重要的是,词法环境是实现块级作用域的基石。每当代码进入一个新的块({}),例如if语句、for循环、while循环,甚至只是一个独立的块,都会创建一个新的词法环境。这个新的词法环境会将其外部环境引用指向包含它的那个词法环境,形成一个临时的作用域链。当块执行完毕,这个临时的词法环境就会被销毁(或不再活跃),从而实现了letconst的块级作用域特性。

function blockScopeExample() {  let x = 1;  if (true) {    let y = 2; // 新的词法环境被创建,包含y    console.log(x); // 1 (从外部词法环境获取)    console.log(y); // 2  }  // console.log(y); // ReferenceError: y is not defined (块级作用域结束,y的词法环境被销毁)}blockScopeExample();

所以,变量环境可以看作是对var和函数声明的“静态”预处理,而词法环境则是对letconst以及整个作用域链进行“动态”和“精细”管理的核心。它们共同协作,构成了JavaScript复杂而强大的变量查找机制。

词法环境如何实现作用域链,以及它与闭包的关系?

词法环境在JavaScript中扮演着至关重要的角色,它不仅管理着变量的声明和生命周期,更是实现“作用域链”的根本机制。而作用域链,又直接引出了JavaScript中最强大也最容易让人困惑的特性之一——“闭包”。

词法环境如何实现作用域链?

我们知道,每个执行上下文都有一个词法环境,而每个词法环境内部都有一个关键的属性叫做Outer Environment Reference(外部环境引用)。这个引用指向的是创建当前词法环境的那个词法环境。换句话说,它指向了父级作用域的词法环境。

当JavaScript引擎尝试解析一个变量时,它会首先在当前执行上下文的词法环境(也就是当前作用域)中查找。如果找到了,就使用它。如果没找到,它就会沿着Outer Environment Reference向上,去父级词法环境中查找。这个过程会一直持续,直到找到变量或者到达全局词法环境。如果到了全局词法环境仍然没有找到,就会抛出ReferenceError。这个由一系列嵌套的词法环境通过Outer Environment Reference连接起来的链条,就是我们所说的作用域链(Scope Chain)

let globalVar = 'I am global';function outerFunction() {  let outerVar = 'I am outer';  function innerFunction() {    let innerVar = 'I am inner';    console.log(innerVar); // 1. 在innerFunction的词法环境找到    console.log(outerVar); // 2. innerFunction的Outer Env Ref指向outerFunction的词法环境,在那里找到    console.log(globalVar); // 3. outerFunction的Outer Env Ref指向全局词法环境,在那里找到  }  innerFunction();}outerFunction();

在这个例子中,innerFunction的词法环境的Outer Environment Reference指向outerFunction的词法环境,而outerFunction的词法环境的Outer Environment Reference又指向全局词法环境。这构成了一个清晰的作用域链:innerFunction -> outerFunction -> Global

词法环境与闭包的关系?

闭包,简单来说,就是一个函数能够记住并访问其“外部词法环境”中的变量,即使在其外部函数已经执行完毕并返回之后。而实现这一“记忆”能力的,正是词法环境的Outer Environment Reference

当一个内部函数被创建时,它不仅仅是创建了一个函数体,更重要的是,它会“捕获”或“记住”其创建时的词法环境。这个被捕获的词法环境,就是内部函数的Outer Environment Reference所指向的那个环境。

function createCounter() {  let count = 0; // count 存在于 createCounter 的词法环境中  return function() { // 这是一个内部函数    count++;        // 它访问了 createCounter 词法环境中的 count    console.log(count);  };}const counter1 = createCounter(); // createCounter 执行完毕,但其词法环境并未被销毁counter1(); // 输出 1counter1(); // 输出 2const counter2 = createCounter(); // 又创建了一个新的 createCounter 词法环境counter2(); // 输出 1

createCounter函数执行完毕后,通常情况下,它的局部变量count应该被垃圾回收。然而,由于createCounter返回的那个匿名内部函数(即counter1counter2所引用的函数)的Outer Environment Reference仍然指向着createCounter函数执行时创建的那个词法环境,这个词法环境(包括count变量)就不会被垃圾回收机制释放。它被“闭包”在了内部函数中。

所以,词法环境是作用域链的物理实现,而闭包则是词法环境这种机制所带来的一个强大且常见的结果。理解了词法环境如何通过Outer Environment Reference构建作用域链,就能彻底理解闭包的原理和它在JavaScript中实现数据私有化、函数工厂等高级模式的能力。

以上就是JS 函数执行上下文 – 变量环境与词法环境在作用域中的区别的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1523472.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
JavaScript:在具有相同类名的多个表单中访问类
上一篇 2025年12月20日 15:56:48
如何利用JavaScript的Service Worker实现离线缓存?
下一篇 2025年12月20日 15:57:00

相关推荐

  • composer require-dev和require有什么不同_Composer Require与Require-Dev区别解析

    require用于声明项目运行必需的依赖,如框架、数据库组件和第三方SDK,这些包会随项目部署到生产环境;2. require-dev用于声明仅在开发和测试阶段需要的工具,如PHPUnit、PHPStan、Faker等,不会默认部署到生产环境;3. 安装时composer install根据环境决定…

    2026年5月10日
    1000
  • 修复Django电商项目中AJAX过滤产品列表图片不显示问题

    在Django电商项目中,当使用AJAX动态加载过滤后的产品列表时,常遇到图片无法正常显示的问题。这通常是由于前端模板中图片加载方式(如data-setbg属性结合JavaScript库)与AJAX动态内容更新机制不兼容所致。解决方案是直接在AJAX返回的HTML中使用标准的标签来渲染图片,确保浏览…

    2026年5月10日
    000
  • 开源免费PHP工具 PHP开发效率提升利器

    推荐开源免费PHP开发工具以提升效率:VS Code、Sublime Text轻量高效,PhpStorm专业强大;调试用Xdebug、Kint、Ray;依赖管理选Composer;代码质量工具包括PHPStan、Psalm、PHP_CodeSniffer;数据库管理可用%ignore_a_1%MyA…

    2026年5月10日
    000
  • Matplotlib 地图中多类型图例的创建与优化

    Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化

    本教程旨在解决matplotlib地图可视化中,如何在一个图例中同时展示颜色块(如区域分类)和自定义标记(如特定兴趣点)的问题。文章详细介绍了当传统`patch`对象无法正确显示标记时,如何利用`matplotlib.lines.line2d`创建标记图例句柄,并将其与颜色块图例句柄合并,从而生成一…

    2026年5月10日 用户投稿
    100
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • 修复点击时按钮抖动:CSS垂直对齐实践

    本文探讨了在Web开发中,交互式按钮(如播放/暂停按钮)在点击时发生意外垂直位移的问题。通过分析CSS样式变化对元素布局的影响,我们发现这是由于按钮不同状态下的边框样式和内边距改变,以及默认的垂直对齐行为共同作用所致。核心解决方案是利用CSS的vertical-align属性,将其设置为middle…

    2026年5月10日
    000
  • 理解编程指令:当结果正确,但实现方式不符要求时

    本文探讨了在编程实践中,即使程序输出了正确的结果,但若其实现方式未能严格遵循既定指令,仍可能被视为“不正确”的问题。我们将通过具体示例,对比直接求和与累加求和两种实现策略,强调理解和遵守编程规范的重要性,以确保代码的健壮性、可维护性及符合项目要求。 在软件开发过程中,我们经常会遇到这样的情况:编写的…

    2026年5月10日
    000
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • php常量怎么用_PHP常量(define/const)定义与使用方法

    PHP中可通过define函数和const关键字定义常量,用于存储不可变值。define适用于全局作用域,支持动态名称和条件定义,如define(‘SITE_NAME’, ‘MyWebsite’);const在编译时生效,语法简洁但限制多,只能在类或全…

    2026年5月10日
    000
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    000
  • 前端缓存策略与JavaScript存储管理

    根据数据特性选择合适的存储方式并制定清晰的读写与清理逻辑,能显著提升前端性能;合理运用Cookie、localStorage、sessionStorage、IndexedDB及Cache API,结合缓存策略与定期清理机制,可在保证用户体验的同时避免安全与性能隐患。 前端缓存和JavaScript存…

    2026年5月10日
    100
  • HTML5网页如何实现手势操作 HTML5网页移动端交互的处理技巧

    首先利用原生touch事件实现滑动判断,再通过preventDefault解决滚动冲突,接着引入Hammer.js处理复杂手势,最后通过优化点击区域、避免事件冲突和增加视觉反馈提升体验。 在移动端浏览器中,HTML5网页可以通过触摸事件实现手势操作,提升用户体验。虽然原生JavaScript提供了基…

    2026年5月10日
    000
  • 深入理解 Express.js 中 next() 参数的作用与中间件机制

    本文深入探讨 express.js 中间件函数中的 `next()` 参数。它负责将控制权传递给请求-响应周期中的下一个中间件或路由处理程序。文章将详细解释 `next()` 的工作原理、中间件的注册与执行顺序,以及不正确使用 `next()` 可能导致请求挂起的风险,并通过代码示例和实际应用场景,…

    2026年5月10日
    000
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信