引言
在编程里面,闭包是一个很强大的概念。它允许开发者创建能够访问并操作其创建环境中变量的函数。闭包帮助我们写出更加高效,灵活的代码。还能让开发者更好地掌握变量和函数的作用域。
什么是闭包?
-
定义闭包:
闭包是一个函数与该函数可以访问的词法环境的组合。
闭包其实简而言之来说就是内部的函数可以调用外部函数的变量(即使外部函数执行完毕,它所适用的局部变量会被销毁。)
-
词法环境和作用域链的概念:
对于大多数现代编程语言来说,每个函数都有自己的作用域,其中包含它可以直接访问的变量集合。词法环境就是函数创建的时候作用域组成起来的。当一个函数作为另外一个函数的内部函数的时候,内部函数会继承外部函数的作用域,形成了一个作用域链。这个链使得内部函数可以访问外部函数的局部变量。即使外层函数已经执行完毕,这些内部函数仍然保留着对外部变量的引用。
-
示例:
function closure(params) { return function innerClosure(param) { console.log("closure:", params); console.log("innerClosure:", param); } } // 闭包的调用 const fn = closure("hello"); fn("world");
在这个例子里面,首先我们传入外层函数的变量为
hello
,接着 外层函数执行结束,并将innerClosure
赋值给fn
再执行fn
函数,传入变量world
给内部变量,最后我们发现打印这就代表了
fn
就是一个闭包,包含了closure
和innerClosure
两个函数的词法环境,因此能够记住外层的param
为hello
。
闭包的工作原理
-
变量的作用域和生命周期:
对于作用域来说,变量的作用域控制了变量可被访问和操作的范围。在不同的作用域中声明的变量只能在那个作用域内被访问。
对于生命周期来说,一般变量的销毁随着函数的运行结束而销毁。当然在我们的闭包中,外部函数即使销毁,内部函数会得到外部函数的变量的引用。
-
内部函数如何访问外部函数的局部变量:
闭包之所以能访问外部函数的局部变量,是因为它保留了对该变量的引用,并将其作为自身词法环境的一部分,所以这也就是为什么内部函数可以访问和修改外部变量。
-
示例:
function createCounter() { let count = 0; // 外部函数的局部变量 return function () { // 内部函数 count += 1; console.log(count); }; } const counter = createCounter(); // 创建闭包 counter(); // 输出 1 counter(); // 输出 2 counter(); // 输出 3
在这个例子里面,
createCounter
函数返回一个内部函数,这个内部函数可以访问并修改createCounter
中的局部变量count
。每次调用counter()
,它都会增加count
的值并输出,即使createCounter
函数早已执行完毕。
闭包的使用场景
-
回调函数:
在异步编程中使用闭包保持状态。
比如说现在有一个
table
循环展示的时候,它的操作里面有按钮,在原生的JavaScript
之中,如果没有使用闭包,其中所有的事件处理器都引用同一个变量,导致所有处理器在执行时都打印最后一个按钮的索引,因为该变量在循环结束后保留了最后的值。在使用闭包的时候,每个按钮都有一个唯一的索引,当按钮被点击时,闭包确保了正确的索引被记录下来,即使事件处理器在所有按钮上都是相同的函数实例。
-
私有变量和方法:
通过闭包实现模块模式,保护变量不被外部直接访问。
// 私有变量和方法 const myMoudle = (function () { let privateVariable = 0; function privateMethod() { console.log('这是一个私有方法'); console.log(privateVariable); } return { publicMethod: privateMethod } })(); myMoudle.publicMethod(); // 输出 '这是一个私有方法' console.log(myModule.privateVariable); // 报错,无法访问私有变量
通过闭包实现私有化的方法,这里我们设计了一个
(function () {...})();
—— 这个函数表达式后面跟着一对圆括号,表示这个函数立即执行。为什么要这么做呢?可以立即准备好供外部代码使用,无需等待或显式调用。 -
迭代器和生成器:
使用闭包保存状态信息。
关于迭代器和生成器后面我会专门写一篇博客来玩转这两个概念。
-
装饰器和高阶函数:
利用闭包增强或修改函数的行为。
// 装饰器和高阶函数 function addDecorator(decorator) { return function (...args) { console.log(`函数:${decorator.name}, 参数:`, args); return decorator(...args); } } const add = function (a, b) { return a + b; }; const addWithLogging = addDecorator(add); addWithLogging(1, 2); // 输出 '函数:add, 参数: [1, 2]'
闭包作为实现模块模式的基础,使得代码变得更加健壮,更加模块化。
实战案例分析
-
避免全局变量污染:
其实就是上面代码中的私有变量和方法,定义的局部变量不会成为全局变量,只通过对外开放的接口来对定义在函数内部的局部变量进行修改,这样就不会污染全局作用域了。
-
实现数据封装:
闭包也可以用来实现数据封装,保护变量不被外部代码直接访问或修改,这有助于代码的安全性和稳定性。下面是一个简单的例子:
function BankAccount(initialBalance) { let balance = initialBalance; this.deposit = function (amount) { balance += amount; }; this.withdraw = function (amount) { if (amount <= balance) { balance -= amount; } }; this.getBalance = function () { return balance; }; } const account = new BankAccount(100); account.deposit(50); account.withdraw(20); console.log(account.getBalance()); // 输出 130
闭包的陷阱和常见问题
-
内存泄漏问题:
其实这个问题我们自然而然也能够想到,因为我们闭包的内部函数会使用到外部变量,持有外部函数的变量的引用,即使外部函数已经执行完毕。如果闭包中的函数很少被调用,或者引用的变量非常大(如大型数组或
DOM
节点),那么这些变量可能会占用不必要的内存空间。 -
闭包与垃圾回收机制的关系:
垃圾回收机制负责自动释放不再使用的内存。然而,当一个闭包中的函数引用外部变量时,只要这个函数还存在,外部变量就不会被垃圾回收器回收。因此,要避免内存泄漏,就需要确保闭包中的函数最终会被销毁,或者在不再需要外部变量时,手动解除引用。
一般手动解除引用也很简单直接设置为
null
即可。
最佳实践
-
明确意图:在你使用闭包的时候,应该用于封装状态和实现私有变量,而不是仅仅为了使用而使用。
-
限制作用域:尽量限制闭包的范围。不要让闭包持有不必要的全局或长生命周期的引用,尤其是大对象或DOM元素,这可能会导致内存泄漏。
-
避免循环中创建闭包:在循环中创建闭包时要小心,因为这可能导致所有闭包共享相同的外部变量引用。例如,在
JavaScript
中,使用IIFE
(立即执行函数表达式)来创建独立的闭包,以避免“闭包陷阱”。举例:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); // 这里期望输出0到9,但实际上输出10十次 }, 100 * i); }
优化过后:
for (var i = 0; i < 10; i++) { (function(i) { setTimeout(function() { console.log(i); // 正确地输出0到9 }, 100 * i); })(i); }
每个
setTimeout
回调函数都有其自己的i
值,这个值在IIFE
执行时被立即捕获,从而避免了闭包陷阱。
结论
-
上面我介绍了闭包的基本概念,闭包是什么,闭包的工作流程。
-
其实闭包在我们日常的开发之中是非常强大的,包括封装变量和函数,以及实现私有变量和方法,保护内部的状态不被外部代码直接访问。
-
闭包可以用于保存状态信息,确保在回调函数或后续调用中可以访问到这些状态。
-
闭包是实现高阶函数和装饰器模式的关键,可以用来增强或修改函数的行为,而无需修改原函数的代码。
-
当然我们在使用闭包的时候也需要注意一些细节:
- 避免闭包陷阱:在循环中创建闭包时要特别小心,确保每个闭包都有其独立的作用域,避免所有闭包共享相同变量引用的问题。
- 性能考量:闭包的使用可能会影响性能,特别是在大规模应用中,过多的闭包可能导致内存消耗增大,应适度使用。
- 内存管理:理解闭包如何影响垃圾回收机制,避免不必要的变量引用,及时解除不再需要的引用,防止内存泄漏。
所以我们要学会闭包,掌握闭包,运用闭包。
希望对您的学习有帮助,有什么问题欢迎大家一起交流!