前言
本文很基础,适合没有了解过闭包的同学入门,我是经常使用 js
书写业务逻辑,但没有刻意使用过闭包,如果你的情况跟我差不多,那么跟着本文,你一定也可以有所收获。
进入今天的主题 闭包 。
闭包
闭包是什么
闭包(closure)
是 JavaScript
的难点,也是它的特色。是号称 JS
面试三座大山(原型与原型链,作用域及闭包,异步和单线程)其中的一座山。
很多高级应用都需要依靠闭包来实验,包括我们去看很多的 JS
库和框架的源码,都少不了闭包的影子。
定义
闭包就是能够读取其它函数内部变量的函数。
为什么我们要借助闭包来 读取其它函数内部的变量 呢?
因为 JavaScript
这个语言的特别之处就在于,函数内部 可以直接读取 全局变量 ,但是在 函数外部 无法直接读取 函数内部 的 局部变量 。只有 函数内部 的 子函数 才能读取 局部变量 ,可以看下面的例子。
// 此部分只为演示全局变量和局部变量 与闭包无关
// 全局变量 在任何地方都可以访问
let s = 100;
function foo() {
// 局部变量,函数运行时创建,函数执行完销毁
let a = 10;
function boo() {
console.log('🚀🚀~ boo:', a); //🚀🚀~ boo: 10
}
boo()
}
foo()
console.log('🚀🚀~ s:', s); //🚀🚀~ s: 100
console.log('🚀🚀~ a:', a); // Uncaught ReferenceError: a is not defined
所以说,闭包可以简单理解成 定义在一个函数内部的函数 。闭包本质上,就是将 函数内部 和 函数外部 连接起来的桥梁。
如何从外部读取函数内部的局部变量
先来思考一个问题。如何从 函数外部 读取 函数内部 的 局部变量 ?可是前面不是已经说了么,在 函数外部 无法直接读取 函数内部 的 局部变量 。
是的,确实无法 直接 读取,但是我们可以 变通 一下。
第一种是 return
返回。
function foo() {
let a = 88;
return a;
}
console.log('🚀🚀~ : a', foo()); // 🚀🚀~ : a 88
第二种是上面提到的子函数。
function foo() {
let a = 99;
function boo() {
console.log('🚀🚀~ a:', a);
}
boo();
}
foo();// 🚀🚀~ a: 99
这里先留下一个思考题。
- 根据闭包的定义,闭包就是能够读取其它函数内部变量的函数,那么以上两种方式是闭包么?如果不是,他们都能拿到局部变量的值,并且更简单,为什么还要用闭包呢?
为什么需要闭包
局部变量在函数执行时被创建,函数执行完被销毁,没有办法 长久的保存状态 和 共享 。
全局变量可能造成 变量污染 ,使代码变得难以阅读,难以维护。
那么我们就希望有一种 即可以长久的保存变量,又不会造成全局污染 的操作,闭包也就应运而生了。
闭包的写法
function f1() {
let a = 10;
function f2() {
a++;
console.log('🚀🚀~ a:', a);
}
return f2;
}
let fn = f1(); // f1执行的结果就是闭包
fn()
思考题解答
现在我们就来解答一下刚才留下的思考题,子函数
和直接 return
也能拿到局部变量的值,为什么还需要闭包呢。
//闭包
function f1() {
let a = 10;
function f2() {
a++;
console.log('🚀🚀~ a:', a);
}
return f2;
}
let fn = f1();
fn();
//直接return
function f3() {
var a = 10;
a++;
return a
}
console.log('🚀🚀~ a:', f3());
//子函数
function f4() {
let a = 10;
function f5() {
a++
console.log('🚀🚀~ a:', a);
}
f5();
}
f4();
可以看到控制台输出的结果是一样的。
那么我们多调用几次呢?
//闭包
function f1() {
let a = 10;
function f2() {
a++;
console.log('🚀🚀~ 闭包 ~ a:', a);
}
return f2;
}
let fn = f1(); // f1执行的结果就是闭包
fn();
fn();
fn();
fn();
//return
function f3() {
let a = 10;
a++;
console.log('🚀🚀~ return a:', a);
}
f3();
f3();
f3();
f3();
//子函数
function f4() {
let a = 10;
function f5() {
a++
console.log('🚀🚀~ 子函数 a:', a);
}
f5();
}
f4();
f4();
f4();
f4();
发现什么了么,我们使用闭包,每次调用后,变量 a
的值都会 +1
,而我们直接 return
以及 子函数
的方式,每次调用后,变量 a
的值一直都是 11
。
到这里之前留下的思考题就已经有答案了。闭包是一个能够读取其它函数内部变量的函数,但是能够读取其它函数内部变量的函数不一定就是闭包,为什么需要闭包,因为闭包 即可以长久的保存变量,又不会造成全局污染。
闭包的缺点
优点上面已经说过了,那么闭包有什么缺点呢。通常情况下,函数的活动对象会随着执行的上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,这意味着闭包比普通函数要消耗更多的内存。
案例-缓存
const cacheMemory = (() => {
let cache = {}
return {
set: (id) => {
if (id in cache) {
return `查找到的结果是${cache[id]}`
}
const result = asyncFn(id);//模拟异步结果
cache[id] = result
return `查找到的结果是${cache[id]}`
}
}
})()
案例-模拟栈
const Stack = (() => {
let arr = [];
return {
push: (value) => {
arr.push(value)
},
pop: (value) => arr.pop(value),
size: () => arr.length,
}
})()
Stack.push("a")
Stack.push("b")
console.log('🚀🚀~ Stack.size:', Stack.size()); // 2
console.log('🚀🚀~ Stack.pop("b"):', Stack.pop("b")); // b
console.log('🚀🚀~ Stack.size:', Stack.size()); // 1