尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript 变量提升(Hoisting)完全指南:声明与定义、函数提升与作用域组织实战

JavaScript 变量提升(Hoisting)完全指南:声明与定义、函数提升与作用域组织实战 教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载导读变量提升Hoisting是 JavaScript 中最容易被误解、也最容易引发隐蔽 Bug 的机制之一。本文基于 jstips 仓库第 11 号技巧中文原版展开系统讲解var声明提升与函数声明提升的完整规则并通过可运行的代码示例对比声明与定义的本质区别同时结合仓库内 var/let 对比、暂时性死区TDZ等相关技巧帮你建立从知道会提升到写出不受提升困扰的作用域组织代码的完整实战能力。一、核心概念声明Declaration与定义Definition的区别理解变量提升的第一步是分清两个被频繁混用的术语声明Declaration让系统引擎知道存在这样一个变量变量名进入当前作用域。定义Definition给已声明的变量赋值让变量拥有实际的值。关键规律在于变量声明和函数定义都会被提升到作用域顶部但变量定义赋值不会被提升——即使你在同一行同时声明并赋值一个变量也只有声明部分会被提升。用一个最简单的例子说明console.log(a); // 输出undefined var a 1; // 声明被提升赋值不提升引擎实际执行的效果等价于var a; // 提升声明到顶部此时 a 为 undefined console.log(a); // 输出undefined a 1; // 定义赋值留在原位这就是为什么在声明之前访问var变量得到的不是ReferenceError而是undefined。二、原版示例逐行拆解一次看清提升的全部行为下面这段来自原文档的doTheThing函数集中展示了变量提升与函数提升的四种典型表现。我们将逐行分析每一条console.log的输出及其背后的机制function doTheThing() { // ReferenceError: notDeclared is not defined console.log(notDeclared); // Outputs: undefined console.log(definedLater); var definedLater; definedLater I am defined! // Outputs: I am defined! console.log(definedLater) // Outputs: undefined console.log(definedSimulateneously); var definedSimulateneously I am defined! // Outputs: I am defined! console.log(definedSimulateneously) // Outputs: I did it! doSomethingElse(); function doSomethingElse(){ console.log(I did it!); } // TypeError: undefined is not a function functionVar(); var functionVar function(){ console.log(I did it!); } }逐行解读console.log(notDeclared)notDeclared从未用var声明过作用域中不存在该变量直接抛出ReferenceError: notDeclared is not defined。console.log(definedLater)var definedLater;的声明被提升到函数顶部此时变量存在但尚未赋值因此输出undefined。随后执行definedLater I am defined!完成定义再次输出即为字符串I am defined!。console.log(definedSimulateneously)var definedSimulateneously I am defined!虽然声明与赋值在同一行但提升的只有声明部分赋值停留在原行所以函数顶部访问到的仍是undefined。doSomethingElse()函数声明function doSomethingElse(){}整体被提升到作用域顶部因此即使在定义之前调用也能正常输出I did it!——这是函数声明与变量声明最关键的区别。functionVar()var functionVar function(){...}是变量声明 函数表达式赋值的组合。提升的只有var functionVar;值为undefined调用functionVar()时实际是在调用undefined于是抛出TypeError: undefined is not a function。引擎视角提升后的代码等价形态为便于理解上面的函数在执行时等价于提升操作发生在编译阶段而非运行时function doTheThing() { // —— 提升部分编译期完成—— var definedLater; // 声明提升值为 undefined var definedSimulateneously; // 声明提升值为 undefined var functionVar; // 声明提升值为 undefined function doSomethingElse(){ // 函数声明整体提升 console.log(I did it!); } // —— 原有代码按顺序执行 —— console.log(notDeclared); // ReferenceError console.log(definedLater); // undefined definedLater I am defined!; console.log(definedLater); // I am defined! console.log(definedSimulateneously); // undefined definedSimulateneously I am defined!; console.log(definedSimulateneously); // I am defined! doSomethingElse(); // I did it! functionVar(); // TypeError functionVar function(){ console.log(I did it!); }; }三、为什么提升只发生在函数作用域var 的作用域规则原文档强调理解提升有助于组织函数作用域function scope这正是var的核心特性var声明的变量属于函数作用域在函数外声明则属于全局作用域而不是块级作用域。jstips 仓库的第 59 号技巧 ES6, var vs let 对此有直接对比function varvslet() { console.log(i); // i is undefined due to hoisting // console.log(j); // ReferenceError: j is not defined for( var i 0; i 3; i ) { console.log(i); // 0, 1, 2 }; console.log(i); // 3 —— var 提升并穿透循环块函数内始终可见 // console.log(j); // ReferenceError: j is not defined for( let j 0; j 3; j ) { console.log(j); }; console.log(i); // 3 // console.log(j); // ReferenceError: j is not defined —— let 是块级作用域 }从这段代码可以清晰看到var i的声明被提升到函数顶部所以函数开头能访问到undefined的i循环结束后i依然存在而let j只在for块内有效块外访问直接ReferenceError。从源码结构看这正是第 59 号技巧所总结的两条规则var是函数作用域函数外即全局let是块级作用域。这也意味着提升现象本质上只属于var与函数声明let/const遵循另一套机制见下文第四节。四、let / const 时代提升机制的演化与暂时性死区TDZES6 引入let与const之后变量提升的规则发生了重要变化。jstips 仓库第 76 号技巧 What is the Temporal Dead Zone? 用一个三连对比讲清了这一点function myFunc(){ console.log(greeting); var greeting Hello World! }; myFunc(); // 输出undefinedvar 提升声明在前 function myFunc() { console.log(greeting); let greeting Hello World!; }; myFunc(); // 输出ReferenceError: greeting is not defined function myFunc() { console.log(greeting); const greeting Hello World!; } myFunc(); // 输出ReferenceError: greeting is not defined需要精确理解的结论是var提升是完整可见的声明提升后变量以undefined存在声明前访问不报错这是传统提升现象。let/const也存在提升进入作用域时绑定即建立但被暂时性死区Temporal Dead Zone, TDZ保护从进入作用域到声明语句执行之间的区域访问该变量一律抛出ReferenceError。这也是第 76 号技巧所描述的TDZ 是使用let和const声明变量时的 JavaScript 行为。换句话说var让你在声明前看到undefined掩盖了错误而let/const让你在声明前直接看到报错暴露了问题。在现代项目中这正是推荐优先使用let/const的重要原因之一。五、提升引发的经典陷阱循环中的闭包变量提升 函数作用域组合出的最经典 Bug是循环中创建闭包。jstips 仓库第 76 号英文编号技巧 Closures inside loops 完整记录了这个陷阱var funcs []; for (var i 0; i 3; i) { funcs[i] function() { console.log(i value is i); }; } for (var k 0; k 3; k) { funcs[k](); }直觉上期望输出i value is 0 i value is 1 i value is 2实际输出却是i value is 3 i value is 3 i value is 3原因正是提升与作用域规则的叠加var i的声明被提升i是整个函数共享的同一个变量循环结束后i停在3所有闭包捕获的都是这个同一个i因此输出三次3。该技巧给出两种解法解法一IIFE 按值传参将i的当前值拷贝进闭包for (var i 0; i 3; i) { funcs[i] (function(value) { console.log(i value is value); })(i); }解法二改用let让循环每次迭代产生新的绑定for (let i 0; i 3; i) { funcs[i] function() { console.log(i value is i); } }正如该技巧所述let将变量限定在for循环块内并在每次迭代重新绑定使i在每次迭代中都有独立取值闭包因此按预期各自捕获不同的值。这与第 59 号技巧中var vs let的结论完全一致——let在循环中按迭代重新绑定可用于规避闭包问题。六、另一个被提升放大的问题隐式全局变量仓库第 47 号技巧 Basics declarations 展示了与提升密切相关的另一个陷阱——未用var声明直接赋值会创建全局变量注意未声明变量的赋值不会提升但因为变量从未用var声明赋值语句会把它变成隐式全局变量即使发生在函数内部;(() { var x y 2 // var x; y 2;y 未声明变成全局变量 console.log(2.0:, x ${x}, y ${y}) })() console.log(-- 2.1:, x ${x}, y ${y}) // 输出 //2.0: x 2, y 2 //-- 2.1: x 1, y 2闭包内只声明了 xy 泄漏到了全局这里的关键差异是var x y 2等价于var x; x (y 2);——y从始至终没有var声明赋值时直接落入全局作用域。这与第 7 号技巧 use strict and get lazy 中提到的严格模式约束直接相关严格模式下变量只能通过带var或let/const的声明引入未声明即赋值会直接报错从而避免这类隐式全局变量与作用域污染。七、最佳实践如何用作用域组织消除提升困扰原文档在结尾给出了非常实用的组织建议这也是本文推荐的日常编码纪律将所有的变量声明放在函数作用域的顶部。这样一眼就能看清该作用域内存在哪些变量、它们来自哪个作用域避免声明分散、提升造成错觉带来的阅读负担。在使用变量之前完成定义赋值。声明集中、赋值紧跟使用点既符合提升规则也让数据流清晰可追踪。将函数定义放在作用域底部。由于函数声明整体提升放在底部既不影响调用又能让它们不挡路保持代码主体可读。将这些建议落实为现代写法配合前文结论可以得到如下推荐范式function doTheThing() { // 1. 变量声明集中在顶部优先使用 let / const let definedLater; const definedSimulateneously I am defined!; // 2. 使用前完成定义 definedLater I am defined!; console.log(definedLater); // 3. 函数定义放到底部函数声明提升可安全后置 function doSomethingElse() { console.log(I did it!); } doSomethingElse(); }八、快速自测检验你是否真正理解了提升尝试在不运行代码的情况下写出下面三段代码的输出再对照前文验证// 测试 1声明与定义同行 console.log(foo); // ? var foo bar; // 测试 2函数声明 vs 函数表达式 sayHello(); // ? function sayHello() { console.log(hello); } sayBye(); // ? var sayBye function() { console.log(bye); }; // 测试 3let 的暂时性死区 console.log(baz); // ? let baz 1;参考答案测试 1 输出undefined提升的只有声明测试 2 第一行输出hello函数声明整体提升第二行抛出TypeError: sayBye is not a function函数表达式只有变量声明被提升测试 3 抛出ReferenceErrorlet处于暂时性死区。总结变量提升是 JavaScript 引擎编译期对var声明与函数声明的预置行为var声明提升值为undefined函数声明整体提升可提前调用赋值定义不提升。理解这一点就能解释undefined与ReferenceError的分野、循环闭包陷阱的成因以及let/const暂时性死区为何是现代代码的更好选择。最终落地的实践很简单变量声明集中到作用域顶部、使用前赋值、函数定义放底部——结合仓库内相关技巧var vs let、Temporal Dead Zone、Closures inside loops、Basics declarations、use strict对照阅读即可完整掌握 JavaScript 作用域与声明的核心机制。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐深入理解 JavaScript 提升Hoisting从 var 到 let、const 与函数声明的完整指南深入理解 JavaScript 提升Hoisting从 var 到 let、const 与函数声明的完整指南 Hoisting提升是 JavaScri教程文档You Dont Know JS俄语版深入理解 JavaScript 变量提升Hoisting——声明、赋值与函数优先机制You Dont Know JS俄语版深入理解 JavaScript 变量提升Hoisting——声明、赋值与函数优先机制 导读 变量提升Hoi教程文档JavaScript 作用域、变量提升与函数提升Web 前端入门必懂的运行机制JavaScript 作用域、变量提升与函数提升Web 前端入门必懂的运行机制 本文是「千古前端图文教程」系列中关于 JavaScript 作用域与声明提升的文档教程前端上一篇番茄小说下载器终极指南打造个人永久数字图书馆的完整教程下一篇番茄小说下载器完整教程三步打造你的永久离线图书馆创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表