news 2026/10/8 13:18:31

JS Tips 11 深度解析:JavaScript 变量提升(Hoisting)——声明、定义与函数作用域组织

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS Tips 11 深度解析:JavaScript 变量提升(Hoisting)——声明、定义与函数作用域组织
  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

变量提升(Hoisting)是 JavaScript 中最容易让新手困惑、却也最能解释各种"反直觉"运行时行为的机制之一。本篇基于 JS Tips 项目第 11 条技巧(_posts/en/javascript/2016-01-11-hoisting.md),系统讲解"声明(declaration)被提升、定义(definition)不被提升"这一核心规律,并通过逐行拆解原始示例,帮助你在阅读完本文后能准确预判var、函数声明与函数表达式在作用域内的真实行为,进而写出结构清晰、可预测的函数代码。

一、核心规律:声明提升,定义不提升

理解变量提升,关键在于区分 JavaScript 中两个常被混用的概念:

  • 声明(Declaration):让系统知道某个变量存在,例如var definedLater;。声明会被提升(hoisted)到所在作用域的顶部。
  • 定义(Definition):给变量赋值,例如definedLater = 'I am defined!';。定义不会被提升——即使你把声明和定义写在同一行(如var x = 1;),被提升的也只有其中的声明部分,赋值动作仍停留在原位置执行。

原技巧给出的总纲只有一句话:变量声明和函数定义会被提升到顶部;变量定义不会,即使你在一行内同时声明并定义变量。这一规律适用于var声明的变量与函数声明(function declaration),是理解下述所有示例的钥匙。

二、逐行拆解 doTheThing():五种场景的运行时真相

原文档通过一个doTheThing()函数集中展示了五种不同写法的运行结果,这里完整保留并逐一剖析:

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!'); } }

场景一:访问从未声明的变量 → ReferenceError

console.log(notDeclared); // ReferenceError: notDeclared is not defined

notDeclared在整个作用域中从未出现过var声明,因此 JavaScript 引擎无法为该标识符建立绑定,访问它直接抛出ReferenceError。提升机制只对"已声明"的标识符生效,这是它与场景二、三最本质的区别。

场景二:声明与定义分离 → 提升后输出 undefined

console.log(definedLater); // Outputs: undefined var definedLater; definedLater = 'I am defined!' console.log(definedLater); // Outputs: 'I am defined!'

var definedLater;的声明被提升到函数顶部,相当于在console.log(definedLater)之前引擎已悄悄执行了"变量已存在、值为undefined"的初始化。因此第一次访问不会报错,而是打印undefined;当赋值语句真正执行后,再次访问才得到字符串'I am defined!'。从引擎行为看,上述代码等价于:

