开始故事前,先来做一道例题~
场景:函数只允许被调用一次
给定一个函数
fn
,它返回一个新的函数,返回的函数与原始函数完全相同,只不过它确保fn
最多被调用一次。
- 第一次调用返回的函数时,它应该返回与
fn
相同的结果。- 第一次后的每次调用,它应该返回
undefined
。场景条件1:fn = (a,b,c) => (a + b + c), calls = [[1,2,3],[2,3,6]]
场景结果1:[{"calls":1,"value":6}]
场景使用1:
const onceFn = once(fn); onceFn(1, 2, 3); // 6 onceFn(2, 3, 6); // undefined, fn 没有被调用
场景条件2:fn = (a,b,c) => (a * b * c), calls = [[5,7,4],[2,3,6],[4,6,8]]
场景结果2:[{"calls":1,"value":140}]
场景使用2:
const onceFn = once(fn); onceFn(5, 7, 4); // 140 onceFn(2, 3, 6); // undefined, fn 没有被调用 onceFn(4, 6, 8); // undefined, fn 没有被调用
代码示例:
var once = function(fn){ let isFirst = false; return function(...args){ if(!isFirst){ isFirst = true; return fn(...args); } return 'returns undefined without calling fn'; } } // let fn = (a,b,c) => (a + b + c) // let onceFn = once(fn) // console.log(onceFn(1,2,3)); // 6 // console.log(onceFn(2,3,6)); // returns undefined without calling fn let fn = (a,b,c) => (a * b * c) let onceFn = once(fn) console.log(onceFn(5,7,4)); // 140 console.log(onceFn(2,3,6)); // returns undefined without calling fn console.log(onceFn(4,6,8)); // returns undefined without calling fn
方法解析:
- once函数:接收一个函数fn作为参数,并返回一个新的函数
- 新函数
- 在第一次被调用时,会执行fn并返回其结果,同时将闭包中的isFirst变量设置为true;
- 从第二次开始,因为called已经是true了,该函数只会返回returns undefined without calling fn
最终拓展:
如何理解once函数中的闭包原理呢?
关于闭包的故事
想象一下你有一个房间,这个房间就是你的函数。在这个房间里,你放了一些东西,比如椅子、桌子和玩具。这些东西就相当于你在函数内部定义的变量。
现在,你有一个朋友想进这个房间看看,但你不能让他直接进去(就像你不能直接访问函数内部的变量一样),因为房间里的某些东西可能很私密或者只能在特定条件下才能看。于是,你给了你的朋友一个过滤镜,这个过滤镜就是你的函数返回的一个“东西”,通常是一个函数。
通过这个过滤镜,你的朋友可以窥视房间里的某些东西,但只能是你允许他看的那些。
- 房间 —— 函数
- 椅子、桌子和玩具(私密) —— 变量
- 过滤镜 —— 函数的返回值
- 我将房间整理,只让椅子、桌子在窗口 —— 函数内部定义的另一个函数
- 我将房间里面的过滤镜给朋友,朋友带上过滤镜,进入房间看到椅子、桌子,将椅子拉出坐了下去 —— 内部函数访问并操作外部函数作用域中的变量
这就是闭包的核心概念:一个函数返回了另一个函数,而返回的这个函数可以访问到外部函数作用域中的变量。
这里的“过滤镜”和“房间”的关系就是闭包。闭包让你可以在一个函数内部定义另一个函数,并且让内部函数访问并操作外部函数作用域中的变量,即使外部函数已经执行完毕并返回了。
为什么这很重要呢?
因为闭包提供了一种封装私有变量的方式。
- 外部函数可以视为一个“私有”的作用域,而内部函数则可以通过闭包来访问这个作用域中的变量,同时外部代码无法直接访问这些变量。
- 另外,闭包还允许你创建具有持久状态(即状态可以被保存并在后续调用中访问)的函数。因为闭包中的变量在外部函数返回后仍然存在于内存中,所以内部函数可以持续访问和修改这些变量。
再来两个小案例
1、案例:for循环里的闭包
该案例涉及到var、let的区别,可以参考小编的另一篇文章
// 错误代码
var list = document.querySelectorAll('li');
for (var i = 0; i < list.length; i++) {
list[i].onclick = function(){
console.log(i);//永远只输出3
}
}
// 正确代码1
var list = document.querySelectorAll('li');
for (let i = 0; i < list.length; i++) {
list[i].onclick = function(){
console.log(i);//输出点击的li下标
}
}
// 正确代码2
var list = document.querySelectorAll('li');
for (var i = 0; i < list.length; i++) {
(function(i){
list[i].onclick = function(){
console.log(i);//输出点击的li下标
}
})(i)
}
2、案例:闭包避免变量被资源回收机制销毁
function add(){
var a = 0;
return function addCount(){
a++;
console.log(a);
}
}
var count = add();
count();//1
count();//2
总结一下
闭包就是一个函数记住了并访问其词法作用域(即外部函数的作用域)的能力。这是通过函数返回另一个函数,而这个返回的函数访问了外部函数的局部变量来实现的。闭包在JavaScript中非常有用,它们是实现模块化和封装的关键技术之一。