教程文档【免费下载链接】you-dont-know-js-ru Russian translation of You Dont Know JS book series项目地址https://gitcode.com/gh_mirrors/yo/you-dont-know-js-ru点击查看免费下载导读本文以开源仓库 you-dont-know-js-ru 中《作用域与闭包》第 1 章为骨架系统讲解 JavaScript 作用域的本质——从编译型语言的视角拆解var a 2的编译与执行两阶段深入剖析引擎Engine、编译器Compiler与作用域Scope三方协作时的 LHS/RHS 两种查找模式并厘清嵌套作用域的逐层回溯规则以及ReferenceError/TypeError的触发机制。读完本文你将能像引擎一样思考准确判断任意一行代码中的变量引用属于 LHS 还是 RHS解释变量提升与隐式全局变量的根源并理解严格模式下行为差异的底层原因。一、引言为什么作用域决定了程序的状态几乎所有编程语言都具备一个最基础的能力把值保存到变量中之后再取出或修改这些值。正是这种存取能力给了程序真正的状态state。没有它程序也能执行一些任务但会非常受限、乏味。但把变量引入程序后立刻浮现出两个核心问题变量活在哪里换句话说它们被存储在哪里程序需要它们时又是如何找到它们的这两个问题指向同一件事必须有一套明确、清晰的规则用来规定变量在何处存放、之后又如何被定位。这套规则就是 JavaScript 中所谓的作用域Scope。更进一步的问题是这些作用域规则究竟由谁、在什么时候确立答案将把我们带进编译理论——这也是第 1 章全部讨论的起点。仓库中本册的完整脉络可参考 《作用域与闭包》目录 与 本册 README。二、编译理论JavaScript 其实是编译型语言一个常见的误解是JavaScript 属于动态或解释型语言因此不需要编译。事实上JavaScript 是一门编译型语言。它确实不像 C/C 那样在构建阶段预先编译编译产物也不可移植、不能分发到其他系统但 JavaScript 引擎在执行代码前会完成与传统编译器几乎相同的一系列步骤——只是方式更复杂、时间更紧。2.1 传统编译的三步走任何一段程序代码在执行之前通常都要经过三个步骤统称编译分词/词法分析Tokenizing/Lexing把字符串拆成对语言有意义的片段——词法单元token。以var a 2;为例通常会被拆成var、a、、2、;五个 token空格是否保留为 token取决于它在语言里是否具有语义。关于 tokenizing 与 lexing 的细微差别两者差别很小且偏理论核心在于 token 是被无状态地识别还是有状态地识别。如果分词器需要用带状态的解析规则来判断a应该是一个独立 token还是某个更长 token 的一部分那么这就是lexing词法分析。解析Parsing把 token 流数组转换为一棵嵌套的元素树整体表达程序的语法结构即ASTAbstract Syntax Tree抽象语法树。var a 2;的 AST 大致是顶层节点VariableDeclaration变量声明其子节点为Identifier标识符值为a和AssignmentExpression赋值表达式后者再包含子节点NumericLiteral数值字面量值为2。代码生成Code Generation把 AST 转换成可执行代码。这一步高度依赖语言、目标平台等。细节可以略过但本质是把上面var a 2;的 AST 变成一组机器指令真正创建名为a的变量包括内存分配等再把值2存入a。注引擎如何管理系统资源远超本章讨论范围这里只需要相信引擎能够在必要时创建并保存变量。2.2 JavaScript 编译的特殊性真实的 JavaScript 引擎远比这三步复杂解析与代码生成过程中还包含大量性能优化如消除冗余。但有一个关键约束是其他语言编译器不必面对的JavaScript 没有在构建阶段预编译的机会编译往往发生在代码执行前几微秒甚至更短之内。为了在如此紧迫的时间内保证高性能JS 引擎用上了各种技巧例如JITJust-In-Time即时编译——惰性编译、甚至在运行时重新编译。这些细节超出本章讨论范围但结论很明确任何一段 JavaScript 代码都必须在执行前通常就是紧邻执行前完成编译然后立即执行。为什么要在高层视角强调这些编译细节因为接下来的整章内容——引擎如何询问作用域、如何做 LHS/RHS 查找——都建立在这个编译模型之上。这正是本章 1 节与仓库中后续章节的衔接点第 2 章 词法作用域 将说明作用域何时定型第 4 章 变量提升Hoisting 将展示编译期声明与运行期赋值的分离带来的经典现象。三、理解作用域三方角色的对话第 1 章用一个绝妙的视角来理解作用域把程序的编译与执行看作一场对话。那对话的主角是谁3.1 三个角色处理var a 2;时真正登场的是三个角色角色职责引擎Engine负责 JavaScript 程序的编译从头到尾与执行编译器Compiler引擎的朋友负责解析与代码生成的全部脏活作用域Scope引擎的另一位朋友收集并维护所有已声明标识符变量的查找列表严格执行一套规则规定当前执行中的代码能以何种方式访问这些标识符要真正理解 JavaScript就需要学会像引擎以及它的朋友们那样思考提出它们会提出的问题并用同样的方式回答。3.2 往返拆解var a 2;其实是两个操作看到var a 2;我们通常认为这是一个语句。但引擎并不这么看——它看到的是两个独立的操作编译期操作由编译器处理运行期操作由引擎处理。假设编译器生成的代码可概括为为变量分配内存、标记为a、再把2放进去——这个直觉并不准确。真实的流程是编译器遇到var a会先请作用域检查当前作用域集合中是否已存在变量a若已存在编译器忽略这个声明继续往后走若不存在编译器请作用域声明一个新变量a加入当前作用域集合。然后编译器为引擎生成后续要执行的代码用来处理赋值a 2。引擎运行这段代码时会先问作用域当前作用域集合里有没有一个可访问的a有则直接使用没有引擎就去别处找详见下文嵌套作用域。若引擎最终找到了变量就给它赋值为2若找不到引擎只能摊手喊错误。小结给变量赋值涉及两个截然不同的动作——其一编译器在尚未声明时于当前作用域中声明变量其二执行时代码运行引擎经作用域查找变量找到后完成赋值。3.3 编译器的话LHS 与 RHS 查找执行编译器在步骤 2 生成的代码时引擎必须先找到变量a才能确认它是否已声明而这个查找行为要借助作用域。关键在于查找的类型会影响查找的结果。这里有两种查找LHSLeft-hand Side左侧查找变量出现在赋值操作的左侧时触发——查找的是变量的容器本身目的是给它赋一个新值RHSRight-hand Side右侧查找变量出现在赋值操作的右侧时触发——就本文的讨论而言RHS 查找与简单地取某个变量的值无异即去把那个值取来给我。更准确地说RHS 并不必然字面指赋值等号右侧它更精确的含义是不在左侧也可以俏皮地记作Retrieve His/Her Source value取回其值。看两个对照例子console.log( a ); // RHS 引用此处没有给 a 赋值只是取出 a 的值传给 console.log(..)a 2; // LHS 引用不关心 a 当前的值只想把 a 作为 2 的赋值目标注LHS/RHS 的左/右并非字面上的等号左右位置。赋值还有其他产生方式如传参因此更贴切的概念是谁是赋值的目标LHS与谁是赋值的来源RHS。一个同时包含 LHS 与 RHS 的程序function foo(a) { console.log( a ); // 2 } foo( 2 );逐行拆解最后一行foo( 2 )调用foo(..)需要一次RHS 查找foo——找到foo的值给我(..)表示这个值要被执行所以它多半是函数。存在一个隐蔽但重要的赋值值2作为实参传入foo(..)时会被隐式赋值给形参a即a 2。为完成这次隐式赋值需要执行一次LHS 查找a。函数体内console.log( a )对a是一次RHS 引用取出的值再传入console.log(..)。调用console.log(..)需要 RHS 查找console对象随后做属性名解析确认存在名为log的方法。最后值2经 RHS 查找变量a后作为实参传给log(..)在log(..)的原生实现内部可以合理推断它也有形参比如arg1在把2赋给arg1前会做一次 LHS 查找。关于函数声明的一个重要澄清你可能想把function foo(a) {...}想象成var foo加foo function(a){...}的合体从而推断函数声明隐含 LHS 查找。这是不恰当的编译器在代码生成阶段同时处理了声明与值定义因此引擎执行时根本不需要任何把函数值赋给 foo的处理。函数声明不应被看作本章讨论意义上的 LHS 赋值。3.4 引擎与作用域的对话实录还是这段程序function foo(a) { console.log( a ); // 2 } foo( 2 );把它想象成引擎Движок与作用域Область видимости之间的一问一答引擎嘿作用域我要对foo做 RHS 查找听说过它吗作用域当然编译器一秒前刚声明了它。它是函数拿去吧引擎太好了谢谢好我开始执行foo。引擎嘿作用域我要对a做 LHS 查找听说过吗作用域当然编译器刚把a声明为foo的形参拿去吧引擎作用域你一如既往地给力再次感谢。现在把2赋给a。引擎嘿作用域抱歉又来打扰。我需要 RHS 查找console听说过这个名字吗作用域没问题引擎这正是我整天干的事。是的我有console它是内置对象拿去吧引擎完美。再找log(..)……很好是函数。引擎嘿作用域能帮我 RHS 查找a吗我记着它但想再确认一下。作用域没错引擎还是同一个引用没变。拿去吧引擎酷把a的值等于2传给log(..)。……这场对话的完整形态就是每次变量读写发生时引擎的真实工作方式。3.5 自测扮演引擎在继续之前先验证理解。请扮演引擎与作用域对话处理下面这段代码function foo(a) { var b a; return a b; } var c foo( 2 );找出全部 3 个 LHS 查找找出全部 4 个 RHS 查找。答案见文末本章小结与自测答案一节。四、嵌套作用域从当前楼层逐级向上回溯前面说过作用域是一套按标识符查找变量的规则集合。但现实是程序中通常存在不止一个作用域。正如块/函数可以嵌套在别的块/函数内部作用域也可以嵌套在其他作用域内部。因此如果变量在最近的当前作用域中找不到引擎就会去下一层更外层的作用域继续找如此反复直到找到或者抵达最外层——即全局作用域。示例function foo(a) { console.log( a b ); } var b 2; foo( 2 ); // 4对b的 RHS 查找无法在foo内部完成但在包裹foo的外层作用域本例即全局作用域中可以解析成功。于是引擎与作用域的对话变成引擎嘿foo的作用域听说过b吗我要对它做 RHS 查找。作用域没听说过。去别处问问吧引擎嘿foo外面的作用域哦你就是全局作用域啊真棒。听说过b吗我要对它做 RHS 查找。作用域有的有的当然有。拿去吧嵌套作用域的查找规则非常简单引擎从当前正在执行的作用域开始查找找不到就向上一层再找不到继续向上一层一层直到全局作用域一旦抵达最外层全局无论找到与否查找都必须停止。4.1 高楼隐喻把作用域链可视化为直观呈现嵌套作用域的解析过程第 1 章用一个高楼隐喻楼中的每一层代表一层作用域一楼是你当前正在执行的作用域顶层则是全局作用域。解析 LHS/RHS 引用时你从当前楼层开始找找不到就坐电梯上一层继续找再找不到再上一层……一旦到达顶层全局作用域要么找到目标要么找不到——但无论如何都必须停下来。需要说明的是这里的嵌套是精确、无歧义的包含关系不是可以互相跨越边界的文氏图。任何函数作用域都不可能部分地同时存在于两个外层作用域中——每个函数都完整地、唯一地属于其书写位置所对应的那个外层作用域。作用域的楼层结构完全由代码书写的位置决定这正是第 2 章 词法作用域 展开的主题作用域在词法分析阶段就已被写死。作为对照附录 A 动态作用域 介绍了另一种非 JavaScript 采用的模型。五、错误为什么必须区分 LHS 与 RHS把查找区分为 LHS 和 RHS 到底有什么意义因为在变量从未被声明在所有逐层查找的作用域中都没找到时这两种查找的行为截然不同。来看这个例子function foo(a) { console.log( a b ); b a; } foo( 2 );这里b第一次出现是 RHS 查找——它不会在任何作用域中被找到属于未声明变量。RHS 查找失败 →ReferenceError如果 RHS 查找在整条嵌套作用域链中都找不到变量引擎会抛出ReferenceError。请特别注意这种错误的类型一定是ReferenceError。LHS 查找失败 → 分两种情况非严格模式lazy/宽松模式若引擎做 LHS 查找一路查到顶层全局作用域仍一无所获且程序未运行在严格模式下则全局作用域会自动创建一个同名的新变量就创建在全局作用域里并把它交还给引擎——以前没有我很乐意为你现造一个。严格模式ES5 引入的严格模式use strict与普通/非严格模式有诸多差异其中之一就是禁止自动/隐式创建全局变量。此时全局作用域没有可交还的变量引擎会抛出ReferenceError与 RHS 失败的情形相同。另外还有一种错误形态RHS 查找成功找到了变量但你试图用它的值做不可能的操作——比如把非函数值当函数执行或对null/undefined的值访问属性。此时引擎抛出的错误类型是TypeError。两者的本质区别一句话概括ReferenceError是作用域层面的名字解析失败根本没找到这个标识符而TypeError意味着作用域解析已经成功名字找到了但随后对解析结果执行了非法/不可能的操作。严格模式的更多细节可以查阅仓库内其他相关章节本册中 第 4 章提升Hoisting 会对编译期声明这一行为做更深入的展开。六、本章小结与自测答案6.1 小结第 1 章核心结论速览作用域是一套规则决定变量标识符在哪里、以何种方式被找到。查找的目的可能是赋值LHS 引用也可能是取值RHS 引用。LHS 引用来自赋值操作。与作用域相关的赋值既可以通过运算符产生也可以通过**实参向形参的传递赋值**产生。JavaScript 引擎先编译、后执行。编译期间引擎会把var a 2;拆成两个独立步骤先处理var a在作用域中声明变量——这发生在执行之前的最早阶段稍后执行a 2对a做 LHS 查找找到就赋值找不到就报错。LHS 与 RHS 查找都从当前执行作用域开始需要时逐层向外一层一个楼层查找标识符直到抵达全局作用域顶层并停止——无论最终找到与否。失败的 RHS 引用 →ReferenceError失败的 LHS 引用 → 非严格模式下自动隐式创建同名全局变量严格模式下抛ReferenceError。6.2 自测答案function foo(a) { var b a; return a b; } var c foo( 2 );3 个 LHS 查找c ..把foo(2)的返回值赋给ca 2实参2隐式赋值给形参ab ..把a的值赋给b。4 个 RHS 查找foo(2..找foo的值以便调用 a;取a的值赋给ba ..return表达式中取a的值.. breturn表达式中取b的值。6.3 下一步阅读本章建立的作用域模型是整册书的地基。沿着本册 目录 继续深入第 2 章词法作用域 —— 作用域何时被定型、哪些机制可以欺骗词法作用域第 3 章函数作用域与块作用域 —— 作用域的具体单元是什么第 4 章变量提升Hoisting —— 编译期声明与运行期赋值分离的直接后果第 5 章闭包 —— 本章 LHS/RHS 模型的最终归宿附录 A动态作用域 —— 与词法作用域相对照的另一种模型附录 C词法 this ——this绑定与作用域的关系。掌握编译 → 声明 → 查找 → 赋值/取值 → 报错这条完整链路你就拥有了理解 JavaScript 一切变量行为提升、隐式全局、闭包、模块的统一心智模型。赞分享教程文档【免费下载链接】you-dont-know-js-ru Russian translation of You Dont Know JS book series项目地址https://gitcode.com/gh_mirrors/yo/you-dont-know-js-ru点击查看免费下载相关推荐gh_mirrors/yo/you-dont-know-js-ru作用域链JavaScript变量查找机制详解gh_mirrors/yo/you dont know js ru作用域链JavaScript变量查找机制详解 你是否曾在调试JavaScript代码时遇到教程文档浏览器内ADB调试Tango如何革新Android开发体验浏览器内ADB调试Tango如何革新Android开发体验 你是否曾为Android开发调试的繁琐环境配置而烦恼传统ADB客户端需要复杂的本地环境设置限制开发工具通信移动开发CLI深度解析music-api构建跨平台音乐聚合服务的核心技术指南深度解析music api构建跨平台音乐聚合服务的核心技术指南 music api作为一个开源的PHP音乐解析工具为开发者提供了统一接入网易云音乐、QQ音乐教程文档上一篇如何在3步内下载B站大会员4K视频Python下载器全解析下一篇终极指南3步实现GitHub下载速度提升10倍的免费加速方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考