- 教程
【免费下载链接】jstips
This is about useful JS tips!
变量提升(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 definednotDeclared在整个作用域中从未出现过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。该技巧给出的两种解法恰好对应两条提升边界:
- 用立即调用包裹,按值传入
i(对应 IIFE 技巧,见_posts/en/javascript/2016-01-25-Using-immediately-invoked-function-expression.md); - 改用
let i,让i成为每次迭代的独立绑定:
for (let i = 0; i < 3; i++) { funcs[i] = function() { console.log("i value is " + i); } } // 输出:0, 1, 2可见,只要清楚"var提升到函数作用域、let绑定到块/迭代",这类陷阱的原因与解法都一目了然。
六、最佳实践:用提升规律组织函数作用域
原技巧在结尾给出了三条可直接落地的编码建议,这也是本篇文章的最终结论:
- 把所有
var变量声明放在函数作用域顶部:由于声明必然被提升,与其让读者在函数中部"意外"发现变量,不如主动在顶部集中声明,让变量来自哪个作用域一目了然; - 在使用变量之前完成定义(赋值):避免依赖"提升后恰好是 undefined"的隐式行为,保证阅读顺序即执行顺序;
- 把函数定义放在作用域底部:利用函数声明整体提升的特性,让函数定义"不挡路",代码主体专注于业务逻辑,需要时可随时调用底部定义的函数。
按此组织,doTheThing()可以重构为:顶部集中var声明,中部放业务逻辑与赋值,底部放函数声明。这样的代码无论对读者还是对调试者,作用域边界都清晰可辨。
结语
变量提升的本质是"声明先注册、赋值后执行"的两阶段模型:var声明与函数声明被提升,而赋值与函数表达式不被提升。掌握这一规律,你不仅能准确解释本文示例中的undefined、ReferenceError与TypeError三种不同结果,还能预判循环闭包、作用域泄漏等实战问题。将"声明置顶、先定义后使用、函数定义置底"作为日常编码习惯,函数作用域的组织将变得井然有序。仓库中同主题的 Basics declarations、var vs let 与 Closures inside loops 提供了更多可继续深挖的实战示例。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
JavaScript 变量提升(Hoisting)完全指南:声明与定义、函数提升与作用域组织实战
JavaScript 变量提升(Hoisting)完全指南:声明与定义、函数提升与作用域组织实战 导读 变量提升(Hoisting)是 JavaScript 中
教程You Don't Know JS(俄语版):深入理解 JavaScript 变量提升(Hoisting)——声明、赋值与函数优先机制
You Don't Know JS(俄语版):深入理解 JavaScript 变量提升(Hoisting)——声明、赋值与函数优先机制 导读 :变量提升(Hoi
教程文档深入理解 JavaScript 提升(Hoisting):从 var 到 let、const 与函数声明的完整指南
深入理解 JavaScript 提升(Hoisting):从 var 到 let、const 与函数声明的完整指南 Hoisting(提升)是 JavaScri
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考