发布于 2026-01-06 1 阅读
0

JS 中的闭包及其重要性 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

JavaScript 中的闭包及其重要性

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

开发者编写 JavaScript 代码时,其中一个主要特性或许是他们最不了解的。这可能是因为没有人会在编写代码时直接思考或意识到,代码不出错的原因恰恰与这个特性有关。

但这个功能究竟是什么呢?

嗯……这其实算不上一个特性。它是 JavaScript 构建方式以及它“编译”、运行和执行方式的副作用。让我们通过一个例子来深入了解一下。

在浏览器开发者工具中运行以下命令将导致

var age = 14;

function getOlder() {
  var age = 14;
  age++;
};

getOlder();

console.log(`I am ${age} years old.`); // <-- ???
Enter fullscreen mode Exit fullscreen mode
  1. 它坏了(🤷)
  2. 打印I am 14 years old.
  3. 打印I am 15 years old.

正确答案是2I am 14 years old.!但是为什么呢?

解释执行过程

关于抽象语法树 (AST) 以及 JavaScript 的设计理念,有很多重要的信息,本文不会深入探讨,但读者可以这样理解(请查阅参考文献!):

当浏览器内部运行的虚拟机(例如 Chrome 中的 V8)执行代码时,它会解析每个变量的名称。解析变量的过程是必要的,这样在使用已声明和定义的变量时就不会破坏代码。如果代码尝试访问尚未正确定义的函数或变量,它将输出著名的错误信息:

Uncaught ReferenceError: yourVariable is not defined

照片由 Unsplash 上的 Mark de Rooij 拍摄

照片由 Unsplash 上的 Mark de Rooij 拍摄

手动解析变量

如果命名解析后的结果可访问,则原始代码将转换为大致类似于以下内容:

var global__age = 14;

function global__getOlder() {
  var getOlder__age = 14;
  getOlder__age++;
};

global__getOlder();

console.log(`I am ${global_age} years old.`); // --> 'I am 14 years old.'
Enter fullscreen mode Exit fullscreen mode

现在输出结果就说得通了,对吧?这个前缀的添加与命名解析时每个变量和方法的闭包I am 14 years old.有关。可以看出,这段代码中有两个闭包:

  • global
  • getOlder

可以看出,getOlder闭包位于global闭包内部,但原始函数内部的变量getOlder()只能在这些括号内访问。

getOlder__age因此,说变量只存在于函数内部更有意义global__getOlder()。一个很好的验证例子是尝试在函数外部记录该变量的值:

var global__age = 14;

function global__getOlder() {
  var getOlder__age = 14;
  getOlder__age++;
};

global__getOlder();

console.log(`I am ${getOlder__age} years old.`); // --> Error!
Enter fullscreen mode Exit fullscreen mode

结果输出为Uncaught ReferenceError: getOlder__age is not defined,原因是没有变量的名称解析为globalClosure 有效getOlder__age

那么,Scopes乐队呢?

在创建函数时,闭包的创建方式与作用域相同。闭包内的所有变量和函数都可以被所有子函数访问,但不能被闭包外的函数访问(除非它们像后面将要讨论的那样被暴露出来)。

作用域闭包几乎是等同的,但后者有一些“超能力”:在闭包内部创建并暴露出来的变量和函数,即使没有作用域,在闭包外部仍然可以正常工作。这两个概念之间的界限非常模糊。

即使这些暴露出来的项依赖于闭包内部的其他变量/函数,但这些变量/函数本身并未暴露出来,这个结论仍然成立。

闭合与内窥镜

以下代码与上述示例几乎相同,仅作少量修改,旨在解释这两个概念之间的区别。

function main() {
  var age = 14;

  function getOlder() {
    age++;

    console.log(`I am ${age} years old now.`); // --> 'I am 15 years old.'
  };

  getOlder();
};

main();
Enter fullscreen mode Exit fullscreen mode

在这个例子中,函数getOlder()会在另一个函数内部被调用main(),并且会打印出I am 15 years old now.结果,对吗?变量age位于main函数的作用域内,可以被函数访问getOlder()

如果将该函数返回getOlder()到外部“世界”,并像以下示例一样执行 3 次,结果会是什么?

function main() {
  var age = 14;

  function getOlder() {
    age++;

    console.log(`I am ${age} years old now.`); // <-- ???
  };

  return getOlder;
};

var getOlder = main();

getOlder(); // <-- ???
getOlder(); // <-- ???
getOlder(); // <-- ???
Enter fullscreen mode Exit fullscreen mode
  1. 什么都没有。代码会出错。
  2. 3次I am 15 years old now.
  3. 变量的值age仍将从15增加到16,然后再增加到17

正确答案是答案 3

但为什么会发生这种情况呢?

每次创建闭包时,所有变量和函数都会存储在其状态中。即使函数执行完毕main(),相应的闭包 状态仍然存在,并继续存储着变量和函数!

或许最神奇的地方在于:这个age变量被困在main() 闭包内部,无法在外部访问!如果下一部分代码尝试访问该age变量,就会出现前面提到的Uncaught ReferenceError: age is not defined错误,因为该变量在函数外部并不存在main()

照片由 Maria Teneva 拍摄,来自 Unsplash

照片由 Maria Teneva 拍摄,来自 Unsplash

包起来

文中讨论了封闭性作用域概念之间的一些重要区别:

  • 闭包总是存储关于其变量和函数的状态
  • 可以通过在创建闭包的函数末尾返回这些变量/函数,来公开部分、全部或不公开任何这些变量/函数。
  • 甚至可以在闭包内部重新定义一些同名的外部变量/函数,虚拟机编译器会自动处理,避免运行时错误和名称冲突。

这篇文章对您有用吗?我在解释过程中是否遗漏了什么?请在评论区留言或私信告诉我!

参考

文章来源:https://dev.to/caiangums/closures-in-js-and-why-it-matters-pb9