JavaScript深入 — 闭包
- 一、概念
- 二、示例
- 三、实用的闭包
- 四、用闭包模拟私有方法
- 五、一个常见错误:在循环中创建闭包
- 🌰 另一个经典例子-定时器与闭包
- 六、优劣
- 好处
- 坏处
- 解决
- 七、图解闭包
- 八、应用 💪
- 封装私有变量
- 函数工厂
- 异步操作中的回调函数
- 柯里化(封装函数)
- 引申
一、概念
闭包简单来说就是引用了另一个函数作用域中变量的函数。
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在JavaScript中,闭包会随着函数的创建而被同时创建
二、示例
//函数作为返回值
function test() {
const a = 1;
return function() {
console.log('a: ',a);
}
}
const fn = test();
const a = 2;
fn();// a: 1
通俗来说,a
这个自由变量查找的规则,它会在函数定义的地方去向上一层查找它的值,而不会在函数执行的地方向上一层去查找
//函数作为参数
function test(fn) {
const a = 1;
fn();
}
const a = 2;
function fn() {
console.log('a:',a);
}
test(fn);//a: 2
依然是上述通俗的定义,在函数定义处查找变量a的值
三、实用的闭包
🤔 假如,我们想在页面上添加一些可以调整字号的按钮。
function makeSizer(size){
return function() {
document.body.style.fontSize = size + 'px';
}
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;
从本质上将,makeSizer
是一个函数工厂,他创建了将字体调整至指定大小的函数,上面的示例中,我们使用函数工厂创建了三个新函数,分别将字体大小调制12、14、16px
size12
、size14
、size16
都是闭包,它们共享相同的函数定义,但是保存了不同的词法环境,在size12
中,size
为12,其他同理。
四、用闭包模拟私有方法
Java支持将方法声明为私有,即它们只能被同一个类中的其他方法调用,而JavaScript没有这种原生支持,但我们可以使用闭包来模拟私有方法。
我们定义了一个匿名函数,用于创建一个计数器,我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另一个变量makerCounter中,并用他来创建多个计数器
var Counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
}
}
})();
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1
在之前的示例中,每个闭包都有它自己的词法环境,而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment
,Counter.decrement
,Counter.value
上面使用了立即执行函数表达式(IIFE)的相关内容,使用立即执行函数表达式好处有下
- 避免变量污染(命名冲突),例如不同的第三方库恰好使用了同一个变量名称
- 隔离作用域
- 提高性能(减少对作用域的查找)
(在ES6前,JS原生并没有块级作用域的概念,所以IIFE可以用函数作用域来模拟块级作用域)
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
五、一个常见错误:在循环中创建闭包
在ECMAScript 2015引入 let
关键字之前,在循环中有一个常见的闭包创建问题
function showHelp(help) {
document.getElementById("help").innerHTML = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
}
}
setupHelp();
这里赋值给 onfocus
的是闭包,在循环中创建了三个闭包,但它们共享了一个词法作用域,在这个作用域中存在一个变量 item
。因为变量 item
使用var
进行声明,由于变量提升,所以具有函数作用域。当 onfocus
的回调执行时,由于循环早已执行完毕,item
已经指向了 helpText
有了 let
就不会有这样的问题(块级作用域)
🌰 另一个经典例子-定时器与闭包
for(var i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i + '');
},100)
}
按照预期它应该依次输出 1 2 3 4 5
,而结果它输出了五次5,同上理,在 setTimeout
的回调函数开始在 Callback Queue
中依次执行时,循环早执行解释,i的值为5,而这回调函数的五个闭包共享一个词法作用域
使用 let
关键字,按照预期依次输出 1 2 3 4 5
for(let i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i + '');
},100)
}
不用 let
关键字修改如下
for(var i = 1; i <= 5; i++){
(function(number) {
setTimeout(function(){
console.log(number);
},100)
})(i)
}
六、优劣
好处
- 保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突,避免全局变量污染
- 在内存中维持一个变量,可以做缓存,延长变量的生命周期
- 匿名自执行函数可以减少内存消耗
坏处
- 内存泄漏:由于闭包中的函数引用了外部函数的变量,而外部函数的作用域在函数执行结束后并不会被销毁,这就导致了闭包函数中的变量也无法被销毁,从而占用了内存空间。如果闭包被滥用,可能会导致内存泄漏的问题。
- 性能问题:闭包中的函数访问外部函数的变量需要通过作用域链来查找,而作用域链的长度决定了查找的速度。如果闭包层数较深,作用域链就会很长,从而影响了函数的执行效率。
解决
- 及时释放闭包:如果不再需要使用闭包,可以手动将其赋值为
null
,从而释放闭包中占用的内存空间。 - 减少闭包层数:尽量减少闭包层数,避免作用域链过长,从而提高函数的执行效率。
- 使用立即执行函数:可以使用立即执行函数来避免闭包的内存泄漏问题。由于立即执行函数在执行结束后会被立即销毁,因此其中的变量也会被释放。
- 使用模块化编程:可以使用模块化编程来避免闭包的性能问题。在模块化编程中,每个模块都是一个独立的作用域,不会对全局作用域造成影响,从而避免了作用域链过长的问题。
七、图解闭包
八、应用 💪
封装私有变量
👇 闭包可以用于创建具有私有成员的对象。通过将变量放在闭包中,使之对外不可见。
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
},
getValue: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 输出 1
函数工厂
👇 如下,makeSizer
是一个函数工厂,他创建了将字体调整至指定大小的函数。
function makeSizer(size){
return function() {
document.body.style.fontSize = size + 'px';
}
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;
异步操作中的回调函数
👇 在异步编程中,回调函数通常是闭包,因为它们可以访问其定义时的上下文,这对于保存状态和数据非常有用。
function fetchData(url, callback) {
// 异步操作获取数据
setTimeout(function() {
const data = /* 获取的数据 */;
callback(data);
}, 1000);
}
fetchData('https://example.com/api', function(data) {
console.log(data);
});
setTimeout
内部延迟执行的「匿名回调函数」可以访问外部函数fetchData
的词法作用域,引用了其中变量callback
,因此形成了闭包
柯里化(封装函数)
// 多参数柯里化
const curry = function(fn){
return function curriedFn(...args){
if(args.length<fn.length){
return function(){
return curriedFn(...args.concat([...arguments]));
}
}
return fn(...args);
}
}
const fn = (x,y,z,a)=>x+y+z+a;
const myfn = curry(fn);
console.log(myfn(1)(2)(3)(1));
引申
手写一个闭包?举个闭包的例子?