:块级作用域、闭包与 ReferenceError)
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载导读本篇文章围绕 JavaScript 教程仓库en.javascript.info中「闭包」章节1-js/06-advanced-functions/03-closure的经典思考题「Function in if」展开剖析一个看似简单、实则牵动块级作用域、函数声明初始化时机与闭包三大概念的场景在if块内部声明的函数为什么无法在块外调用。读完本文你将能够准确判断这类代码的报错原因理解函数声明与let/const变量在块级词法环境中的存活范围并能写出规避此类错误的正确写法。一、题目原文if内的函数能否在外部调用该思考题位于 task.md重要性标记为importance: 5最高级别题目代码如下let phrase Hello; if (true) { let user John; function sayHi() { alert(${phrase}, ${user}); } } sayHi(); // 问题很直接最后一行调用sayHi()的结果是什么题目给出了几个值得注意的细节sayHi是一个函数声明Function Declaration而不是函数表达式它声明在if (true) { ... }代码块内部它访问了两个外部变量块外部的phrase和块内部的user调用发生在if块结束之后、代码块的「外面」。在动手运行之前先在心里给出你的答案再继续往下读。二、标准答案会抛出错误ReferenceError仓库中的官方解答位于 solution.md原文结论非常明确The result isan error. The functionsayHiis declared inside theif, so it only lives inside it. There is nosayHioutside.即结果是错误。sayHi声明在if块内部因此它只在if块内存在块外不存在sayHi这个标识符所以最后一行的调用会抛出ReferenceError: sayHi is not defined。更精确地说alert里拼接phrase与user的逻辑根本不会有机会执行——在解析sayHi标识符的阶段就已经失败了。三、为什么报错函数声明同样受代码块约束要理解这个结果需要回到闭包章节 article.md 中反复强调的一条基本规则如果一个变量在代码块{...}内声明那么它只在那个块内部可见。该章节给出了同构的let示例{ // 用局部变量完成某个任务它们不应被外部看到 let message Hello; // 仅在本块内可见 alert(message); // Hello } alert(message); // Error: message is not defined以及针对if的示例if (true) { let phrase Hello!; alert(phrase); // Hello! } alert(phrase); // Error, no such variable!函数声明本质上也是一种「绑定」。虽然 JavaScript 中的函数是值、可以像变量一样被传递和复制但它声明在哪个词法环境Lexical Environment中就归属于哪个环境。本题目中的sayHi声明在if代码块的词法环境中块结束时该词法环境随之失效sayHi自然也就不可见了。这与let user只能活在块内的道理完全一致——题目中sayHi访问的user和sayHi自身都被限制在了同一个块内。四、从 Lexical Environment 看函数声明的「即时初始化」闭包章节 article.md 对函数声明和let变量在词法环境中的初始化方式作了明确的区分let变量在进入作用域时处于Uninitialized未初始化状态直到执行到let声明行之前都无法引用即所谓的暂时性死区 TDZ而函数声明在词法环境创建时就被立即完整初始化成为一个立即可用的函数。// 函数声明可以在声明之前调用这是允许的 sayHi(); // 正常工作 function sayHi() { alert(Hello); }这个「即时初始化」特性让函数声明在其所属的作用域内部拥有提升hoisting能力。但请注意提升的边界提升只发生在声明所在的词法环境内部。在本题中进入if (true) { ... }时引擎为该块创建了一个新的词法环境在这个块环境的 Environment Record 中sayHi函数声明被立即初始化user变量被记录初始为 uninitialized因此在块内部即使在sayHi声明之前调用它也是合法的但块一旦执行完毕这个块级词法环境连同其中的sayHi、user一起退出可见范围外部全局词法环境中从未登记过sayHi于是块外的调用自然失败。图示化地理解调用sayHi()时代码先查找当前全局词法环境再沿 outer 引用逐级向外查找但sayHi只存在于已经「关闭」的if块词法环境中查找路径上根本找不到它。补充说明关于严格模式的历史兼容细节教程给出的规则——块内函数声明只活在块内——对应的是现代严格模式use strict、ES Module、class 内部下 ECMAScript 的确定行为。在旧式的非严格浏览器环境中曾存在 Annex B 的历史兼容处理某些引擎会让块级函数声明同时产生外部可见的绑定这是为兼容古老网页而保留的特例不应作为新代码的依赖。日常开发与面试场景中请以「块内声明、块内使用」这条现代规则为准。五、闭包视角sayHi记住了什么本题的第二层考点是闭包。闭包章节 article.md 指出在 JavaScript 中所有函数天然就是闭包——它们通过隐藏属性[[Environment]]记住自己创建时的词法环境从而无论在哪里被调用都能访问创建时环境中的外层变量。题目中的sayHi是一个典型的闭包它通过[[Environment]]引用着创建它的词法环境在该环境中它既能沿 outer 链访问全局的phrase值为Hello也能访问块内的user值为John如果在块内调用sayHi()会正确弹出Hello, John。但闭包的前提是「函数本身还触达得到」。闭包只保证函数记住的环境不被垃圾回收、变量值可被读取它并不改变函数绑定的可见范围。sayHi的绑定与函数体是两个层面绑定名字sayHi属于块级词法环境块外无法通过这个名字找到函数即便函数体作为一个值理论上「还存在」外部代码也没有拿到指向它的引用。因此闭包能力再强也救不了块外的sayHi()调用——先有引用才有闭包。这正是本题被放在「变量作用域、闭包」章节的原因它同时考察了块级作用域边界与闭包对环境的记忆这两个正交概念。六、对比与修正怎样写才能让函数在块外可用6.1 与var的对比var没有块级作用域同样是声明在if块内用var与用let/函数声明的命运截然不同。04-var/article.md 专门讲解了var的差异if (true) { var test true; // 用 var 而不是 let } alert(test); // true变量在 if 之后仍然存活而改用let后if (true) { let test true; } alert(test); // ReferenceError: test is not defined结论是var没有块级作用域它要么是函数级、要么是全局级会「穿透」if、for等代码块。但这是历史遗留行为早期 JavaScript 中代码块没有词法环境现代代码应使用let、const与块级函数声明即「块内声明的东西归块内所有」。function sayHi在块内的行为与let user一致——严格块级化。6.2 修正方案一把函数声明移到块外最直接的修法是把函数声明提升到if之外让sayHi的绑定进入外层此处为全局词法环境let phrase Hello; let user John; // 需要让 user 也在外层可见 function sayHi() { alert(${phrase}, ${user}); } if (true) { // 其他仅限块内的逻辑…… } sayHi(); // 正常弹出 Hello, John6.3 修正方案二用函数表达式赋值给外层变量如果确实希望函数的定义过程发生在块内例如依赖块内计算的某些值来构造函数可以借助函数表达式把函数值赋给一个外层声明的变量。函数表达式不会像函数声明那样被绑定到块级环境值本身可以自由跨越块边界let phrase Hello; let sayHi; // 在外层声明变量 if (true) { let user John; sayHi function() { // 函数表达式赋值给外层变量 alert(${phrase}, ${user}); }; // 注意sayHi 作为闭包仍能记住块内的 user } sayHi(); // Hello, John —— 可以调用这里的sayHi变量在全局词法环境中所以块外可以访问同时sayHi函数体依旧通过[[Environment]]闭包捕获了块内词法环境中的user因此弹出的结果是Hello, John与题目设想的行为完全一致。这正是「函数作为值可被到处传递、但闭包环境不变」这一特性参见 02-first-steps/16-function-expressions/article.md 中函数表达式的讲解的实战体现。6.4 何时在块内声明函数是合理的块内函数声明本身并没有错关键在于使用位置必须与声明位置同处一个块。例如只在if分支内部使用的辅助函数放在块内反而是更好的封装if (condition) { function formatPrice(price) { return $${price.toFixed(2)}; } // 本分支内可以正常使用 formatPrice console.log(formatPrice(19.99)); } // 离开 if 后 formatPrice 不可见——这恰好是期望的封装效果这样既利用了函数声明的即时初始化与提升又把辅助函数的作用域限制在需要的分支内避免污染外层命名空间。七、总结三条可复用的判断法则回到「Function in if」这道题可以把结论浓缩为三条通用法则它们同样适用于for、while、裸{...}等所有代码块绑定归属法则在{...}内用let、const声明的变量以及用函数声明创建的函数绑定都属于该块对应的词法环境块外不可见var是例外无块级作用域。查找法则访问一个标识符时引擎从当前词法环境开始逐级向外查找详见 closure/article.md 的词法环境查找机制找不到即抛ReferenceError。块外的sayHi()之所以报错正是因为查找路径上不存在该绑定。闭包前置法则闭包让函数记住创建环境中的外层变量但前提是外部代码能拿到对函数本身的引用要让块内定义的函数在块外可用要么把声明移到块外要么用函数表达式把值赋给外层变量。理解这一题等于同时打通了块级作用域、函数声明初始化与闭包三条主线后续再遇到makeCounter、循环闭包陷阱等更复杂的场景时这套分析框架可以直接复用。参考资料仓库内相对路径题目原文1-js/06-advanced-functions/03-closure/5-function-in-if/task.md官方解答1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md闭包与词法环境章节1-js/06-advanced-functions/03-closure/article.mdvar与块级作用域对比1-js/06-advanced-functions/04-var/article.md函数声明与函数表达式1-js/02-first-steps/16-function-expressions/article.md赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐ServerBox 代码生成全指南Freezed、json_serializable、Riverpod、Hive 与 Rust 绑定的工程化实践ServerBox 代码生成全指南Freezed、json_serializable、Riverpod、Hive 与 Rust 绑定的工程化实践 Server文档/教程前端Go 夜读 · 深入解析 if-else 条件表达式中的变量声明作用域Go 夜读 · 深入解析 if else 条件表达式中的变量声明作用域 本文源自 Go 夜读社区 content/discuss/2018 05 13 decl文档教程you-dont-know-js-ruJavaScript 的领域作用域——函数与块《你并不了解 JS作用域与闭包》第 3 章深度解析you dont know js ruJavaScript 的领域作用域——函数与块《你并不了解 JS作用域与闭包》第 3 章深度解析 导读 本文围绕《教程文档上一篇终极指南Symfony Finder组件AccessDeniedException防御策略下一篇PySpur团队协作指南5个高效版本控制最佳实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考