JavaScript 变量提升与函数声明提升机制详解
本文深入解析 JavaScript 中的 Hoisting(提升)机制。通过剖析执行期上下文与词法环境的两个阶段,阐明变量和函数声明如何在代码执行前被注册。文章解释了 var、let、const 的区别,以及为何访问未赋值变量返回 undefined 而非报错,帮助开发者建立扎实的基础知识。
JavaScript 变量提升与函数声明提升机制详解
本文旨在介绍 JavaScript 中 Hoisting(变量提升与函数声明提升)的核心概念,供开发者参考学习。
核心问题在于:函数声明与变量是如何被“移动”至作用域顶部的。
术语 Hoisting(提升)常用于解释标识符的解析过程。实际上,该术语特指变量和函数声明被提升至函数或全局作用域顶部的行为。尽管标准文档中未必直接使用该词,但它是基于字面含义的比喻性描述。
若已掌握 JavaScript 作用域的基本原理,深入理解 Hoisting 将有助于构建更坚实的知识体系。作为前端开发中的重要概念,变量提升和函数声明提升常出现在技术面试或笔试题中,其重要性不言而喻。
为深入理解基础,需明确“Hoisting”的具体含义。需注意的是,JavaScript 属于解释型语言,代码逐行执行,这与编译型语言存在差异。
示例代码与疑问分析
请观察以下示例代码。
console.log(notyetdeclared);
// 打印 'undefined'
var notyetdeclared = 'now it is declared';
hoisting();
function hoisting(){
console.log(notyetdeclared);
// 打印 'undefined'
var notyetdeclared = 'declared differently';
console.log(notyetdeclared);
// 打印 'declared differently'
}
在分析上述代码后,可提出以下疑问:
- 为何在第 6 行,即函数声明之前就能访问该函数?
- 为何第 1 行未抛出错误,是因为变量 notyetdeclared 此时尚不存在吗?
- 第 4 行中,notyetdeclared 已在全局作用域声明,为何在第 9 行打印时仍显示为 undefined?
JavaScript 的设计逻辑严密,上述看似异常的现象均有明确解释。
执行期上下文与调用栈
我们从执行过程入手:当代码在 JavaScript 中运行时,会建立执行期上下文。JavaScript 主要包含两种执行期上下文:全局执行期上下文和函数执行期上下文(注:执行期上下文指作用域,不同于通常所说的 this 指向的上下文)。由于 JavaScript 基于单线程模型,每次仅执行一段代码。
上述代码的执行流程如图所示。

示例代码的调用栈结构如下。
- 程序从栈(stack)上的全局执行期上下文开始执行。
- 当调用 hoisting() 函数时,一个新的函数执行期上下文被压入栈中,同时全局执行期上下文处于暂停状态。
- hoisting() 执行完毕后,其执行期上下文从栈中弹出,全局执行期上下文恢复运行。
词法环境与标识符解析
该过程虽直观,但未完全解释示例代码中的异常现象。当执行期上下文跟踪代码执行时,词法环境负责跟踪标识符到具体变量的映射。词法环境本质上是 JavaScript 作用域机制的内部实现,通常与特定代码结构(如函数或 for 循环块)关联。每次创建函数时,对其词法环境的引用会存储在一个名为 [[Environment]] 的内部属性中。
上述术语涵盖了一个简单且合乎逻辑的概念,可分解如下:词法环境是一个用于跟踪代码块中变量和函数的概念。除跟踪局部变量、函数声明和参数外,每个词法环境还记录其父级词法环境。因此,上述示例代码在 JavaScript 引擎中的解析方式如图所示。

注:
若理解存在困难,建议参考以下三篇文章:
- 深入理解 JavaScript 中的作用域和上下文
- JavaScript 核心概念之作用域和闭包
- 实例分析 JavaScript 作用域
为在词法环境中解析标识符,JavaScript 引擎会检查当前环境的引用。若未找到,则通过 [[environment]] 移至外部环境。此过程持续进行,直至找到标识符或抛出 'not defined'(未定义)错误。
代码执行的两个阶段
总体而言,JavaScript 代码执行分为两个阶段:第一阶段在当前词法环境中注册所有变量和函数声明;随后进入第二阶段,开始实际执行 JavaScript 代码。
因此,详细说明第一阶段:它包含两个步骤。
- 扫描当前函数声明中的代码。函数表达式和箭头函数会被跳过。对于每个发现的函数,都会创建一个新的函数对象,并使用函数名称将其绑定到环境中。若标识符名称已存在,其值将被覆盖。
- 接着扫描当前环境的变量。找到使用 var 定义的变量以及位于其他函数之外的变量,并注册一个标识符,其值初始化为 undefined。若标识符已存在,则该值保持不变。
注意:使用 let 和 const 定义的是块级变量,处理方式与 var 略有不同。更多内容将在另一篇文章中介绍。
既然已对词法环境这一基本概念有所了解,让我们回到示例代码,解释前述问题。
在设置全局上下文时,引擎会扫描环境并将 hoisting() 函数附加到标识符。随后,变量 notyetdeclared 被注册,其值初始化为 undefined。按此步骤继续理解代码。
示例代码问题解答
现在解释示例代码中提出的三个问题:
第 6 行,该函数声明之前为何能访问?
在第 1 阶段,hoisting() 函数已注册至标识符。当 JS 代码在第 2 阶段的全局执行期上下文中开始执行时,它会查找 hoisting 的词法环境,并在其定义之前找到该函数。
第 1 行,未抛出错误,是因为此时变量 notyetdeclared 不存在吗?
同理,notyetdeclared 在标识符中注册,并在第 1 阶段初始化为 undefined,因此不会抛出任何错误。
最后,
第 4 行,notyetdeclared 已在全局作用域内声明,为何在第 9 行打印时仍为 undefined?
现在进入函数 hoisting 环境。在第 1 阶段,notyetdeclared 被注册并初始化为 undefined,因为在此词法环境中,该变量尚未被注册。若第 12 行不包含 var 关键字,情况则不同。
希望此时能清楚看到,在 JavaScript 中,Hoisting(提升)仅是用于解释背后原理的概念模型。从技术层面讲,函数和变量并不会真正移动到任何地方。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对本站的支持。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
