JavaScript 变量提升与函数声明提升机制详解

作者:函数茶馆时间:2026-10-22 07:24:12浏览:225

本文深入解析 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 基于单线程模型,每次仅执行一段代码。

上述代码的执行流程如图所示。

JavaScript 变量提升与函数声明提升机制详解

示例代码的调用栈结构如下。

  • 程序从栈(stack)上的全局执行期上下文开始执行。
  • 当调用 hoisting() 函数时,一个新的函数执行期上下文被压入栈中,同时全局执行期上下文处于暂停状态。
  • hoisting() 执行完毕后,其执行期上下文从栈中弹出,全局执行期上下文恢复运行。

词法环境与标识符解析

该过程虽直观,但未完全解释示例代码中的异常现象。当执行期上下文跟踪代码执行时,词法环境负责跟踪标识符到具体变量的映射。词法环境本质上是 JavaScript 作用域机制的内部实现,通常与特定代码结构(如函数或 for 循环块)关联。每次创建函数时,对其词法环境的引用会存储在一个名为 [[Environment]] 的内部属性中。

上述术语涵盖了一个简单且合乎逻辑的概念,可分解如下:词法环境是一个用于跟踪代码块中变量和函数的概念。除跟踪局部变量、函数声明和参数外,每个词法环境还记录其父级词法环境。因此,上述示例代码在 JavaScript 引擎中的解析方式如图所示。

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(提升)仅是用于解释背后原理的概念模型。从技术层面讲,函数和变量并不会真正移动到任何地方。

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

作者最新文章