JavaScript闭包本质上是邪恶的吗?
JavaScript闭包经常被使用,但常常被误解。深入理解闭包对于编写干净、可维护且无bug的代码至关重要。我们之前讨论过它们是什么以及它们如何工作。
我强烈建议您阅读关于闭包的先前文章,如果您还没有阅读过的话。我不想重复相同的信息,而是想讨论使用闭包的危险并提出我的观点。
隐藏状态
反对闭包的主要论点是隐藏状态。隐藏状态指的是隐藏对象或函数的状态。论点是内部可变状态可能会导致不可预测的行为和意外的结果。因此,人们常常说隐藏状态是编程中一切邪恶的根源。
虽然这个论点本身有一定的道理,但我不太喜欢这种概括。有一些完全合理的情况下隐藏状态是可以预期的,甚至是必要的。然而,隐藏状态确实可能会导致bug和难以维护的代码。
闭包导致的隐藏状态的一个例子是我在原始介绍中提到的例子:
const initCounter = (start = 0) => {
let value = start;
return {
get: () => value,
increment: () => ++value,
decrement: () => --value,
reset: () => value = start
};
}
const counter = initCounter(5);
counter.get(); // 5
counter.increment(); // 6
counter.increment(); // 7
counter.decrement(); // 6
counter.reset(); // 5
在这种情况下,initCounter
函数返回一个包含隐藏可变状态的对象,该状态以value
变量的形式存在。显然,这是一个非常简单的例子,但是单独使用counter.get()
或counter.increment()
将被视为非确定性表达式。在没有分析周围代码的情况下,无法知道这样的方法调用的结果。
尽管这并不罕见,但当涉及到共享状态或多个代码片段相互交互时,情况可能会变得更加复杂。解决这个问题的常见方法是使用函数式编程,将隐藏的可变状态重构为参数或共享全局变量。
访问上下文
并非所有闭包都是相等的。事实上,闭包有一些完全合理的用例,可以让生活变得更加轻松。例如,访问共享常量应该被认为是相当安全的。毕竟,如果你想要真正纯粹的函数,甚至不应该访问全局变量和Web API。这样做可能相当不实际,因为你将不得不将每个全局变量和API作为参数传递给函数。
尽管相对安全,但重要的是确保在使用常量之前进行初始化,并在没有初始化的情况下显式抛出错误。此外,适当的文档化这些闭包将最小化摩擦,并确保其他开发人员理解发生了什么。最后,如果可能的话,应该考虑提供一个逃生通道,通常以可以覆盖的默认参数的形式。
以下是一个简单的随机数生成器的示例,遵循这些规则:
const randomNumber = (limit = 100, random = Math.random) => random() * limit;
randomNumber(10); // 4.0
randomNumber(10, () => 0.2); // 2.0
这些做法的另一个好处是编写测试更加容易,因为在任何给定时间,我们都不会困惑于需要模拟什么。在这个示例中,我们可以轻松地用任何我们想要的函数替换Math.random()
并知道结果值。
结论
闭包本身只是另一个你需要理解的语言特性。作为一个经验法则,要谨慎使用它们,明确你的代码意图,并提供逃生通道以减少潜在的错误面。
当正确使用时,它们可以成为你工具库中的另一个工具。然而,如果使用不当,它们可能会导致一些非常严重的错误或难以维护的代码。需要时间来适应它们,并能够在问题出现之前识别出反模式。