深入理解 JavaScript 作用域与变量提升机制

作者:拾光

时间:2026-10-30

浏览:275

本文深入解析 JavaScript 中的作用域机制与变量提升(Hoisting)原理。对比 ES5 的全局与函数作用域,以及 ES6 引入的块级作用域。详细阐述 var、function 与 let、const 在提升行为上的差异,解释临时死区(TDZ)概念,并分析函数声明与变量声明冲突时的优先级规则,帮助开发者避免常见陷阱。

文章目录

JavaScript 作用域基础

作用域(Scoping)

对于许多 JavaScript 开发者而言,作用域是一个容易混淆的概念,即便是一些经验丰富的程序员也可能对此理解不深。JavaScript 的作用域之所以令人困惑,部分原因在于其语法结构与 C 语言家族相似,但行为却大相径庭。从本质上看,作用域是一个封闭的空间,它对特定范围产生影响,而不会波及外部。在这样的空间中,外部代码无法直接访问内部变量,但内部代码可以访问外部变量。

在 C 语言中,变量分为全局变量和局部变量。全局变量的作用范围覆盖任何文件和函数(其他 C 文件需使用 extern 关键字声明,而 static 关键字可将作用范围限制在当前文件内)。局部变量的作用范围则从声明处开始,直到最近的大括号所涵盖的块级范围。Java 语言没有全局变量,而是包含类变量、成员变量和局部变量,其作用范围取决于 public、protected、private 等访问权限修饰符,此处不再赘述。

JS 作用域有哪些?

在 ES5 标准中,JavaScript 仅存在两种形式的作用域:全局作用域和函数作用域。

全局作用域实际上是指全局对象的作用域,只要未被函数作用域覆盖,代码中的任意位置均可访问它。

函数对象作用域与 C 语言的局部变量作用域不同,它的作用范围覆盖整个函数体,无论变量是在函数的哪个位置声明的。这就是所谓的“变量提升”(hoisting)概念的基础。稍后我们将专门针对 hoisting 进行详细解释。

在 ES6 标准中,新增了一种块级作用域(即最近的大括号涵盖的范围),但该特性仅限于使用 let 关键字声明的变量。 作用域演示:

JavaScript 作用域基础

在定义变量时,如果省略 var 关键字,例如 i=0,则该变量会被定义为全局变量,其作用域为全局作用域;否则,它将是局部变量,作用域为函数作用域。上述代码第一行的 var i=0 之所以被视为全局变量,是因为它是在全局区域声明的,并未处于任何函数范围内,因此其效果与 i=0 相同。

至于为什么结果会呈现这种状态,继续阅读下文即可明了。

申明形式

变量声明:

JavaScript 作用域基础

函数申明:

JavaScript 作用域基础

变量提升(Hoisting)机制

变量提升(Hoisting)

引出一个问题

下面这段代码会输出什么内容?

JavaScript 作用域基础

我在面试中多次提出这道题,大多数人都认为输出的是日期。但真实的结果却是 undefined。为什么会这样呢?这里引出了一个核心概念——hoisting,中文意为“变量提升”。MDN 对变量 hoisting 的解释如下:

var hoisting

Because variable declarations (and declarations in general) are processed before any code is executed, declaring a variable anywhere in the code is equivalent to declaring it at the top. This also means that a variable can appear to be used before it's declared. This behavior is called "hoisting", as it appears that the variable declaration is moved to the top of the function or global code.

这段话翻译下来就是

因为变量声明是在任何代码执行前处理的,在代码区中任意地方声明变量和在最开始(最上面)的地方声明是一样的。也就是说,看起来一个变量可以在声明之前被使用!这种行为就是所谓的“hoisting”,也就是变量提升,看起来就像变量的声明被自动移动到了函数或全局代码的最顶上。

注意:仅仅是声明提升了,定义并不会被提升。

如此,上面这段代码其实就是下面的形式:

JavaScript 作用域基础

所以,这样就应该理解了,console 输出的时候,tmp 变量仅仅是声明了但未定义,所以输出应该是 undefined。

这里需要说明的是,虽然所有的声明(包括 ES5 的 var、function,和 ES6 的 function *、let、const、class)都会被提升,但是 var、function、function * 和 let、const、class 的提升行为却并不相同!具体原因可以参考相关说明(大体的意思是虽然 let,const,class 也被提升了,但是却并不会被初始化,这时候去访问它们则会报 ReferenceError 异常,它们需要到语句执行的时候才会被初始化,而在被初始化之前的状态叫做 temporal dead zone)。我们来看一段代码就知道了:

JavaScript 作用域基础 这里a被提升,但因为定义在后,所以输出undefined

这里a虽然被提升,但却报了引用错误!

之所以或这样

因为这样的原因,推荐的做法是在声明变量的时候,将所用的变量都写在作用域(全局作用域或函数作用域)的最顶上,这样代码看起来就会更清晰,更容易看出哪个变量是来自函数作用域的,哪个又是来自作用域链(本文不对此多做解释,请读者自行百度,有机会再补充说明)。

重复声明与函数提升优先级

重复声明

JavaScript 作用域基础

上面的输出其实是:1 2 2。虽然看起来里面 x 声明了两次,但上面说了,js 的 var 变量只有全局作用域和函数作用域两种,且声明会被提升,因此实际上 x 只会在最顶上开始的地方声明一次,var x=2 的声明会被忽略,仅用于赋值。也就是说上面的代码实际上跟下面是一致的。

JavaScript 作用域基础

函数和变量同时提升的问题

如果是函数和变量类型同时声明定义了,会发生什么事情呢?看下面的代码

JavaScript 作用域基础 A

上面的输出结果其实是: function foo(){} ,也就是函数内容。

而如果是这样的形式呢

JavaScript 作用域基础 B

它的输出却变成:undefined

为什么会这样呢?

原来函数提升分为两种情况:

一种:函数声明。就是上面A,function foo(){}这种形式

另一种:函数表达式。就是上面B,var foo=function(){}这种形式

第二种形式其实就是 var 变量的声明定义,因此上面的 B 输出结果为 undefined 应该就能理解了。

而第一种函数声明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此 A 跟下面的这种方式是等价的!

JavaScript 作用域基础

原因是因为:1、函数声明被提升到最顶上;2、声明只进行一次,因此后面var foo='i am text'的声明会被忽略。

并且函数声明的优先级优于变量声明,所以以下形式的输出,同样是函数内容:

JavaScript 作用域基础

核心总结与注意事项

总结

要彻底理解 JS 的作用域和 Hoisting,只要记住以下三点即可:

1、所有声明都会被提升到作用域的最顶上

2、同一个变量声明只进行一次,并且因此其他声明都会被忽略

3、函数声明的优先级优于变量声明,且函数声明会连带定义一起被提升

注意:

通过 with 语句,可以临时改变运行期上下文的作用域链,此时的对非 var 定义的变量进行访问,会首先访问 with 中对象的属性,然后才会向上顺着作用域链向上检查该属性。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能有所帮助,如果有疑问大家可以留言交流。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章