前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注前端
设计高阶函数
写在前头
本节教程的目标有三个:
- 知道什么是高阶函数
- 知道高阶函数在什么场景下用
- 能够设计合理的高阶函数
什么是高阶函数(what)
高阶函数指的是那些能够接受一个或多个函数作为参数或者返回一个函数作为结果的函数
换句话说就两个情况:
- 该函数能够接收最起码一个函数作为参数
- 该函数能够返回一个函数
接收一个函数作为参数的,在我们学习JS
的过程中见过不少,例如map
、filter
,示例代码如下:
// map的作用是根据函数参数中的条件生成新的数组
const numbers = [1, 2, 3, 4];
// 使用 map 对每个元素进行操作
const doubled = numbers.map(function (num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8]
// filter的作用是根据函数参数中的条件生成新的数组,看上去好像和map一样?
const ages = [12, 20, 17, 35];
// 过滤出成年年龄
const adults = ages.filter(function (age) {
return age >= 18; // 这里是true/false,真假层面的操作,而map是数值层面的操作
});
console.log(adults); // [20, 35]
// map与filter结合使用,前端最常见的高阶函数用法!
const users = [
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
];
const activeUserNames = users
.filter(user => user.active) // 筛选活跃用户
.map(user => user.name); // 提取名字
console.log(activeUserNames); // ["Alice", "Charlie"]
返回一个函数的高阶函数可以看这个计时器的案例:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
此外,常见的定时器、AJAX请求也是高阶函数(原来高阶离我们这么近),代码如下所示:
// 定时器
// 使用 setTimeout 的例子
console.log("开始计时...");
setTimeout(function() { // 接收了一个函数作为参数,就算高阶函数!
console.log("3秒过去了!");
}, 3000); // 3000 毫秒 = 3 秒
console.log("此消息会在计时开始后立即显示");
// 使用 fetch API 发起 GET 请求的例子
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json()) // 将响应转换为 JSON 格式,这里的response => response.json()是一个函数
.then(data => console.log(data)) // 成功获取数据后的处理,这里的data => console.log(data)是一个函数
.catch(error => console.error('错误:', error)); // 错误处理,这里的error => console.error('错误:', error)也是一个函数
知道了高阶函数是什么没啥大用处,还需要怎么用,哪里用
高阶函数的作用
笔者认为高阶函数的主要作用在于增加代码的复用性、灵活性、同时让代码更加的简洁,同时让代码具有模块化
-
代码复用
从map
、filter
的例子可以看出高阶函数的的第一个作用是代码复用,从数组
这个对象
的角度来说,过滤内容和映射内容(这里取map
之意,也可以理解为挑选)是再常见不过的操作,那需要针对不同类型的数组都写出不一样的过滤和映射方法吗?这样太复杂了,故而出现了map
和filter
这样的高阶函数便于直接操作👉体现了复用、简洁 -
接收回调函数(更加进一步处理数据)
在上面的定时器和AJAX请求中可以看到setTimeout
、.then()
、.catch()
作为高阶函数接收了回调函数作为参数,目的是为了进一步的处理数据,作为调用者,我们无需知道其内部是怎么导致延时执行的,怎么通过.then()
处理结果的,错误怎么被捕获的👉体现了简洁,简化了异步编程 -
函数组合
通过map
与filter
结合使用的例子可知,高阶函数可以进行组合,让复杂的逻辑变得简单👉简洁、模块化
4.提高代码的可读性
以第2点为例,从另一个角度来看高阶函数能够让调用者专注做什么,而不用去了解其内部是怎么做的👉提高了可读性
装饰器模式(难点)
上述4点都是高阶函数简单的作用体现,下面来看高阶函数的另一个作用——装饰器模式,以下面的代码为例:
function withLogging(fn) {
return function (...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result of ${fn.name}:`, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
loggedAdd(5, 3);
// 输出:
// Calling add with [5, 3]
// Result of add: 8
不熟悉高阶函数的同学可能对这段代码有点懵,我通过截图展示一下数据流,如下图所示:
可以看到,最终的实现还是把(5,3)
传给了add()
,但在add()
执行之前和执行之后,分别输出了函数的名字、参数以及结果
这段代码在没有改变原函数的情况下,通过包装函数添加了额外的功能,即输出了函数的名字、参数以及结果,这种就叫做装饰器模式,这种做法遵循了开放-封闭原则
:对扩展开放,对修改关闭
这种玩法可以用来做日志,例如在调用某个函数之前,先记录一下调用者是谁、调用的函数是什么;可以做权限,在调用某个函数之前,判断一下调用者的权限是否有资格调用这个函数;或者根据其他的条件看能否执行这个函数(例如当某对象已经执行过该函数,就不执行?如果没有就执行),即根据条件选择性执行函数;可以做性能检测,计算函数的执行时间…
是不是很像node.js
的中间件,或者是react
的HOC
?没错,它们都是装饰器模式设计模式
如果学到这里,那么恭喜你,你已经有设计高阶函数的思路了(你已经知道怎么玩了)
为什么需要使用高阶函数(why)
从结果来看,使用高阶函数的好处不言而喻,让代码更简洁,同事也会觉得你写的代码很漂亮🌼
高阶函数的实际场景(where/when)
日志模式(例子在上面)
权限控制
function withPermission(permission) {
return function(fn) {
return function(...args) {
if (checkUserPermission(permission)) { // 假设有一个检查权限的函数
return fn(...args);
} else {
console.error('权限不足');
}
};
};
}
const deleteUser = withPermission('admin')(function(userId) {
console.log(`删除用户: ${userId}`);
});
deleteUser(123); // 如果当前用户没有 'admin' 权限,则会输出 "权限不足"
性能检测
function timeIt(fn) {
return function(...args) {
console.time(fn.name);
const result = fn(...args);
console.timeEnd(fn.name);
return result;
};
}
const computeHeavyTask = timeIt(function() {
// 模拟耗时计算
let sum = 0;
for (let i = 0; i < 1e7; i++) {
sum += i;
}
return sum;
});
computeHeavyTask();
缓存
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fibonacci = memoize(function(n) {
return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(30)); // 缓存中间结果,加速计算
缓存的思路即条件性判断是否执行函数
函数组合
与组合map
、filter
类似,将多个小函数组合成一个更复杂的功能
function compose(...fns) {
return function(x) {
return fns.reduceRight((v, f) => f(v), x);
};
}
const toUpperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const shout = compose(exclaim, toUpperCase);
console.log(shout("hello")); // 输出 "HELLO!"
数据验证
对输入的数据进行校验,并根据校验结果决定是否继续执行后续逻辑
function validate(schema) {
return function(fn) {
return function(data) {
const errors = schema.validate(data);
if (errors.length > 0) {
console.error(errors);
return;
}
return fn(data);
};
};
}
const userSchema = {
validate: function(data) {
const errors = [];
if (!data.name) errors.push('缺少名字');
if (!data.age) errors.push('缺少年龄');
return errors;
}
};
const createUser = validate(userSchema)(function(userData) {
console.log('创建用户:', userData);
});
createUser({ name: 'John' }); // 输出错误信息:缺少年龄
怎么设计高阶函数(how)
设计高阶函数需要具体场景具体实现,但有以下几点关键:
- 确定要做什么,基本上高阶函数的场景在*为什么需要使用高阶函数(why)*中已有覆盖
- 设计参数,需要接收什么样的函数,函数需要完成什么功能,假设这个函数为
A
- 设计返回值参数,一般需要设计的高阶函数的返回值也是个函数,这个函数需要接收什么样的参数?假设这个函数为
B
、参数为C
,一般C
是传给A
的,执行的也是A
- 返回值(函数)的逻辑,是否需要判断?在函数
A
的执行前还是执行后运行其他逻辑?又或是执行前后都要?(例如日志) - 错误处理,在函数
A
执行过程中出现错误如何解决? - 设计时考虑是否能够复用,高阶函数的特点就是复用
- 对于重复计算的需求,首先考虑缓存用法,优化性能
高阶函数与组件如何选择?(which)(经典取舍问题)
假设存在一个给定一个数字数组,返回其中所有偶数的需求
Vue实现
在Vue
中,可以将<AppliedDiscount :price="product.price" :discount="product.discount" />
封装为一个单独的组件,如下代码所示:
<!-- EvenFilter.vue 计算组件-->
<template>
<div>
<p>原始数据: {{ numbers }}</p>
<p>偶数结果: {{ evenNumbers }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue';
defineProps({
numbers: {
type: Array,
required: true
}
});
function filterEven(numbers) {
return numbers.filter(num => num % 2 === 0);
}
const evenNumbers = computed(() => filterEven(numbers));
</script>
组件内
<template>
<EvenFilter :numbers="[1, 2, 3, 4, 5, 6]" />
</template>
<script setup>
import EvenFilter from './components/EvenFilter.vue';
</script>
注意,computed
具有缓存的作用
高阶函数实现
// highOrderUtils.js
// 高阶函数:filterArray 接收一个 predicate 函数
export function filterArray(predicate) {
return function(array) {
return array.filter(predicate);
};
}
// main.js
import { filterArray } from './highOrderUtils.js';
// 定义具体筛选规则
const isEven = num => num % 2 === 0;
// 创建专门用于筛选偶数的函数
const filterEven = filterArray(isEven);
// 使用
const input = [1, 2, 3, 4, 5, 6];
const result = filterEven(input); // [2, 4, 6]
console.log('偶数:', result);
对比
其实两者都能实现,但探讨的点主要在于
- 复用性: 单独设立一个
Vue
组件,这个组件能否复用?如果不能,设计一个组件专门用来计算是否合理?(从Vue
的设计理念来看没问题) - 可读性(嵌套前提): 前提在于
Vue
组件被其他的组件嵌套,即在被嵌套的组件中再设计组件进行计算,数据不够直观,代码可读性不高 - 测试:
Vue
能易于执行单元测试 - **UI:**如果在
UI
上未来有改动的需求,或者对UI
关联性高,那么只能选择Vue
组件 - 状态管理:在组件中可以使用
Pinia
等进行复杂状态管理,但高阶函数没有这方面的实现能力 - **纯数据情况:**与
UI
需求相反,如果只需处理数据,那么高阶函数
是个不错的选择
两者结合
当然,也可以把核心逻辑写成工具函数,再在组件中调用该函数,这是非常折中的办法了…这里不再举例
欢迎关注csdn前端领域博主: 前端小王hs,喜欢可以点个赞!您的支持是我不断更新的动力!🔥🔥🔥
前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注前端