news 2026/4/23 11:22:41

深入理解JavaScript词法作用域与作用域链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解JavaScript词法作用域与作用域链

为什么 JavaScript 的函数总能清楚地"记住"变量在哪里被定义?为什么闭包如此神奇?这一切的答案都隐藏在"词法作用域"这个核心概念中。

前言:从一道经典面试题说起

var a = 1; function outer() { var a = 2; function inner() { console.log(a); } return inner; } var innerFunc = outer(); innerFunc(); // 输出什么?为什么?

大多数前端开发者都知道输出结果为:2,但能完整解释"为什么"的人却不多。本篇文章就来彻底揭开JavaScript作用域的神秘面纱。
什么是词法作用域?
静态作用域 vs 动态作用域
词法作用域(Lexical Scope),也称为静态作用域,是 JavaScript 采用的作用域模型。它的核心特点是:
函数的作用域在函数定义时就确定了,而不是在函数调用时确定。

这与动态作用域形成鲜明对比。让我们通过代码理解两者的区别:

var value = "global"; function foo() { console.log(value); } function bar() { var value = "local"; foo(); // 输出什么? } bar();

上述代码的输出结果是:global。因为foo()函数在定义时,它的作用域链就已确定,包含全局作用域。所以它访问的是全局的value变量,而不是调用位置的value
如果JavaScript动态作用域(实际上不是),又会发生什么呢?

var value = "global"; function foo() { console.log(value); // 动态作用域下:访问调用位置的value } function bar() { var value = "local"; foo(); // 动态作用域下会输出:"local" } bar();

关键区别总结

  • 词法作用域:函数的作用域由定义位置决定。
  • 动态作用域:函数的作用域由调用位置决定。

词法环境的结构
在 JavaScript 引擎内部,每个执行上下文都有一个关联的词法环境(Lexical Environment)。词法环境由两部分组成:环境记录器(EnvironmentRecord)和对外部词法环境的引用(Outer)。

LexicalEnvironment = { EnvironmentRecord: { // 1. 环境记录器:存储变量和函数声明 // 包含:声明式环境记录、对象环境记录 }, Outer: null | <父级词法环境引用> // 2. 对外部词法环境的引用 } // 实际代码示例 var globalVar = "global"; function outer() { var outerVar = "outer"; function inner() { var innerVar = "inner"; // inner函数的词法环境: // { // EnvironmentRecord: { innerVar: "inner" }, // Outer: <outer函数的词法环境> // } } }

作用域链的形成过程
作用域链就是由这些词法环境通过Outer引用连接起来的链式结构。
作用域链的查找机制
变量查找的完整流程
当 JavaScript 引擎需要访问一个变量时,它会按照以下步骤进行查找:

// 多层嵌套作用域示例 var a = "global a"; var b = "global b"; var c = "global c"; function level1() { var a = "level1 a"; var b = "level1 b"; function level2() { var a = "level2 a"; function level3() { var a = "level3 a"; console.log(a); // "level3 a" - 找到最近的a console.log(b); // "level1 b" - 向上两层找到b console.log(c); // "global c" - 向上三层找到c } level3(); } level2(); } level1();

查找变量c的过程如下:

  1. 检查level3的环境记录 → 没有c
  2. 通过Outer引用检查level2的环境记录 → 没有c
  3. 通过Outer引用检查level1的环境记录 → 没有c
  4. 通过Outer引用检查全局环境记录 → 找到c = "global c"
  5. 如果一直找到最外层都没找到:undefined

图解:作用域链的树状结构
让我们用可视化方式理解作用域链:

全局词法环境 (Global Lexical Environment) ├─ EnvironmentRecord: { a: "global a", b: "global b", c: "global c" } ├─ Outer: null │ ├─ level1函数词法环境 (调用时创建) │ ├─ EnvironmentRecord: { a: "level1 a", b: "level1 b" } │ ├─ Outer: 引用 → 全局词法环境 │ │ │ ├─ level2函数词法环境 (调用时创建) │ │ ├─ EnvironmentRecord: { a: "level2 a" } │ │ ├─ Outer: 引用 → level1词法环境 │ │ │ │ │ ├─ level3函数词法环境 (调用时创建) │ │ │ ├─ EnvironmentRecord: { a: "level3 a" } │ │ │ ├─ Outer: 引用 → level2词法环境 │ │ │ └─ 变量查找路径:level3 → level2 → level1 → 全局 │ │ └─ │ └─ └─

作用域链的关键特性

  1. 静态性(词法作用域):作用域链在函数定义时就已经确定,而不是在调用时确定的。
  2. 链式结构:像链条一样一环扣一环,从当前作用域指向外层作用域。
  3. 单向性:只能从内层作用域访问外层作用域的变量,不能反向访问。
  4. 与执行上下文相关:每次函数调用都会创建新的执行上下文,但作用域链基于函数定义位置确定。

闭包与作用域链的持久化
闭包的本质就是:函数记住了它被创建时的词法环境

function createCounter() { let count = 0; // 这个变量本该在函数执行后销毁 return function() { count++; // 保持对外部变量的引用,这就是闭包 return count; }; } const counter = createCounter(); // 即使createCounter执行完毕,它的词法环境也不会被销毁 // 因为返回的内部函数仍然引用着它 console.log(counter()); // 1 console.log(counter()); // 2

块级作用域的实现原理
ES5作用域的问题
在ES5中,只有两种作用域:全局作用域和函数作用域。这导致了一些问题:

// ES5的问题:变量提升和缺少块级作用域 function problematic() { console.log(i); // undefined,而不是ReferenceError for (var i = 0; i < 3; i++) { // i在整个函数内都可见 setTimeout(function() { console.log(i); // 全部输出3 }, 100); } console.log(i); // 3,循环结束后的i } problematic();

ES6引入的let/const带来了真正的块级作用域:

// 块级作用域示例 function withBlockScope() { if (true) { // 块级作用域开始 let blockScoped = "只在块内有效"; const constantValue = "常量"; { // 嵌套块级作用域 let nestedBlock = "嵌套块"; console.log(blockScoped); // 可以访问外层块的变量 } // console.log(nestedBlock); // ReferenceError } // console.log(blockScoped); // ReferenceError }

let/const的实现原理:

  1. 在编译阶段,let/const声明的变量被记录在词法环境中
  2. 在变量声明之前访问会抛出错误(暂时性死区)
  3. 每个{}代码块都会创建一个新的词法环境

块级作用域的嵌套结构

// 多层块级作用域 { let a = "外层块 a"; const b = "外层块 b"; { let a = "内层块 a"; // 可以重新声明,因为不同块 console.log(a); // "内层块 a" console.log(b); // "外层块 b" - 可以访问外层 { console.log(a); // "内层块 a" console.log(b); // "外层块 b" } } console.log(a); // "外层块 a" }

其词法环境结构如下:

外层块词法环境: { a: "外层块 a", b: "外层块 b", Outer: 全局 } ↓ 内层块词法环境: { a: "内层块 a", Outer: 外层块词法环境 } ↓ 最内层块词法环境: { Outer: 内层块词法环境 }

暂时性死区(Temporal Dead Zone)

{ // TDZ开始 console.log(myVar); // undefined console.log(myLet); // ReferenceError var myVar = "var变量"; let myLet = "let变量"; // TDZ结束 }

上述实际执行过程(简化):

  1. 进入块级作用域,创建词法环境
  2. var声明被提升,初始值为 undefined
  3. let声明被记录,但未初始化(在TDZ中不可调用)
  4. 在let初始化前访问 → ReferenceError

常见面试题解析
多级嵌套作用域

var x = 10; function foo() { console.log(x); } function bar() { var x = 20; foo(); } bar(); // 输出什么?

上述代码输出结果为:10:

  1. foo函数定义在全局作用域。
  2. 因此foo的词法作用域链:foo作用域 → 全局作用域。
  3. foo在定义时就确定了作用域链,与调用位置无关。
  4. foo中访问x时,在自身作用域没找到,到全局作用域找到x=10

闭包与循环

function createFunctions() { var result = []; for (var i = 0; i < 3; i++) { result[i] = function() { return i; }; } return result; } var funcs = createFunctions(); console.log(funcs[0]()); // 3 console.log(funcs[1]()); // 3 console.log(funcs[2]()); // 3

详细解析过程与解决方案,可以查看这篇文章:JavaScript内存管理揭秘:变量究竟存在哪里

复杂的嵌套作用域

var a = 1; function test() { var a = 2; function innerTest() { var a = 3; return function() { console.log(a); console.log(this.a); }; } var obj = { a: 4, getFunc: innerTest() }; return obj.getFunc; } var func = test(); func();

上述代码的输出结果是:3 1:

  1. funcinnerTest返回的匿名函数
  2. 匿名函数定义在innerTest内部,所以它的词法作用域链:
  • 匿名函数作用域 →innerTest作用域(a=3) →test作用域(a=2) → 全局(a=1)
  1. console.log(a):在自身作用域没找到,到innerTest找到a=3
  2. console.log(this.a)this指向全局window对象,输出全局a=1

思考题
如果JavaScript采用动态作用域而不是词法作用域,会有什么影响?闭包还能工作吗?
结语
JavaScript的词法作用域机制既是其强大之处,也是初学者容易困惑的地方。深入理解这一机制,不仅能帮助你写出更好的代码,还能在面试中游刃有余地解答相关题目。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

原文: https://juejin.cn/post/75999128
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 9:58:20

宇树Java面试被问:RocketMQ事务消息的二阶段提交实现

一、核心概念理解 事务消息解决什么问题&#xff1f; java 复制 下载 // 分布式事务典型问题&#xff1a;本地事务与消息发送的一致性 // 传统方式存在的问题&#xff1a; 1. 先发消息&#xff0c;后执行本地事务 → 消息发送成功但本地事务失败 2. 先执行本地事务&#xff…

作者头像 李华
网站建设 2026/4/23 8:36:16

计算机毕业设计springboot高校线上选课管理系统 高校在线选课系统的设计与实现 基于B/S架构的教务选课服务平台开发

计算机毕业设计springboot高校线上选课管理系统 &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。高等教育规模持续扩大与教学模式数字化转型交织推动&#xff0c;传统线下选课方式…

作者头像 李华
网站建设 2026/4/22 22:41:21

客服接待功能

功能提示&#xff1a; 客服接待可通过PC端和手机移动端PC端登录PC端浏览器输入 &#xff1a; 您的域名/kefu 即可进入客服登录页面。&#xff08;1&#xff09;账号密码登录&#xff08;2&#xff09;扫码登录也可通过系统后台&#xff0c;客服管理 > 客服列表 > 进入工作…

作者头像 李华
网站建设 2026/4/23 8:36:16

西门子200smart模拟量滤波防抖程序:让信号采集更稳更准

西门子200smart模拟量滤波防抖程序&#xff0c;能实现电流电压和热电阻模拟量信号的采集&#xff0c;有滤波&#xff0c;有高位和低位报警&#xff0c;采用for循环指令和间接寻址&#xff0c;让程序简单好用&#xff0c;并且针对程序&#xff0c;录制了视频讲解&#xff0c;详细…

作者头像 李华
网站建设 2026/4/23 8:36:48

GIF裁剪后画面变形?教你3种精准裁剪动图尺寸的方法

制作表情包、编辑聊天动图、发布社交媒体内容时&#xff0c;裁剪GIF动画画面尺寸是高频需求。但很多人遇到这些问题&#xff1a;裁剪后动图播放卡顿、帧序错乱、关键元素被裁掉&#xff0c;或上传后被强制拉伸变形&#xff0c;尤其是制作微信表情时&#xff0c;没按1:1方形比例…

作者头像 李华