☰
JavaScript 变量提升(Hoisting)完全指南:声明与定义、函数提升与作用域组织实战
2026/10/8 14:10:59 网站建设 项目流程
  • 教程

【免费下载链接】jstips

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

逐行解读:

  1. console.log(notDeclared):notDeclared从未用var声明过,作用域中不存在该变量,直接抛出ReferenceError: notDeclared is not defined。
  2. console.log(definedLater):var definedLater;的声明被提升到函数顶部,此时变量存在但尚未赋值,因此输出undefined。
  3. 随后执行definedLater = 'I am defined!'完成定义,再次输出即为字符串'I am defined!'。
  4. console.log(definedSimulateneously):var definedSimulateneously = 'I am defined!'虽然声明与赋值在同一行,但提升的只有声明部分,赋值停留在原行,所以函数顶部访问到的仍是undefined。
  5. doSomethingElse():函数声明function doSomethingElse(){}整体被提升到作用域顶部,因此即使在定义之前调用,也能正常输出'I did it!'——这是函数声明与变量声明最关键的区别。
  6. 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 时代:提升机制的演化与暂时性死区(TDZ)

ES6 引入let与const之后,变量提升的规则发生了重要变化。jstips 仓库第 76 号技巧 What is the Temporal Dead Zone? 用一个三连对比讲清了这一点:

function myFunc(){ console.log(greeting); var greeting = 'Hello World!' }; myFunc(); // 输出:undefined(var 提升,声明在前) 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(闭包内只声明了 x,y 泄漏到了全局)

这里的关键差异是:var x = y = 2等价于var x; x = (y = 2);——y从始至终没有var声明,赋值时直接落入全局作用域。这与第 7 号技巧 use strict and get lazy 中提到的严格模式约束直接相关:严格模式下,"变量只能通过带var(或let/const)的声明引入",未声明即赋值会直接报错,从而避免这类隐式全局变量与作用域污染。

七、最佳实践:如何用作用域组织消除提升困扰

原文档在结尾给出了非常实用的组织建议,这也是本文推荐的日常编码纪律:

  1. 将所有的变量声明放在函数作用域的顶部。这样一眼就能看清该作用域内存在哪些变量、它们来自哪个作用域,避免"声明分散、提升造成错觉"带来的阅读负担。
  2. 在使用变量之前完成定义(赋值)。声明集中、赋值紧跟使用点,既符合提升规则,也让数据流清晰可追踪。
  3. 将函数定义放在作用域底部。由于函数声明整体提升,放在底部既不影响调用,又能"让它们不挡路",保持代码主体可读。

将这些建议落实为现代写法,配合前文结论,可以得到如下推荐范式:

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'); }; // 测试 3:let 的暂时性死区 console.log(baz); // ? let baz = 1;

参考答案:测试 1 输出undefined(提升的只有声明);测试 2 第一行输出hello(函数声明整体提升),第二行抛出TypeError: sayBye is not a function(函数表达式只有变量声明被提升);测试 3 抛出ReferenceError(let处于暂时性死区)。

总结

变量提升是 JavaScript 引擎编译期对var声明与函数声明的"预置"行为:var声明提升(值为undefined),函数声明整体提升(可提前调用),赋值定义不提升。理解这一点,就能解释undefined与ReferenceError的分野、循环闭包陷阱的成因,以及let/const暂时性死区为何是现代代码的更好选择。最终落地的实践很简单:变量声明集中到作用域顶部、使用前赋值、函数定义放底部——结合仓库内相关技巧(var vs let、Temporal Dead Zone、Closures inside loops、Basics declarations、use strict)对照阅读,即可完整掌握 JavaScript 作用域与声明的核心机制。

  • 教程

【免费下载链接】jstips

This is about useful JS tips!

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

相关推荐

上一篇:番茄小说下载器终极指南:打造个人永久数字图书馆的完整教程
下一篇:番茄小说下载器完整教程:三步打造你的永久离线图书馆

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询