闭包是 JavaScript 语言的一个特色,很多高级应用都要依靠闭包实现。
变量的作用域
要学习闭包,首先我们需要了解变量的作用域。关于作用域,我们在前面深入 JavaScript 系列已经介绍过了,这里就不再赘述了。
我们先来看下面这段代码:
const a = 1;
function fn() {
console.log(a); // 1
}
fn();
这段代码可以打印出 1,因为 fn 函数作用域可以访问全局作用域上的 a 变量。
function fn() {
const a = 1;
}
console.log(a); // error
这段代码会报错,因为外部作用域无法访问内部作用域的变量。
那么有没有什么方式能让外部作用域访问内部作用域的变量呢?
有,答案是:闭包!
function f1() {
const a = 1;
function f2() {
return a;
}
return f2;
}
const f3 = f1();
const result = f3();
console.log(result); // 1
这是什么原因呢?
f1 执行结束返回了内部函数 f2 的引用,f3 全局变量保存了内部函数 f2 的引用,f2 函数作用域链是可以访问 a 变量的,所以 f3 执行后 result 变量可以获取 a 的值。
闭包的概念
闭包是能够读取其他函数内部变量的函数。上例中 f2 函数就是闭包,闭包是函数内部和函数外部连接的一座桥梁。
闭包的用途
闭包的最大用处有两个:
- 1.前面提到的可以读取函数内部的变量
- 2.让这些变量的值始终保持在内存中
我们来看个变量的值始终保持在内存中的例子
function f1() {
let a = 1;
function add() {
a += 1;
}
function f2() {
return a;
}
return {
f2,
add,
};
}
const { f2: f3, add } = f1();
add();
add();
const result = f3();
console.log(result); // 3
打印的结果是 3。这就证明了,函数 f1 中的局部变量 a 一直保存在内存中,没有在 f1 调用后清除。
为什么会这样呢?
原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量 f3,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
使用闭包的注意点
1、闭包有内存消耗,所以不能滥用闭包。2、如果你把父函数当做对象来使用,把闭包当作对象的公用方法,把内部变量当作对象的私有属性,注意不要随便改变父函数内部变量的值。
经典必考题
const list = [];
for (var i = 0; i < 3; i++) {
list[i] = function () {
console.log(i);
};
}
list[0]();
list[1]();
list[2]();
答案是:都是 3
那么如何打印出 0、1、2
方法 1——使用立即执行函数
const data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function () {
console.log(i);
};
})(i);
}
data[0]();
data[1]();
data[2]();
方法 2——使用块级作用域
ES6 中新增了块级作用域的概念,{}里的区域可以是块级作用域( {},if{},else{},while{},for{},swicth{}…),使用 let 关键字声明变量,可以添加块级作用域。
const list = [];
for (let i = 0; i < 3; i++) {
list[i] = function () {
console.log(i);
};
}
list[0]();
list[1]();
list[2]();