高阶函数
高阶函数是一个接受其他函数作为参数,或返回一个新函数的函数。
高阶函数有以下几种形式:满足其中一个就是高阶函数
- 一个函数的参数是另一个函数
- 一个函数返回另一个函数
- 一个函数的参数是另一个函数并返回一个新的函数
高阶函数举例
有一个函数,它接受一个数字并返回该数字的平方
js
function square(x) {
return x * x;
}
现在,我们需要扩展square
函数的功能,通过square
函数来计算一个数字数组的平方。此时就可以写一个高阶函数来对square
函数的功能进行扩展。以下代码就是一个高阶函数,它接受了一个函数作为参数,并将该函数应用于一个接受的数字数组。最后这个高阶函数就会返回一个新的数组,其中包含每个数字的结果。
js
// squareStrong就是一个高阶函数,它复用了square的逻辑,并且还扩展了新的功能
function squareStrong(arr,callBack) {
let result = [];
for(let i = 0;i<arr.length;i++){
result.push(callBack(i));
}
return result
}
let numArr = [1,2,3,4,5,6,7];
// 使用高阶函数来计算一个数字数组的平方
let squaresArr = squareStrong(numArr,square);
console.log(squaresArr); // [1,4,9,16,25,36]
在这个例子中,将square
函数作为squareStrong
函数的第二个参数传递。squareStrong
函数将遍历数字数组,并将每个数字传递给square
函数。square
函数将返回该数字的平方,并将其添加到结果数组中。最后,squareStrong
函数将返回结果数组。
这个例子演示了高阶函数其中一个形式:将函数作为参数传递。
高阶函数使用场景
高阶函数有以下几种常见的使用场景:
- 函数柯里化:将一个接受多个参数的函数转换为一个接受单个参数的函数
- 函数组合:将多个函数组合成一个新的函数
- 函数节流和防抖:通过限制函数的调用频率来优化性能
- 函数缓存:将函数的结果缓存起来,避免重复计算
函数柯里化
函数柯里化是将一个接受多个参数的函数转换为一个接受单个参数的函数。柯里化不会调用函数,它只是对函数进行转换的一个过程。
举例:函数柯里化有一个比较经典的题目,那就是求和。
js
// 实现一个求三个数的加和的函数,接收三个参数,返回最终的加和值
function addThreeNum (a, b, c) {
return a + b + c;
}
addTreeNum(6, 9 ,10);// 返回结果为25
下面对addThreeNume
进行柯里化
js
function addThreeNumCurry(a) {
console.log(a, "a");
return function (b) {
console.log(b, "b");
return function (c) {
console.log(c, "c");
return a + b + c;
}
}
}
const result = addThreeNumCurry(6)(9)(10);// 返回结果同样是25
console.log(result, "result");
// 分部调用柯里化后的函数
const add1 = addThreeNumCurry(6);// 返回的是一个函数
console.log(add1, "add1");
const add2 = add1(9);// 返回的是一个函数
console.log(add2, "add2");
const add3 = add2(10);// 已接收到所有的参数,返回最终的计算结果
console.log(add3, "add3");// 25
柯里化后的加和函数,每次都是传入单个参数,返回的函数都会保留之前传入的所有参数,并在最后一个函数传入后进行最终的计算。即函数一直保留着之前的所有状态,等到所有条件都满足后,执行最终的操作。
柯里化的用处
- 参数复用:柯里化函数将在接收到最后一个参数的时候才进行最后的计算,与普通一次性接收所有参数的函数相比,延迟了最终计算,并且前面传入的参数还可以被后续的函数调用所复用。
需求:通过正则校验电话号、邮箱、身份证是否合法
js
// 封装一个校验函数
function checkByRegExp(regExp, str) {
return regExp.test(str)
}
// 要检验多个手机号、邮箱就需要调用多次,传入校验的正则表达式都一样
// 校验手机号
checkByRegExp(/^1\d{10}$/, '15152525634');
checkByRegExp(/^1\d{10}$/, '13456574566');
checkByRegExp(/^1\d{10}$/, '18123787385');
// 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fsds@163.com');
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fdsf@qq.com');
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fjks@qq.com');
// 函数柯里化,复用正则表达式这个参数
function checkByRegExp(regExp) {
return function(str) {
return regExp.test(str)
}
}
// 校验手机
const checkPhone = curryingCheckByRegExp(/^1\d{10}$/)
// 校验邮箱
const checkEmail = curryingCheckByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
// 校验手机号
checkPhone('15152525634');
checkPhone('13456574566');
checkPhone('18123787385');
// 校验邮箱
checkEmail('fsds@163.com');
checkEmail('fdsf@qq.com');
checkEmail('fjks@qq.com');
这就是参数复用
- 提前返回
需求:为了兼容IE和其他浏览器的添加事件方法,通常会将代码进行兼容行处理
js
// 给任意元素绑定任意的事件
// 传入的el是任意的元素,type是事件的类型,fn是事件处理函数
// arg是addEventListener()的第3个参数,不传,默认为false
function myaddEventListener(el, type, fn, arg) {
if (arg == undefined) { arg = false; };
//判断浏览器是否支持这个方法
if (el.addEventListener) {
el.addEventListener(type, fn, arg);
console.log("支持addEventListener");
} else if (el.attachEvent) {
el.attachEvent("on" + type, fn);
console.log("支持attachEvent");
} else {
el["on" + type] = fn;
console.log("addEventListener和attachEvent都不支持");
};
};
myaddEventListener(document.getElementById("btn"), "click", function () { console.log("gloria"); });
这里就存在一个问题,就是在每一次绑定事件的时候,都需要进行一次环境的判断,再去进行绑定。但实际上浏览器一旦确立下来,就不需要每次都进行这样的判断。
可以将上面的函数进行柯里化,就能规避这个问题,在使用前做一次判断即可。
js
function curryingAddEvent() {
if (window.addEventListener) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
} else if (window.attachEvent) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
}
}
// 会立即执行函数,然后进行浏览器的判断
const addEvent = (curryingAddEvent)();
// 调用
addEvent(document.getElementById('app'))('click')((e) => {console.log('click function has been call:', e);})(false);
// 分步骤调用会更加清晰
const ele = document.getElementById('app');
// 得到浏览器的环境
const environment = addEvent(ele)
// 绑定事件
environment('click')((e) => {console.log(e)})(false);
- 延迟执行
在上述的两个案例中,正则校验和事件监听的例子中已经体现了延迟执行curryingcheckByRegExp
函数调用后返回了checkPhone
和 checkemail
函数。curryingAddEvent
函数调用后返回了 addevent
函数。这些返回的函数都不会立即执行,而是等待调用。
手写柯里化函数
js
function addThreeNum(a, b, c) {
return a + b + c;
}
// 柯里化函数
const curry = function (fn) {
console.log(fn,"接受的函数")
return function nest(...args) {
console.log(fn.length,'fn函数的形参个数')
console.log(...args,"fn函数接受的所有参数")
// fn.length表示函数的形参个数
if (args.length === fn.length) {
// 当参数接收的数量达到了函数fn的形参个数,即所有参数已经都接收完毕则进行最终的调用
return fn(...args);
} else {
// 参数还未完全接收完毕,继续递归将新的参数传入
return function (paramse) {
console.log(paramse,"新参数")
return nest(...args, paramse);
}
}
}
}
// fn函数就是addThreeNum,形参个数是3
const addCurry = curry(addThreeNum);
const result = addCurry(1)(2)(3);
console.log(result)
这段代码实现了函数柯里化,柯里化是一种将多个参数的函数转化为一系列只接受一个参数的函数。代码中定义了一个名为curry
的函数,它接受一个函数fn
作为参数。curry
函数返回一个嵌套函数nest
。当调用nest
函数时,它会检查传入的参数数量是否等于函数fn
的形参个数(通过fn.length
获取)。如果参数数量相等,则直接调用fn
函数并传入这些参数,并返回结果。如果参数数量不等于fn
的形参个数,nest
函数会返回一个新的函数。这个新函数接受一个参数paramse
,并将之前传入的参数args
和新参数paramse
一起传递给nest
函数进行递归调用。简而言之,这个柯里化工具函数用来接收部分参数,然后返回一个新函数等待接收剩余参数,递归直到接收到全部所需参数,然后调用原函数,将接受的所有参数传给原函数。
通过调用curry
函数并传入addNum
函数来创建一个新的函数addCurry
。这个addCurry
函数可以通过连续调用来实现柯里化的效果。调用addCurry
函数,并传入三个参数1、2、3,输出结果为6。
当调用addCurry(1)(2)(3)
时,会按照以下步骤执行:
- 首先调用
addCurry(1)
,相当于调用了nest(1)
。args = [1];args.length == 1;fn.length == 3
,此时传入的参数数量为1不等于addNum
函数的形参个数3,所以返回一个新的函数,接受一个参数paramse
,实际上最后返回的也还是nest(1)
。 - 接着调用返回的新函数,即
function (paramse) { return nest(...args, paramse); }
,相当于调用了nest(1,2)
。args = [1,2];args.length == 2;fn.length == 3
,此时传入的参数数量为2不等于addNum
函数的形参个数3,所以继续返回一个新的函数,接受一个参数paramse
,实际上最后返回的也还是nest(1,2)
。 - 最后再次调用返回的新函数,即
function (paramse) { return nest(...args, paramse); }
,相当于调用nest(1, 2, 3)
。args = [1,2,3];args.length == 3;fn.length == 3
,此时传入的参数数量为3等于addNum
函数的形参个数3,就直接调用addNum
函数并传入参数1、2、3,最后结果就是6。
总结:柯里化是闭包的一个典型应用,利用闭包形成了一个保存在内存中的作用域,把接收到的部分参数保存在这个作用域中,等待后续使用,并且返回一个新函数接收剩余参数
函数组合
函数组合是将多个函数组合成一个新的函数,实际上就是把多个函数所需要操作的数据像管道一样连接起来,然后让数据穿过管道连接起来,得到最终的结果。函数组合的作用是实现函数的复用,保证函数的职责单一。
比如现在需要对某一个数据进行函数的调用,执行两个函数fn1
和fn2
,这两个函数是依次进行的;那么如果每次都需要进行两个函数的调用,操作上就会显得很重复;就可以将这两个函数组合起来自动依次调用,这个过程就是对函数的组合。
举例:比如定义了两个功能函数:double
方法将数据乘以2,square
方法将数据进行平方
js
// 普通情况下:
function double (num) {
return num * 2
}
function square (num) {
return num * num
}
let count = 10
let result = square(double(count))
console.log(result)
// 组合函数情况下:
function composeFn (d, s) {
return function (count) {
return s(d(count))
}
}
let newFn = composeFn(double, square)
let result2 = newFn(10)
console.log(result2)
手写组合函数
js
const compose = (...funcs) => {
console.log(...funcs,funcs,"funcs");
// ...funcs收集所有传递给 compose 的参数,并将它们存储在 funcs 数组中:数组中记录的是所有的函数。(传入多个参数会将所有参数存放在类数组,扩展运算符...可以接收任意数量的参数并将它们收集到一个数组中)
// funcs.length传入参数的个数
let len = funcs.length;
// 判断传入的全部参数的类型都是函数类型,不是的话就抛出异常
for (let i = 0; i < len; i++) {
if (typeof funcs[i] !== 'function') {
throw new TypeError('Expected arguments are functions')
}
}
// 这里其实也是利用了柯里化的思想,函数执行,生成一个闭包,预先把一些信息存储,供这个compose函数的函数作用域使用
return (x) => {
console.log(x,"x");
// 如果没有函数执行,直接返回结果
if (len === 0) return x;
// 如果只传递了一个函数,则直接调用该函数并返回结果
if (len === 1) funcs[0](x);
// 如果传递了多个函数,那么使用 Array.prototype.reduceRight 方法从右向左依次调用这些函数。
// reduceRight 方法接受一个累加器 res 和当前处理的函数 func,每次迭代都会用当前函数处理累加器的结果,直到所有函数都被应用。
return funcs.reduceRight((res, func) => {
console.log(res,"res");
console.log(func,"func");
return func(res);
}, x);
};
};
// 假设有以下三个简单的函数
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// 使用 compose 函数来组合这三个函数
const composedFunc = compose(square, double, addOne);
console.log(composedFunc(3)); // 输出: ((3+1)*2)^2 = 64
composedFunc(3) 实际上等价于
square(double(addOne(3)))
,先增加1,然后乘以2,最后求平方。
函数节流和防抖
函数防抖和节流是优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize
、scroll
,鼠标的mousemove
、mouseover
,输入框的input
以及键盘keypress
等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。
函数节流和防抖就是限制函数的执行次数:
- 防抖:在一定的时间间隔内,将多次触发的回调函数变为一次触发。(只执行最后一次)
- 节流:在一定的时间间隔内,将多次触发的回调函数只在这个时间间隔内执行一次。(控制执行次数)
防抖
原理:维护一个计时器,规定在delay时间后执行事件的处理函数,但是在delay时间内再次触发事件的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
handle
函数是事件触发后所要处理的业务逻辑的函数。
防抖函数的封装:防抖函数是一个高阶函数
js
// fn:需要防抖处理的函数,在这个函数中执行真正的业务逻辑
// delay:指定的时间间隔,在最后一次调用fn之前等待的时间
function debounce(fn,delay){
let timer = null;
// 返回的新函数是一个闭包,它可以访问外部函数 debounce 中的 timer 变量。
return function(...args){
// 使用 ...args 收集所有传递给新函数的参数,args是数组并将其传递给fn函数
if(timer != null){
// 定时器存在就清除之前的定时器
clearTimeout(timer)
}
// 设置新的定时器
timer = setTimeout(()=>{
// console.log(this)
// 使用apply改变fn函数this指向,以及args传递参数
// 并且这行代码相当于调用fn
fn.apply(this,args);
// 事件回调函数执行完毕后把计时器重置
timer=null;
},delay)
}
}
返回的新的函数就是事件触发后要执行的业务逻辑,这个函数的
this
就表示绑定事件的元素。本来this
是window
对象,而箭头函数的this
就是外层作用域也就是闭包函数的this
,这样就改变了fn
函数的this
指向。apply
的第1个参数指定this
对象,第2个参数可以传数组接收闭包函数的实参集合。
工作原理:
- 首次调用也就是第一次事件触发的时候:当第一次调用返回的新函数时,
timer
变量初始化为null
。就会设置一个新的定时器,在设置的delay
毫秒后执行fn
函数,这个函数才是事件触发后需要执行的逻辑处理。 - 后续调用也就是在
delay
毫秒内事件再次被触发:再次调用返回的新函数,timer
变量被赋值为之前的定时器就会执行clearTimeout(timer)
用于清除之前的定时器。然后设置一个新的定时器,重新开始计时。直到经过delay
毫秒的等待,执行setTimeout
的回调函数此时fn
函数 才会被执行,然后重置定时器。这样可以确保delay
毫秒的时间内只有在最后一次调用事件触发后要执行的真正的业务逻辑。
防抖场景
-
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
-
调整浏览器窗口大小时,
resize
次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖 -
文本编辑器实时保存,当无任何更改操作一秒后进行保存
防抖示例使用
对输入框的input
事件进行防抖
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debounce Example</title>
</head>
<body>
<input type="text" id="searchInput" placeholder="Search...">
<div id="results"></div>
<script>
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 模拟发送请求的函数
function search(query) {
console.log(query, "query");
const data = [{ id: 1, name: 'G.E.M.' }, { id: 2, name: 'gloria' }, { id: 3, name: 'jessica' }, { id: 4, name: 'hyomin' }]
setTimeout(() => {
displayResults(data);
},2000)
}
// 显示搜索结果的函数
function displayResults(results) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '';
results.forEach(result => {
const resultElement = document.createElement('div');
resultElement.textContent = result.name;
resultsDiv.appendChild(resultElement);
});
}
// 获取输入框元素
const searchInput = document.getElementById('searchInput');
// 事件触发后所要处理的业务逻辑,处理函数
function handle(event) {
const query = event.target.value.trim();
if (query) {
search(query);
} else {
displayResults([]);
}
};
// 绑定输入事件
searchInput.oninput = debounce(handle, 500); // 延迟500毫秒
</script>
</body>
</html>
对窗口resize
事件进行防抖
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>hello,world</h1>
</body>
<script>
// fn:需要防抖处理的函数,在这个函数中执行真正的业务逻辑
// delay:指定的时间间隔,在最后一次调用fn之前等待的时间
function debounce(fn, delay) {
let timer = null;
// 返回的新函数是一个闭包,它可以访问外部函数 debounce 中的 timer 变量。
return function (...args) {
// 使用 ...args 收集所有传递给新函数的参数,args是数组并将其传递给fn函数
console.log(args, "args");
if (timer != null) {
// 定时器存在就清除之前的定时器
clearTimeout(timer)
}
// 设置新的定时器
timer = setTimeout(() => {
// 使用apply改变fn函数this指向,以及args传递参数
// 并且这行代码相当于调用fn
fn.apply(this, args);
// 事件回调函数执行完毕后把计时器重置
timer = null;
}, delay)
}
}
// 事件触发后需要处理的逻辑:获取当前窗口的尺寸
function resizeHandle(type) {
console.log(type, "type");
console.log("窗口大小发生了变化");
console.log(window.innerWidth, "当前窗口的宽度");
console.log(window.innerHeight, "当前窗口的高度");
};
// resize事件的回调函数
const debounceResize = debounce(resizeHandle, 3000);
// 监听window的resize事件
window.addEventListener("resize",debounceResize);
</script>
</html>
当持续触发resize
事件时,事件处理函数resizeHandle
只在停止改变窗口大小之后3000毫秒之后才会调用一次,也就是说在持续触发resize
事件的过程中,事件处理函数resizeHandle
一直没有执行。
节流
原理:维护一个计时器,通过判断是否到达一定时间来执行事件的处理函数,没有达到一定时间再次触发事件的话,就会取消之前的计时器而重新设置。这样一来,只在固定的时间段内执行一次事件的处理函数
handle
函数是事件触发后所要处理的业务逻辑的函数。
节流函数的封装:节流函数是一个高阶函数
js
// 节流函数
// func:需要节流处理的函数,在这个函数中执行真正的业务逻辑
// limit: 在一定时间内只允许执行一次 func 的时间间隔(以毫秒为单位)
function throttle(func, limit) {
// inThrottle 是一个布尔变量,用于标记是否正在节流中,初始值为 undefined,表示不在节流中。
let inThrottle;
// 返回的新函数是一个闭包,它可以访问外部函数 throttle 中的 inThrottle 变量
return function (...args) {
// 使用 ...args 收集所有传递给新函数的参数,args是数组并将其传递给func函数
if (!inThrottle) {
// 如果 inThrottle 为 false 或 undefined,表示当前不在节流中,可以执行 func
inThrottle = true; // 将 inThrottle 设置为 true,表示进入节流状态
// 使用 setTimeout 设置一个新的定时器,在 limit 毫秒后执行 func
setTimeout(() => {
func.apply(this, args); // 相当于调用 func,apply改变func函数this指向,以及args传递参数
inThrottle = false; // 在 func 执行完毕后,将 inThrottle 重置为 false,表示退出节流状态
}, limit);
}
};
}
返回的新的函数就是事件触发后要执行的业务逻辑,这个函数的
this
就表示绑定事件的元素。本来this
是window
对象,而箭头函数的this
就是外层作用域也就是闭包函数的this
,这样就改变了func
函数的this
指向。apply
的第1个参数指定this
对象,第2个参数可以传数组接收闭包函数的实参集合。
工作原理:
- 首次调用也就是第一次事件触发的时候:当第一次调用返回的新函数时,
inThrottle
为undefined
。if (!inThrottle)
判断为true
,进入节流状态。然后设置定时器,在limit
毫秒后执行func
函数,执行完func
函数后并将inThrottle
重置为false
。 - 后续调用也就是在
limit
毫秒内事件再次被触发:再次调用返回的新函数,inThrottle
为true
。if (!inThrottle)
判断为false
,不会执行func
函数,也不会设置新的定时器。直到当前的定时器的回调函数执行完毕,inThrottle
被重置为false
,下一次调用才会再次进入节流状态。这样就可以确保limit
这段时间内只执行一次事件触发后要执行的真正的业务逻辑
节流场景
-
scroll 事件,每隔一秒计算一次位置信息等
-
浏览器播放事件,每隔一秒计算一次进度信息等
-
DOM元素的拖拽功能
-
input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)
节流示例使用
页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次网络请求,而不是在用户停下滚动页面操作时才去请求数据。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="content" style="height: 1000px;border: 1px solid gold;">
<!-- 初始内容 -->
</div>
</body>
<script>
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
inThrottle = true;
setTimeout(() => {
func.apply(this, args);
inThrottle = false;
}, limit)
}
}
}
// 模拟请求数据
function loadMoreData() {
let data = [];
setTimeout(() => {
data = [{ id: 1, text: "G.E.M." }, { id: 2, text: "gloria" }, { id: 3, text: "jessica" }, { id: 4, text: "hyomin" }]
appendDataToPage(data)
}, 100);
}
// 将新数据添加到页面中
function appendDataToPage(data) {
const contentDiv = document.getElementById('content');
data.forEach(item => {
const itemElement = document.createElement('div');
itemElement.textContent = item.text;
contentDiv.appendChild(itemElement);
});
}
// 滚动事件触发后要处理的逻辑
function scrollHandle() {
console.log("执行了")
loadMoreData();
}
// scroll事件的回调函数
const throttleScroll = throttle(scrollHandle, 3000);
// 监听window的scroll事件
window.addEventListener("scroll", throttleScroll);
// 初始化数据
appendDataToPage([{ id: 1, text: "摩天动物园" }, { id: 2, text: "启示录" }, { id: 3, text: "Beep Beep" }, { id: 4, text: "sketch" }])
</script>
</html>
当持续触发scroll
事件时,事件处理函数scrollHandle
3000毫秒这段时间内只执行了一次。
函数缓存
函数缓存,就是将函数运算过的结果进行缓存。本质上就是用空间(缓存存储)换时间(计算过程),常用于缓存数据计算结果和缓存对象。
原理:把参数和对应的结果数据存在一个对象中,调用时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果。
示例:斐波那契数列的缓存
斐波那契数列是一个经典的例子,展示了递归函数如何从缓存中获取会被重复计算的值。
代码如下:
js
// func:需要被缓存的函数
// context:可选参数,用于指定函数调用时的上下文(即 this 的值),如果未提供,则默认为 memoize 被调用时的上下文
const memoize = function (func, context) {
// cache 是一个普通的对象,用于存储已计算的结果,使用 Object.create(null) 创建一个纯净的对象,避免继承自 Object.prototype 的属性干扰
// let cache = {},这种方式通过字面量创建的对象会继承Object.prototype原型,如toString等
let cache = Object.create(null);
context = context || this; // 函数被定义时的上下文
// 返回一个新的函数
return (...args) => {
// ...args表示接受任意数量的参数
const key = JSON.stringify(args); // 使用 JSON.stringify 将参数数组 args 转换为一个字符串,确保每个参数组合都有一个唯一的键
console.log(cache);
// 检查 cache 中是否存在当前参数组合的键 key
if (!cache[key]) {
console.log(args, "args接受的参数")
console.log("开始调用func计算结果")
// 不存在,则调用 func 函数并传入 context 和参数 args,并将结果存储在 cache[key] 中
cache[key] = func.apply(context, args);
}
// 存在就返回 cache[key] 中存储的结果
return cache[key];
};
};
// 斐波那契数列
const fibonacci = function (n) {
// 如果 n 小于或等于 1,直接返回 n
if (n <= 1) return n;
// n大于1,递归
return fibonacci(n - 1) + fibonacci(n - 2);
};
const fibonacciMemoized = memoize(fibonacci);
// 测试
console.log("fibonacci(4)结果:", fibonacciMemoized(4)); // 输出: 3
console.log("fibonacci(10)结果:", fibonacciMemoized(10)); // 输出: 5
console.log("fibonacci(20)结果:", fibonacciMemoized(20)); // 输出: 6765
console.log("fibonacci(10)结果:", fibonacciMemoized(10)); // 输出: 5,这个是从缓存中取得,并没有再次调用fibonacci计算结果
-
n
小于1的情况:fibonacci(0)
返回 0,fibonacci(1)
返回 1。 -
n
大于1的情况,函数会递归地调用自身两次:- 第一次调用
fibonacci(n - 1)
,计算第 n-1 个斐波那契数。 - 第二次调用
fibonacci(n - 2)
,计算第 n-2 个斐波那契数。 - 最后,将这两个结果相加,得到第 n 个斐波那契数。
- 第一次调用
这个递归过程:
- 计算
fibonacci(2)
:fibonacci(2)
=fibonacci(1)
+fibonacci(0)
。因为fibonacci(1)
= 1、fibonacci(0)
= 0,所以fibonacci(2)
= 1 + 0 = 1 - 计算
fibonacci(3)
:fibonacci(3)
=fibonacci(2)
+fibonacci(1)
,fibonacci(2)
= 1 (前面已经计算过),fibonacci(1)
= 1,因此,fibonacci(3)
= 1 + 1 = 2 - 计算
fibonacci(4)
:fibonacci(4)
=fibonacci(3)
+fibonacci(2)
,fibonacci(3)
= 2 (前面已经计算过),fibonacci(2)
= 1 (前面已经计算过),因此,fibonacci(4)
= 2 + 1 = 3
这个递归存在着很多重复计算,例如 fibonacci(5)
时,fibonacci(3)
和 fibonacci(2)
都会被多次计算。通过使用缓存来优化递归计算,避免重复计算。