理解 JavaScript 作用域与变量提升(二)

作者:闭包旅人

时间:2026-10-29

浏览:466

本文深入解析 JavaScript 中的变量提升(Hoisting)机制,解释为何变量声明会被提升至作用域顶部。通过对比函数声明与函数表达式,阐明命名提升与定义分离的差异,并提供利用立即执行函数(IIFE)创建块级作用域以规避提升陷阱的实用技巧。

文章目录

JavaScript 变量提升与函数作用域详解

Scoping & Hoisting


var a = 1;

function foo() {
  if (!a) {
    var a = 2;
  }
  alert(a);
};

foo();

上述代码在执行时会输出什么结果?

虽然这对资深开发者而言轻而易举,但为了帮助初学者理清思路,我们不妨顺着常见的直觉进行推演:

1. 首先创建全局变量 a,并将其初始值设为 1。 2. 接着定义函数 foo。 3. 进入 foo 函数体内部,由于变量 a 存在,表达式 !a 会将其转换为布尔类型的 false。因此,if 语句的条件不成立,其内部代码块不会执行。 4. 程序跳过条件分支,执行 alert 输出变量 a 的值,最终结果预期为输出 1。

看似逻辑严密的推导,结果却令人意外:实际输出竟然是 2!这是为什么呢?

无需惊讶,这并非程序错误,而是 JavaScript 解释器的一种(非官方标准)行为特性。Ben Cherry 将其命名为:Hoisting(中文常译作“提升”)。

声明与定义的区别

声明与定义

为了透彻理解 Hoisting,我们先观察一个基础场景:

var a = 1;

你是否思考过,这行代码在运行时究竟发生了什么? 你是否清楚,对于这行代码,“声明变量 a”与“定义变量 a”这两个概念中,哪一个描述更为准确? • 以下代码示例被称为“声明变量”:

var a;

• 以下代码示例被称为“定义变量”:

var a = 1;

• 声明(Declaration):指告知解释器某个变量或函数存在,但不提供具体实现细节,仅通知解释器该标识符已注册。 • 定义(Definition):指明具体的实现内容,例如变量的具体数值或函数的代码体,确切地表达了该实体的意义。

总结如下:

var a; // 这是声明 a = 1; // 这是定义(赋值) var a = 1; // 合二为一:声明变量的存在并赋值给它

关键点在于:当你认为只执行了一件事(var a = 1)时,解释器实际上将其拆解为两个步骤:首先是声明(var a),其次是定义(a = 1)。

这与 Hoisting 有何关联?

回到最初那个令人困惑的例子,解释器处理代码的逻辑如下:


var a;
a = 1;

function foo() {
  var a;    // 关键在这里
  if (!a) {
    a = 2;
  }
  alert(a);   // 此时的 a 并非函数体外的那个全局变量
}

如代码所示,进入函数体后,解释器首先声明了局部变量 a。无论 if 语句的条件如何判断,都会为这个局部变量 a 赋值为 2。若存疑,可在函数体外 alert(a),再执行 foo() 对比结果即可验证。

函数作用域与 Hoisting 机制

Scoping(作用域)

有人可能会问:“为什么变量 a 不是在 if 语句块内声明的呢?”

因为 JavaScript 不具备块级作用域(Block Scoping),仅具备函数作用域(Function Scoping)。这意味着并非看到一对花括号 {} 就代表生成了新的作用域,这与 C 语言不同。

当解析器遇到 if 语句时,发现其中包含变量声明,于是将该声明提升至当前作用域的顶部(这是默认且不可更改的行为),这种行为即称为 Hoisting。

OK,概念大家都明白了,你真正理解了吗……

理解不等于掌握。以最初的例子为例,如果我希望 alert(a) 输出那个 1 该怎么办?

利用 IIFE 创建局部作用域

创建新的作用域

当执行 alert(a) 时,它会从当前作用域开始向上(向外)查找变量 a,直至顶层作用域。若找不到则返回 undefined。

由于在 alert(a) 的同级作用域中重新声明了局部变量 a,导致输出 2。因此,我们可以将局部变量 a 的声明向下(向内)移动,使得 alert(a) 无法访问到它。

记住:JavaScript 只有函数作用域!


var a = 1;

function foo() {
  if (!a) {
    (function() {    // 这是上一篇说到过的 IIFE,它会创建一个新的函数作用域
      var a = 2;    // 并且该作用域在 foo() 的内部,所以 alert 访问不到
    }());        // 不过这个作用域可以访问上层作用域哦,这就叫:“闭包”
  };
  alert(a);
};

foo();

作用域中命名的优先级

你可能在无数 JavaScript 书籍和文章中读到:“请始终保持作用域内所有变量的声明放置在作用域的顶部”。现在你应明白此建议的原因:这样可以避免 Hoisting 特性带来的困扰(尽管 Hoisting 本身并无过错),并能明确告知代码阅读者(包括你自己)当前作用域内可访问的变量。然而,变量声明的提升并非 Hoisting 的全部。在 JavaScript 中,有四种方式使命名进入作用域(按优先级排列):

1. 语言定义的命名:如 this 或 arguments,它们在所有作用域内有效且优先级最高,因此严禁将变量命名为 this 等保留字,否则无意义。 2. 形式参数:函数定义时声明的形式参数会作为变量被 hoisting 至该函数的作用域内。因此形式参数属于本地变量,而非外部或全局变量。当然,执行函数时可传入外部变量,但传入后即变为本地变量。 3. 函数声明:函数体内部可声明函数,但它们同样属于本地作用域。 4. 变量声明:优先级最低,但最为常用。

另外,回顾之前讨论过的“声明”与“定义”的区别?当时未解释其原因,现在揭晓:

Hoisting 只提升了命名,没有提升定义

这一点与接下来要讲的内容息息相关,请看:

函数声明与函数表达式的差异

函数声明与函数表达式的差别

先看两个例子:


function test() {
  foo();

  function foo() {
    alert("我是会出现的啦……");
  }
}

test();

function test() {
  foo();

  var foo = function() {
    alert("我不会出现的哦……");
  }
}

test();

在了解了 Scoping & Hoisting 之后,你能解释这一切了吗?

在第一个例子中,函数 foo 是一个声明。既然是声明,它就会被提升(此处特意包裹了一个外层作用域,因为全局作用域需要更多想象,不如局部直观,但原理相同)。因此在执行 foo() 之前,作用域已知晓函数 foo 的存在。这称为函数声明(Function Declaration),函数声明会将命名和函数体一同提升至作用域顶部。

而在第二个例子中,被提升的仅仅是变量名 foo,其定义仍停留在原处。因此在执行 foo() 之前,作用域仅知道 foo 的命名,不知其具体类型,故执行会报错(通常为:undefined is not a function)。这称为函数表达式(Function Expression),函数表达式仅提升命名,不提升函数体定义。

尾记:Ben Cherry 的原文解释更为详尽,但为英文。本文借花献佛,旨在为初学者提供更浅显的解释。若需查看更多示例,请移步原作,谢谢。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章