数字营销 · Web开发 · 基础设施

JavaScript 闭包及其应用

从函数作用域出发,通过实际代码理解 JavaScript 闭包的形成原理,以及它在状态保存和数据封装中的应用。

JavaScript 闭包是什么?

关于闭包,经常能看到一些比较抽象的解释,例如“函数与其词法环境的组合”。这句话本身没有错,但对于刚开始学习 JavaScript 的人来说,往往不太容易理解。

相比直接记住定义,我更喜欢通过一个简单的问题来理解:

为什么一个函数已经执行结束了,它内部的变量却还能继续被使用?

理解了这个问题,基本也就理解了闭包。

函数内部的变量,外部无法直接访问

首先来看一个简单的例子:

functiona() {
vari=0;
}

a();

console.log(i);

执行后会报错:

ReferenceError: i is not defined

这是因为 i 是函数 a 内部定义的局部变量,只能在 a 的作用域中访问。

函数外部无法直接访问它:

functiona() {
vari=0;
console.log(i); // 正常
}

a();

// 函数外部无法访问
console.log(i);

那么,如果函数内部还有另外一个函数,而这个内部函数访问了外部函数的变量,会发生什么?

函数执行结束后,变量为什么还存在?

来看下面这个例子:

functiona() {
vari=0;

functionb() {
i++;
console.log(i);
}

returnb;
}

varres=a();

res(); // 1
res(); // 2
res(); // 3

这里发生了一件很有意思的事情。

正常理解的话,a() 执行结束后,函数内部的变量 i 应该无法再直接访问了。

但实际上:

res(); // 1
res(); // 2
res(); // 3

每次调用 res(),变量 i 都会继续累加。

原因就在于,a() 返回了内部函数 b:

return b;

然后:

varres=a();

变量 res 保存了这个函数。

虽然外层函数 a() 已经执行结束,但内部函数 b 仍然需要访问 a() 中的变量 i。

因此,i 所在的外部词法环境仍然会被保留。

可以简单理解为:

res
内部函数 b
继续访问外部变量 i

所以每次执行:

res();

操作的都是同一个 i:

第一次:i = 1
第二次:i = 2
第三次:i = 3

这就是闭包最典型的表现之一。

那么,闭包到底是什么?

简单来说:

当一个函数能够访问并持续使用其外部作用域中的变量时,就形成了闭包。

重点并不只是“函数里面还有一个函数”,而是:

内部函数仍然引用了外部作用域中的变量。

即使外部函数已经执行结束,只要内部函数还存在,并且仍然需要访问这些变量,那么这些变量对应的词法环境就会继续保留。

例如前面的:

functiona() {
vari=0;

returnfunction () {
i++;
returni;
};
}

返回的内部函数始终引用着 i,因此 i 可以在多次调用之间保存自己的状态。


闭包有什么用?

看到这里可能会有一个问题:

既然只是保存一个变量,直接使用全局变量不也可以吗?

例如:

var count=0;

function add() {
count++;
return count;
}

当然可以。

但全局变量的问题是,任何地方都可能直接修改它:

count=100;

当项目越来越复杂时,这种变量很容易被意外修改。

闭包的一个重要作用,就是:

把数据保存在一个相对独立的作用域中,只通过指定的方法访问。

使用闭包保存状态

例如一个简单的计数器:

function createCounter() {
varcount=0;

return function () {
count++;
return count;
};
}

var counter=createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

这里外部无法直接访问:

count

只能通过返回的函数:

counter()

对它进行操作。

而且,每次创建的计数器都是独立的:

varcounterA=createCounter();
varcounterB=createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2

console.log(counterB()); // 1
console.log(counterB()); // 2

counterA 和 counterB 分别拥有自己的状态,互不影响。


使用闭包实现数据封装

再来看一个对象的例子。

如果直接这样定义:

varobj= {
name: "Hito",

say: function () {
console.log("My name is "+this.name);
}
};

那么对象属性可以被外部直接修改:

obj.name ="Jack";
obj.say(); // My name is Jack

有时候我们并不希望外部可以随意修改某些数据。

可以使用闭包把变量保存在内部:

var obj= (function () {
var name="Hito";

return {
say: function () {
console.log("My name is "+name);
},

getName: function () {
returnname;
},

setName: function (newName) {
name=newName;
}
};
})();

现在直接访问:

console.log(obj.name);

结果是:

undefined

因为真正的 name 并不是 obj 的公开属性,而是保存在函数内部:

varname="Hito";

外部只能通过提供的方法访问:

console.log(obj.getName()); // Hito
obj.setName("Jack");
console.log(obj.getName()); // Jack

这就是闭包在数据封装和私有状态中的一种应用。

虽然现在 JavaScript 已经有了 class 私有字段等新的实现方式,但闭包依然是一种非常重要的机制。


立即执行函数与闭包

早期 JavaScript 中,立即执行函数(IIFE)非常常见:

(function () {
var title=document.title;
var suffix=" - 我的博客";

document.title =title+suffix;
})();

这种写法的主要作用之一,是创建一个独立的作用域,避免变量直接污染全局环境。

例如:

(function () {
var message="Hello";
})();
函数外部无法直接访问:
console.log(message);

这里需要注意一点:

立即执行函数本身并不等于闭包。

IIFE 的作用主要是创建独立作用域。

是否形成闭包,关键仍然在于函数是否引用了外部作用域中的变量,并且这个引用关系持续存在。

例如:

functionouter() {
var name="Hito";

return function () {
return name;
};
}

这里返回的内部函数持续访问 name,这才是一个典型的闭包场景。


现代 JavaScript 中,闭包依然无处不在

虽然现在 JavaScript 已经有:

  • let
  • const
  • ES Module
  • class
  • 私有字段

很多早期依赖闭包实现的模式已经有了新的方式,但闭包本身依然是 JavaScript 非常核心的机制。

在实际开发中,我们经常会在这些场景中遇到闭包:

  • 回调函数
  • 事件处理
  • 定时器
  • Promise
  • 模块化
  • 函数工厂
  • 状态保存
  • React Hooks

例如一个事件处理函数,往往仍然能够访问它定义时所在作用域中的变量。

这也是理解 JavaScript 时非常重要的一点:

函数在哪里定义,就决定了它能够访问哪些变量。


总结

闭包并不是什么神秘的 JavaScript 特性。

可以简单理解为:

一个函数引用了外部作用域中的变量,并且这个函数在外部作用域结束后仍然可以继续访问这些变量。

通过闭包,我们可以:

  • 保存函数内部状态
  • 避免全局变量污染
  • 实现数据封装
  • 创建多个互不影响的独立状态
  • 在回调和异步操作中访问外部变量

理解闭包,不只是为了回答一道 JavaScript 面试题。

更重要的是理解 JavaScript 的作用域和词法环境,理解一个函数为什么能够访问某个变量,以及这个变量为什么会在函数执行结束后仍然存在。

一旦理解了这一点,再去看回调函数、事件、异步操作甚至现代前端框架中的一些机制,也会更容易理解。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。