- Published on
来谈谈闭包
- 闭包的概念
- 闭包解决了什么问题?
- 闭包带来的副作用是什么?
- 闭包的应用场景都有哪些?
概念
MDN 上给出的闭包的定义是:一个函数和其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包可以让你在一个内层函数中访问到外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
来看个例子:
function parent() {
var name = 'closure'; // name 是在 parent 函数里创建的处于函数作用域内的一个局部变量
function closure() { // closure 是一个内部函数,它就是闭包
alert(name); // 使用了 父函数 parent 里创建的变量 name
};
closure();
}
parent();
可以看到 parent 函数中有一个局部变量 name 和 closure 的函数。closure 仅能在 parent 函数体内使用,没有自己的局部变量,但是它可以访问外部函数的变量。
这里类似面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。而闭包则是将函数与其操作的某些数据或者说是词法环境关联起来。词法作用域根据源代码中变量声明的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
所以简单点讲,闭包可以理解为能够访问另一个函数作用域变量的函数。我们可以这样定义闭包的公式:
闭包 = 函数 + 词法环境
闭包解决了什么问题?
在 JavaScript 中,没有私有变量这一定义。所以在实际场景中,我们会经常遇到一些变量污染的问题,而这里我们正好可以利用闭包来模拟私有变量,这样的话不仅仅可以避免变量污染导致的错误,还有利于限制对代码的访问,提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
function self() {
const name = 'cecil';
return function () {
return name;
}
}
console.log(name); // ''
console.log(self()()); // 'cecil'
可以看到直接访问 name 变量是访问不到的,因为此时我们并没有定义,得到一个空字符串。只有调用闭包函数,才能读取到 name 变量。
当然闭包不仅仅只是做模拟私有变量这一点,在高阶函数中我们会常常看到闭包的身影:
function makeAdder(x) {
return function (y) {
return x + y;
}
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
上述代码是一个简单的工厂函数,在这里 add5 和 add10 都是闭包。共享相同的函数定义,但是保存了不同的词法环境,从而计算得到不同的值。(可以理解为,闭包可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰)
使用闭包给我们提供了许多与面向对象编程相关的好处,特别是数据隐藏和封装。
闭包的副作用
闭包的副作用也是显而易见的,那就是内存泄漏。每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。
所以说如果不是特定的需求需要用到闭包,平时是不推荐使用闭包的,毕竟它在处理速度和内存消耗方面对脚本性能会有负面影响,如果使用不当,就会有内存泄漏的风险。
应用场景
实际项目中,闭包的应用场景有很多,下面是经常会遇到的一些场景:
- 定时器
- 回调函数
- IIFE
- 函数防抖节流
- 函数柯里化
- 模块化
小结
- 闭包是说函数与其周围的引用捆绑在一起的组合。也就是说一个内层函数可以访问其外层函数的作用域;
- 模拟私有变量,防止全局变量污染
- 局部变量会常驻内存,在内存中维护这个变量
- 会造成内存泄漏(内存长期被占用,而不被释放)
- 闭包找到的是同一地址中父级函数里对应变量的最终值
- 高阶函数中使用
- 闭包函数可以理解为携带状态的函数