ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript 函数为什么能在声明前被调用?提升机制与 var、let 行为差异详解

JavaScript 函数为什么能在声明前被调用?提升机制与 var、let 行为差异详解 JavaScript 函数为什么能在声明前被调用提升机制与 var、let 行为差异详解【免费下载链接】33-js-concepts 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts这篇文章解决一个具体的疑惑为什么function声明的函数可以在代码中定义之前就调用而var变量提前访问得到undefinedlet变量提前访问却直接抛ReferenceError以 33-js-concepts 项目中的 Hoisting 指南 为主线结合配套的 Vitest 测试文件 逐条核对这些行为最后给出项目测试套件可以验证的运行路径让你不只是看懂解释而是能亲手跑出每一种结果。文档中给出的前置知识是 作用域与闭包 和 调用栈Hoisting 指南 开头明确标注了这两项先修内容。如果你对作用域还生疏先读这两篇再回来。准备条件需要 Node.js 环境和 npm。项目通过 package.json 声明了vitest、vitest/coverage-v8、jsdom三个开发依赖需要先安装npm installCONTRIBUTING.md 说明项目使用 Vitest 作为测试运行器to verify that code examples in the documentation work correctly即测试的作用就是核对文档中的代码示例行为是否属实。本文后续所有文档说某段代码输出 X的结论都可以用测试套件复核。先跑一遍基线确认环境与文档行为一致在安装完依赖后执行项目脚本均定义在 package.json 中npm test这会把tests/下所有用例跑一遍。其中 hoisting.test.js 覆盖了本文涉及的每一种行为var提升后读到undefined、let/const在 TDZ 中抛ReferenceError、函数声明完整提升、var函数表达式调用抛TypeError等。所有用例通过说明你当前的运行环境与文档示例描述的行为一致后面的代码示例可以直接采信。如果需要边改边看可以用npm run test:watchwatch 模式文件变化时自动重跑。注意一点测试在 Node.js 中运行见 CONTRIBUTING.md且 Vitest 以 strict mode 运行因此测试中无法在同一作用域直接写先访问、后声明的let/const代码同作用域重声明本身就会报错测试文件用eval包裹这类片段例如it(should throw ReferenceError when accessing let before declaration, () { expect(() { eval( const before x let x 10 ) }).toThrow(ReferenceError) })为什么函数声明能在定义前被调用两阶段执行Hoisting 指南 给出的核心答案是JavaScript 引擎处理代码分两个阶段。编译阶段引擎扫描代码把所有声明注册进内存。变量会被创建但不赋值函数除外函数会被完整创建。执行阶段引擎逐行执行此时才把值赋给变量。所以提升并不是引擎真的把你的代码搬来搬去而是描述声明何时被引擎知晓的概念模型——指南特别指出ECMAScript 规范中根本没有 hoisting 这个词它只是对可观察行为的命名。文档中的原始示例输出均为文档给出的结果// This works - but how? sayHello() // Hello! function sayHello() { console.log(Hello!) }用文档里的比喻说编译阶段相当于搬家前每个房间已经贴好了标签、预留了位置执行阶段才是家具进场。变量名在编译阶段就已可寻址值要等执行到赋值语句。指南还解释了引擎为什么要这么设计第一条理由是相互递归函数声明可以被随意排序地互相调用不需要像 C 那样写前向声明function isEven(n) { if (n 0) return true return isOdd(n - 1) // Can call isOdd before its defined } function isOdd(n) { if (n 0) return false return isEven(n - 1) // Can call isEven } console.log(isEven(4)) // true console.log(isOdd(3)) // true四种声明的提升行为对照Hoisting 指南 用一张表总结了各类声明的差异下表按该表整理声明类型被提升被初始化声明前可访问var是是undefined是返回undefinedlet/const是否TDZ否ReferenceError函数声明是是完整函数是完全可用函数表达式取决于var/let/const否否class是否TDZ否ReferenceError下面逐条展开每一段的输出都以文档示例为准。var提升到函数作用域并初始化为 undefinedvar声明会被提升到所在函数或全局作用域顶部并自动初始化为undefined。这就是为什么提前访问不会报错而是拿到undefinedconsole.log(greeting) // undefined (not an error!) var greeting Hello console.log(greeting) // Hello用文档的说法引擎在编译期把上面这段看成var greeting // 提升初始化为 undefined console.log(greeting) // undefined greeting Hello // 赋值留在原处 console.log(greeting) // Hello关键规则是提升的是声明赋值留在原地。还要注意var是函数作用域会忽略代码块。message写在if块里但被提升到整个函数function example() { console.log(message) // undefined if (true) { var message Hello } console.log(message) // Hello } example()这个行为在测试中有对应用例块内var message声明后块外仍能访问。let / const确实被提升了但落在时间死区里这是最容易搞错的点。文档明确纠正了一个常见误解许多教程说let和const没有被提升这是不正确的。它们被提升了只是在声明语句被执行之前处于未初始化状态这个区间叫时间死区Temporal Dead ZoneTDZ。TDZ 从进入作用域/块开始到声明语句被求值时结束期间任何访问都抛ReferenceError// TDZ starts at the beginning of the block console.log(name) // ReferenceError: Cannot access name before initialization let name Alice // TDZ ends here如何证明内层let真的被提升了看这个文档示例let x outer { console.log(x) // ReferenceError: Cannot access x before initialization let x inner }如果内层的let x没有被提升这里应该输出外层的outer但实际抛出ReferenceError说明内层x从块开头就遮蔽shadow了外层x且它本身还在 TDZ 中。这也是 TDZ 专题文档 给出的 TDZ 设计动机没有 TDZ 时内层变量未初始化前会悄悄读到外层值产生难以发现的 bug有了 TDZ引擎会立刻告诉你你在用还没就绪的变量。测试文件里的同名用例用outer变量加eval块验证了这一点断言抛出的正是ReferenceError。另一个容易忽略的点TDZ 按执行时间算不是按代码位置算。在 TDZ 内定义引用x的函数没有问题只有调用时真正访问x才会触发错误见 TDZ 文档{ const getX () x // OK: 只是定义还没访问 getX() // ReferenceError! 此时调用才算访问 let x 42 getX() // 42 - 现在可以了 }函数声明 vs 函数表达式同一种名字两种结果函数为什么能提前调用的答案只对函数声明成立。函数声明的名字和函数体都被完整提升而函数表达式、箭头函数本质上是给变量赋一个函数值只提升变量声明本身于是走的是var/let/const各自的规则。文档给出的三组对照输出为文档示例// 函数声明完全提升可直接调用 greet() // Hello! function greet() { console.log(Hello!) }// var 函数表达式变量提升到 undefined调用 undefined() greet() // TypeError: greet is not a function var greet function() { console.log(Hello!) }// const 函数表达式变量还在 TDZ greet() // ReferenceError: Cannot access greet before initialization const greet function() { console.log(Hello!) }箭头函数永远是表达式规则与const版本相同sayHi() // ReferenceError: Cannot access sayHi before initialization const sayHi () { console.log(Hi!) }文档给出的速记规则function关键字作为独立语句出现不是表达式的一部分时完整提升一旦赋值给变量就只提升变量声明。class的行为与let/const一致声明被提升但落入 TDZ声明前new会抛ReferenceErrorconst dog new Animal(Buddy) // ReferenceError: Cannot access Animal before initialization class Animal { constructor(name) { this.name name } }同名冲突函数声明先赢var 赋值后追变量名和函数名重名时有固定的优先级顺序。文档示例console.log(typeof myName) // function var myName Alice function myName() { return Im a function! } console.log(typeof myName) // string过程是var myName和function myName都被提升函数声明在变量声明之后处理因此先覆盖了var留下的undefined执行到var myName Alice时它退化为普通赋值把名字重新指向字符串。注意这里两个typeof结果不同说明函数声明的赢只发生在提升阶段后续var赋值随时可以改写。另外两个相关行为多个var声明同名变量会被合并成一个var x 1; var x 2; var x 3等价于声明一次、赋值三次最终x为3且var允许重复声明不报错。let和const不允许重声明let x 1; let x 2会抛SyntaxError: Identifier x has already been declared。最典型的陷阱var 函数表达式文档把var 函数表达式称为第一大提升陷阱// What does this print? console.log(sum(2, 3)) var sum function(a, b) { return a b }结果是TypeError: sum is not a function——调用发生时sum还是提升带来的undefined等于执行undefined(2, 3)。文档给出的两种修法// 修法一改成函数声明 console.log(sum(2, 3)) // 5 function sum(a, b) { return a b }// 修法二先定义后使用 const sum function(a, b) { return a b } console.log(sum(2, 3)) // 5测试文件中有一个函数表达式陷阱用例断言捕获到的错误名就是TypeError与文档结论一致。var 循环 闭包let 的另一处差异var的函数作用域还会在循环闭包中制造经典问题。项目测试hoisting.test.js var loop problem with closures 一节中的对照示例const funcs [] for (var i 0; i 3; i) { funcs.push(function() { return i }) } funcs[0]() // 3 funcs[1]() // 3 funcs[2]() // 3三个闭包共享同一个提升出来的i循环结束后它已是3。把var换成let每次迭代各自持有独立的i三个返回值分别是0、1、2。这是let块级作用域带来的实际收益也是文档建议避免var的具体场景之一。文档给出的写法建议Hoisting 指南 的 Best Practices 一节给出了五条建议都是围绕不要让读者靠提升机制脑补代码把变量声明放在作用域顶部——即使提升会兜底显式放顶部更清晰。优先级constletvar——默认const需要重新赋值用let避免varconst/let的作用域更可预测TDZ 还能提前暴露 bug。具名函数用函数声明——完整提升、意图明确箭头函数则先定义后使用。import 保持在文件顶部——import声明会被提升到模块最顶被导入模块的代码先于当前模块执行但为了可读性仍应放在顶部多数 linter 也会强制这一点。不要依赖提升来获取变量值——console.log(x); var x 5能跑输出undefined但令人困惑改用const x 5放在使用之前。限制与边界最后明确文档给出的几条边界避免把提升机制理解过头提升是概念模型而非引擎行为描述ECMAScript 规范不使用该术语规范层面说的是声明在环境记录创建/编译阶段被实例化。var忽略块级作用域只提升到函数或全局级这是它和let/const行为分叉的根源。let/const的 TDZ 只影响访问在 TDZ 内定义引用该变量的函数是合法的调用时才会检查。var允许重复声明let/const重声明直接SyntaxError。对照 hoisting.test.js 再跑一次npm test以上每一条行为都有对应的断言全部用例通过即表示文档描述的 var/let/const/函数/class 提升行为在你的环境中全部成立。若某个用例失败优先检查运行的是否为 Node.js 环境测试不含 DOM 用例也不依赖浏览器。【免费下载链接】33-js-concepts 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表