JavaScript新手必看:彻底搞懂变量提升机制(避坑指南+实战技巧)
- JavaScript新手必看:彻底搞懂变量提升机制(避坑指南+实战技巧)
- 为什么你的代码总在声明前就“认识”变量?
- 从一段让人困惑的代码说起,揭开提升机制的神秘面纱
- var、let、const 三兄弟谁被提升了?函数声明和表达式有何不同?
- JS 引擎如何在幕后悄悄“搬家”你的变量和函数
- 在 if 语句里用 var、函数重名覆盖等真实开发翻车现场
- 1. if 块里的“幽灵变量”
- 2. 函数重名“你死我活”
- 3. 循环里的“延迟打印”经典坑
- 实际开发中如何安全利用或规避 Hoisting
- 1. 模块初始化顺序控制
- 2. IIFE 的妙用——“洁癖”开发者的好伙伴
- 3. 严格模式下的行为变化
- 当控制台输出“undefined”却找不到原因时,你应该检查的5个地方
- 写出更健壮代码的实用技巧
- 1. 用 ESLint 规则约束提升行为
- 2. 代码组织习惯——“先声明,后使用”写成肌肉记忆
- 3. 拥抱 let/const 的最佳实践
- 让 Hoisting 为你打工的小彩蛋
- 1. 利用函数提升做“微插件”架构
- 2. “惰性初始化”技巧
- 写在最后的“防呆口诀”
JavaScript新手必看:彻底搞懂变量提升机制(避坑指南+实战技巧)
“我明明还没写
var a,控制台怎么就认识a了?—— 别慌,这不是灵异事件,而是 JavaScript 的‘搬家小能手’Hoisting 在偷偷帮你‘整理行李’。”
为什么你的代码总在声明前就“认识”变量?
先上一段让无数新手怀疑人生的代码:
console.log(name);// undefinedvarname='Lily';还没赋值就能打印?JS 引擎怕不是成精了?
别急,把镜头拉远——这段代码在 JS 眼里其实是这样的:
varname;// 搬家:声明被提升到作用域顶部console.log(name);// undefinedname='Lily';// 原地不动:赋值留在老位置这就是Hoisting(变量提升):声明被提前,赋值被留在原地。
听起来像“空间瞬移”,但本质上只是编译阶段的小把戏。接下来,我们把它拆成“乐高积木”,一块块拼给你看。
从一段让人困惑的代码说起,揭开提升机制的神秘面纱
先来个“面试高频”坑:
functionfoo(){console.log(a);// ?vara=1;console.log(a);// ?vara=2;console.log(a);// ?}foo();很多小伙伴第一次跑完输出undefined → 1 → 2,松了口气:“嗯,符合预期。”
但如果把var换成let呢?
functionfoo(){console.log(a);// 报错:Cannot access 'a' before initializationleta=1;}foo();瞬间翻车。为什么var能“提前打招呼”,let却直接翻脸?
答案藏在编译阶段的两张表里:
| 阶段 | var 行为 | let/const 行为 |
|---|---|---|
| 编译 | 把变量名登记进变量环境(VariableEnvironment),初始值undefined | 登记进词法环境(LexicalEnvironment),但不赋初值,进入“暂时性死区”(TDZ) |
| 执行 | 遇到赋值即覆盖 | 遇到声明才“解锁” |
用一段“伪代码”模拟 JS 引擎的“小算盘”:
// 引擎视角(伪代码)functionfoo(){// 编译阶段varEnv.a=undefined;// var 先占坑// lexEnv.a = <uninitialized> // let 先锁死// 执行阶段console.log(varEnv.a);// undefinedvarEnv.a=1;console.log(varEnv.a);// 1}小结:
var是“先上车后补票”,上车时给张“undefined”站票。let/const是“检票进站”,没票(未初始化)就硬闯,直接红牌罚下。
var、let、const 三兄弟谁被提升了?函数声明和表达式有何不同?
继续加料,看函数的表现:
console.log(cat);// 函数体被完整提升console.log(dog);// undefinedconsole.log(hamster);// 报错functioncat(){return'🐱';}vardog=function(){return'🐶';};lethamster=()=>'🐹';运行结果:
cat:打印整个函数体——函数声明连身子带尾巴一起“瞬移”。dog:只是var的“站票”,值为undefined。hamster:let还在 TDZ 里睡懒觉,访问即炸。
再补一刀——函数名与变量名重名:
console.log(x);// 函数体varx=1;functionx(){}console.log(x);// 1JS 引擎的“优先座”规则:函数声明 > 变量声明。
编译阶段先给函数占座,变量来了只能候补;执行阶段变量赋值再把函数覆盖。
JS 引擎如何在幕后悄悄“搬家”你的变量和函数
把镜头切到引擎内部,来看“搬家”流水线的两张工单:
- 词法分析→语法树
- 生成执行上下文(Execution Context)
- 变量环境(VariableEnvironment)——
var& 函数声明的“集体宿舍” - 词法环境(LexicalEnvironment)——
let/const的“VIP 包厢” - 外部环境引用(OuterEnv)——闭包的灵魂
- 变量环境(VariableEnvironment)——
- 代码执行
用一段“超长”代码把过程揉在一起:
// 源码functionshowRoom(){console.log(tesla,benz,bmw);vartesla='S';letbenz='E';functionbmw(){return'X5';}}// 引擎视角(编译后)functionshowRoom(){// 变量环境vartesla=undefined;varbmw=functionbmw(){return'X5';};// 词法环境letbenz=<uninitialized>;// 执行阶段console.log(tesla,benz,bmw);// undefined 报错 functiontesla='S';benz='E';}小彩蛋:函数表达式不会享受“函数声明”的 VIP 待遇:
console.log(fe);// undefinedvarfe=functionnamedFE(){return42;};namedFE只在函数内部可见,外部拿不到,命名函数表达式的彩蛋自己留着用。
在 if 语句里用 var、函数重名覆盖等真实开发翻车现场
1. if 块里的“幽灵变量”
functiondiscount(price){if(price>100){varrate=0.8;}console.log(rate);// 0.8 还是 undefined?returnprice*rate;}discount(200);// 160var才不管你是不是在if里,作用域是整个函数。
所以rate会被提升到老大哥的位置,只是赋值留在if块内。
改写成let就能让rate老实待在块里:
functiondiscount(price){if(price>100){letrate=0.8;returnprice*rate;}console.log(rate);// ReferenceError}2. 函数重名“你死我活”
functiongo(){console.log('A');}functiongo(){console.log('B');}go();// B后写的函数声明直接盖楼。
但如果一个是函数声明、一个是函数表达式呢?
vargo=function(){console.log('C');};functiongo(){console.log('D');}go();// C编译阶段:go先被函数声明占坑;执行阶段:变量赋值把函数覆盖。
3. 循环里的“延迟打印”经典坑
for(vari=0;i<3;i++){setTimeout(()=>console.log(i),0);}// 3 3 3var只有一份,共享作用域。
解法 A:IIFE 包一层(老派写法)
for(vari=0;i<3;i++){(function(j){setTimeout(()=>console.log(j),0);})(i);}解法 B:直接let搞定(块级作用域每次循环都新造一份)
for(leti=0;i<3;i++){setTimeout(()=>console.log(i),0);}实际开发中如何安全利用或规避 Hoisting
1. 模块初始化顺序控制
利用函数提升做“自解释”的模块:
// math.jsexportfunctionadd(a,b){returna+b;}exportfunctionmul(a,b){returna*b;}// main.jsimport{add,mul}from'./math.js';console.log(add(2,mul(3,4)));// 14因为函数声明提升,调用写在前面也不会报错,阅读顺序更符合人类直觉。
2. IIFE 的妙用——“洁癖”开发者的好伙伴
constresult=(functioninit(){// 临时变量全部关进小黑屋,全局污染为零constsecret=Math.random();return{get:()=>secret};})();console.log(result.get());IIFE 立刻执行,内部变量不会泄露,顺带规避提升带来的命名冲突。
3. 严格模式下的行为变化
'use strict';不会阻止提升,但禁止函数声明出现在块级作用域(ES6 之后放松),减少“意外提升”:
'use strict';if(true){functionblockFun(){}// 老版本浏览器会报错}当控制台输出“undefined”却找不到原因时,你应该检查的5个地方
- 有没有在声明前使用 var 变量——99% 的
undefined都是它。 - 函数表达式是否被提前调用——表达式只有变量提升,值为
undefined。 - 重复声明——后面把前面覆盖,打印时机不对。
- 把 let/const 错写成了 var——块级作用域失效。
- 外层作用域悄悄声明了同名变量——“幽灵变量”阴影。
排查口诀:“先搜 var,再看调用时机,最后检查作用域链”。
写出更健壮代码的实用技巧
1. 用 ESLint 规则约束提升行为
.eslintrc.json
{"rules":{"no-use-before-define":["error",{"functions":false,"variables":true}],"prefer-const":"error","no-var":"error"}}no-use-before-define:禁止变量提前使用,让代码“顺序即真相”。prefer-const:能const就别let。no-var:直接踢馆var,从源头干掉提升烦恼。
2. 代码组织习惯——“先声明,后使用”写成肌肉记忆
// 👎 riskyfunctionutil(){console.loghelper();// 提升允许,但阅读断层functionhelper(){}}// 👍 clearfunctionutil(){functionhelper(){}console.log(helper());}3. 拥抱 let/const 的最佳实践
// 默认 const,实在要改再用 letconstAPI_HOST='https://api.xxx.com';letretryCount=0;让 Hoisting 为你打工的小彩蛋
1. 利用函数提升做“微插件”架构
// 插件注册器constPluginManager={plugins:[],register(fn){this.plugins.push(fn);},run(){this.plugins.forEach(fn=>fn());}};// 写在后面也不会报错helloPlugin();functionhelloPlugin(){PluginManager.register(()=>console.log('Hello'));}// 启动PluginManager.run();// Hello函数声明提升让你先写业务逻辑,后写插件声明,阅读顺序更顺滑。
2. “惰性初始化”技巧
// 利用 var 提升 + 惰性赋值functiongetCache(){if(!cache){cache=expensiveCompute();}returncache;}varcache;// 提升但不赋值,等第一次调用再干活写在最后的“防呆口诀”
var老油条,作用域 whole 函数;let/const小年轻,块级坐牢不瞎蹦;
函数声明 VIP,连身带尾飞屋顶;
表达式买站票,提升只给 undefined;
暂死区里别硬闯,TDZ 报错没商量;
先声明后使用,ESLint 帮你忙;
口诀背得滚瓜烂,Hoisting 坑我抗!
把这段口诀贴在工位上,下次再看到“undefined”时,微笑面对:“哦,又是你在调皮。” 然后三下五除二,定位、重构、跑测试、push、下班打卡,一气呵成。
祝你编码路上,提升不再有坑,只有优雅的代码与准点下班的夕阳。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 | |
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!