经典问题闭包

闭包是 JavaScript 中一个非常经典且重要的概念,它不仅是理解 JavaScript 中作用域和执行上下文的关键,也是许多高级功能和技巧的基础。但是很多人对其也是一知半解,网上关于闭包的定义更是五花八门,很多人都说函数套函数就是闭包,一问来源就是阮一峰是这么说的…我特地去了大佬的博客查了下,他确实这么说了,只是大佬说的更长,闭包不仅仅是函数嵌套。

MDN 对闭包的解释:闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。一如既然的抽象,很 MDN。

我对闭包的理解:闭包是指在 JavaScript 中,函数与其词法作用域的结合体,使得函数能够捕获并持久化其定义时所处的作用域中的变量。这种机制允许函数在执行时访问那些本应在其执行完毕后销毁的变量,从而实现状态的保持。

闭包的形成条件

  1. 函数嵌套:一个函数内部定义了另一个函数。
  2. 内部函数引用外部函数的变量:内部函数使用了外部函数的局部变量。
  3. 外部函数返回内部函数:外部函数将内部函数作为返回值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}

const increment = outer();
increment(); // 输出 1
increment(); // 输出 2
increment(); // 输出 3

在这个例子中,inner函数形成了闭包,能够访问并修改outer函数中的私有变量count

闭包常见用途

  1. 数据封装与私有变量
    闭包可以用于创建私有变量,防止外部直接访问和修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
console.log(count);
},
decrement: function () {
count--;
console.log(count);
},
};
}

const counter = createCounter();
counter.increment(); // 输出 1
counter.increment(); // 输出 2
counter.decrement(); // 输出 1

这里,count变量对外部是私有的,只能通过incrementdecrement方法访问。

  1. 回调函数
    闭包常用于回调函数,尤其是在异步操作中。
1
2
3
4
5
6
7
8
9
10
11
function fetchData(url, callback) {
fetch(url)
.then((response) => response.json())
.then((data) => callback(data));
}

function processData(data) {
console.log("Processing:", data);
}

fetchData("https://api.wyxup.top/data", processData);

在这个例子中,processData函数作为回调函数传递给fetchData,并在数据获取后被调用。

  1. 函数柯里化
    闭包可以用于函数柯里化,即将多参数函数转换为一系列单参数函数。
1
2
3
4
5
6
7
8
// 柯里化函数示例
function multiply(a) {
return function (b) {
return a * b;
};
}
const multiplyBy2 = multiply(2);
console.log(multiplyBy2(3)); // 输出 6

闭包的常见问题

内存泄漏:闭包会导致内存泄漏问题,因为闭包会“记住”外部函数的变量,如果不小心管理这些变量的引用,会导致不再使用的变量无法被垃圾回收,从而占用内存。
变量的作用域问题:尤其在使用循环和回调函数时,闭包容易引发意料之外的结果。例如,使用 setTimeout 在循环中创建多个回调时,容易因为闭包捕获的是循环中的变量引用,而非循环时变量的值,导致所有的回调使用相同的最终值。

1
2
3
4
5
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 3 次 3
}, 1000);
}

为了解决这个问题,可以使用 let 替代 var,因为 let 是块级作用域,会在每次循环中生成新的作用域。

1
2
3
4
5
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 0, 1, 2
}, 1000);
}

闭包的优缺点

优点:

  • 数据封装:可以创建私有变量,防止外部访问和修改。
  • 灵活性:可以在函数外部访问函数内部的变量。

缺点:

  • 内存消耗:闭包会保留对外部函数作用域的引用,可能导致内存泄漏。
  • 性能影响:频繁使用闭包可能影响性能,尤其是在内存有限的环境中。

Vue 中使用闭包的场景:

1.事件处理和回调函数

闭包常常在事件处理器和回调函数中使用,尤其是在处理异步操作时。比如,在 Vue 中,我们经常需要在某些事件触发时保存和使用一些数据。

示例:点击事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<button @click="incrementCount">Increment Count</button>
<p>Count: {{ count }}</p>
</div>
</template>

<script>
export default {
data() {
return {
count: 0
};
},
methods: {
incrementCount() {
// 闭包:increment 是一个回调函数,可以访问外部的 `count` 变量
setTimeout(() => {
this.count += 1; // 闭包保留了对 `this.count` 的引用
}, 1000);
}
}
};
</script>

在上面的代码中,setTimeout 通过箭头函数(=>),使得它形成了一个闭包,能够保持对 Vue 实例(this)和其 data中的 count 变量的引用。即使异步回调函数在 incrementCount 执行完毕后才被调用,它依然能够正确地访问和修改this.count

2.自定义事件处理和数据封装

闭包还可以用来封装数据,尤其是在创建自定义事件或提供特定的接口时。你可以利用闭包保持一些数据的私有性,不让外部直接访问。

示例:私有数据和封装方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
<p>Count: {{ counter.getCount() }}</p>
</div>
</template>

<script>
export default {
data() {
// 使用闭包封装数据,防止直接修改
const counter = (() => {
let count = 0; // 私有变量
return {
increment: function() {
count++;
},
decrement: function() {
count--;
},
getCount: function() {
return count;
}
};
})();

return {
counter
};
}
};
</script>

在这个例子中,counter 对象是通过闭包来创建的,count 变量是私有的,只能通过 incrementdecrementgetCount 方法来访问和修改。外部无法直接访问或修改 count,这就是闭包的封装效果。

3.在计算属性中使用闭包

计算属性(computed)在 Vue 中很常用,计算属性本质上也是通过闭包来实现的,它能“记住”计算过程中引用的变量。当数据源变化时,计算属性会重新计算并返回结果。

示例:闭包在计算属性中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<p>Original Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
</div>
</template>

<script>
export default {
data() {
return {
count: 5
};
},
computed: {
doubleCount() {
// 计算属性也是通过闭包保持对 `this.count` 的引用
return this.count * 2;
}
}
};
</script>

在上面的代码中,doubleCount 是一个计算属性,它形成了一个闭包,能够访问 this.count,即使 count发生变化,doubleCount 会自动更新。

4.通过闭包管理定时器或延时操作

有时,我们需要在 Vue 中使用定时器或者延时操作。在这种情况下,闭包非常有用,因为它能够让定时器回调函数在执行时仍然能够访问外部的数据或方法。

示例:通过闭包管理定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<button @click="startTimer">Start Timer</button>
<p>Time: {{ time }}</p>
</div>
</template>

<script>
export default {
data() {
return {
time: 0
};
},
methods: {
startTimer() {
let timer = setInterval(() => {
this.time++;
if (this.time >= 10) {
clearInterval(timer); // 停止定时器
}
}, 1000);
}
}
};
</script>

在这个例子中,setInterval 使用了箭头函数,这样就形成了一个闭包,this.time依然能在回调函数中被访问和修改,即使回调函数是异步执行的。