function doTheThing() { var definedLater; // 声明被提升到顶部,初始值为 undefined console.log(definedLater); // undefined definedLater = 'I am defined!' // 赋值仍留在原位执行 console.log(definedLater); // 'I am defined!' }

场景三:声明与定义同行 → 定义不提升,依旧先打印 undefined

console.log(definedSimulateneously); // Outputs: undefined var definedSimulateneously = 'I am defined!' console.log(definedSimulateneously); // Outputs: 'I am defined!'

这是本技巧最反直觉、也最值得记住的一点:即使var definedSimulateneously = 'I am defined!'写在同一行,提升的也只是var definedSimulateneously;这半句,赋值= 'I am defined!'仍在原位。所以第一行输出依然是undefined,赋值执行后才变为'I am defined!'。"同一行声明并定义"并不能让变量在提升后立刻拥有值。

场景四:函数声明 → 整体提升,可提前调用

doSomethingElse(); // Outputs: 'I did it!' function doSomethingElse(){ console.log('I did it!'); }

函数声明(function declaration)与var变量声明不同:它不仅把名字提升到作用域顶部,连函数体(定义)一起整体提升。因此即使在定义语句之前调用doSomethingElse(),也能正常执行并打印'I did it!'。

场景五:函数表达式 → 仅变量名被提升,调用时报 TypeError

functionVar(); // TypeError: undefined is not a function var functionVar = function(){ console.log('I did it!'); }

var functionVar = function(){...}的本质是"变量声明 + 函数表达式赋值"。按照"声明提升、定义不提升"的规律,提升到顶部的只有var functionVar;,此时functionVar的值是undefined。在赋值尚未执行的时刻调用functionVar(),相当于对undefined做函数调用,于是抛出TypeError: undefined is not a function。这正是场景三在函数形态下的复现,也是"函数声明 vs 函数表达式"在提升行为上的关键分水岭。

三、从仓库佐证看声明机制:声明是作用域绑定的前提

变量提升不是"把代码物理移动到顶部",而是 JavaScript 在进入一个作用域时,先完成对该作用域内所有var声明与函数声明的绑定注册,再开始逐条执行语句。这一理解与仓库第 47 条技巧(_posts/en/javascript/2016-02-16-basics-declarations.md)相互印证。该技巧演示了复合声明与赋值的展开规则:

var y, x = y = 1 // == var x; var y; x = y = 1 console.log('--> 1:', `x = ${x}, y = ${y}`) //--> 1: x = 1, y = 1

可以看到var x = y = 1会被拆解为"两个var声明 + 两个赋值",这与本技巧"声明与定义分离"的规律完全一致:声明负责建立绑定,赋值负责填充值。该文档还通过 IIFE 演示了未用var声明的变量(如var x = y = 2中的y)会泄漏到全局作用域——因为y根本没有被声明,自然谈不上被提升到闭包内部,这正是"声明是作用域绑定前提"的负面例证。

四、var 与 let:提升机制的边界在哪里

仓库第 59 条技巧(_posts/en/javascript/2016-08-25-keyword-var-vs-let.md)从另一个角度划出了提升机制的边界:

  • var声明的变量作用域是函数作用域(或全局),声明会被提升,访问未赋值变量得到undefined;
  • let声明的变量作用域是块级作用域,不会像var那样提升到整个块——在声明语句之前访问会抛出ReferenceError(即暂时性死区 TDZ)。
{ console.log(c); // undefined. Due to hoisting var c = 2; } { console.log(b); // ReferenceError: b is not defined let b = 3; }

对比可见:var c因提升而可被提前访问(值为undefined),而let b在声明前访问直接报错。该技巧还指出,let不应被视为var的全面替代品——var仍然是"变量贯穿整个函数"这一意图的有效信号,let只是在需要块级作用域时提供了更精确的选项。理解这一点有助于你在组织作用域时按需选择关键字。

五、提升在实战中的经典陷阱:循环中的闭包

提升不仅影响单一函数内的访问顺序,还会在循环与闭包组合时制造经典陷阱。仓库第 76 条技巧(_posts/en/javascript/2017-06-14-closures-inside-loops.md)展示了这一现象:

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 3 // i value is 3 // i value is 3

循环中var i的声明被提升到函数作用域顶部,三个闭包共享同一个i绑定;循环结束后i停在 3,因此三个函数都打印3。该技巧给出的两种解法恰好对应两条提升边界:

  1. 用立即调用包裹,按值传入i(对应 IIFE 技巧,见_posts/en/javascript/2016-01-25-Using-immediately-invoked-function-expression.md);
  2. 改用let i,让i成为每次迭代的独立绑定:
for (let i = 0; i < 3; i++) { funcs[i] = function() { console.log("i value is " + i); } } // 输出:0, 1, 2

可见,只要清楚"var提升到函数作用域、let绑定到块/迭代",这类陷阱的原因与解法都一目了然。

六、最佳实践:用提升规律组织函数作用域

原技巧在结尾给出了三条可直接落地的编码建议,这也是本篇文章的最终结论:

  1. 把所有var变量声明放在函数作用域顶部:由于声明必然被提升,与其让读者在函数中部"意外"发现变量,不如主动在顶部集中声明,让变量来自哪个作用域一目了然;
  2. 在使用变量之前完成定义(赋值):避免依赖"提升后恰好是 undefined"的隐式行为,保证阅读顺序即执行顺序;
  3. 把函数定义放在作用域底部:利用函数声明整体提升的特性,让函数定义"不挡路",代码主体专注于业务逻辑,需要时可随时调用底部定义的函数。

按此组织,doTheThing()可以重构为:顶部集中var声明,中部放业务逻辑与赋值,底部放函数声明。这样的代码无论对读者还是对调试者,作用域边界都清晰可辨。

结语

变量提升的本质是"声明先注册、赋值后执行"的两阶段模型:var声明与函数声明被提升,而赋值与函数表达式不被提升。掌握这一规律,你不仅能准确解释本文示例中的undefined、ReferenceError与TypeError三种不同结果,还能预判循环闭包、作用域泄漏等实战问题。将"声明置顶、先定义后使用、函数定义置底"作为日常编码习惯,函数作用域的组织将变得井然有序。仓库中同主题的 Basics declarations、var vs let 与 Closures inside loops 提供了更多可继续深挖的实战示例。

  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 13:18:28

Superpowers技能系统:让Claude从聊天机器人升级为工作流执行者

上周有个朋友跟我吐槽&#xff0c;说自己用 Claude 做深度调研&#xff0c;每次都要在提示词里写一大堆背景、要求、输出格式&#xff0c;换个项目又得重写一遍&#xff0c;烦得要死。我跟他说&#xff1a;你该试试 superpowers 了。他一脸疑惑地问&#xff0c;这是什么&#x…

作者头像 李华
网站建设 2026/10/8 13:18:14

上海母婴室微生物检测:空气、护理台和共用用品为什么要分开确认

上海母婴室微生物检测&#xff0c;先区分母婴室空气、拟测接触部位和真实共有用品&#xff0c;核对每种对象的方法与判定依据。不能套用医院整套项目或给所有台面统一限值。说明设施形式、清洁使用状态和进场条件后再委托&#xff0c;现场记录保护顾客隐私&#xff0c;结果只用…

作者头像 李华
网站建设 2026/10/8 13:18:09

代码审查的核心是代码可读性:从找bug思维到团队资产构建

“一个家庭&#xff0c;只有一个父亲&#xff1b;一个项目&#xff0c;只有一个可以阅读的源码。”这句话我是在一次代码审查的会议室里听到的&#xff0c;当时一个老工程师为了说服我们重写一段完全没人能看懂的模块&#xff0c;引用了这句不知道从哪本书里翻出来的话。那句调…

作者头像 李华
网站建设 2026/10/8 13:13:43

pytest接口自动化测试实战:fixture、参数化与项目搭建

1. 为什么接口自动化测试首选pytest而不是unittest做接口自动化测试&#xff0c;很多人都是从Python的unittest起步的。这套标准库框架的好处是零安装、开箱即用&#xff0c;网上关于它的资料也多到泛滥。但真正等项目用例数量跑起来&#xff0c;达到几百上千条之后&#xff0c…

作者头像 李华