前端进阶:设计高阶函数

前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者 
网络工程师 前端工程师 项目经理 阿里云社区博客专家 

email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注前端

写在前头

本节教程的目标有三个:

  1. 知道什么是高阶函数
  2. 知道高阶函数在什么场景下用
  3. 能够设计合理的高阶函数

什么是高阶函数(what)

高阶函数指的是那些能够接受一个或多个函数作为参数或者返回一个函数作为结果的函数

换句话说就两个情况:

  1. 该函数能够接收最起码一个函数作为参数
  2. 该函数能够返回一个函数

接收一个函数作为参数的,在我们学习JS的过程中见过不少,例如mapfilter,示例代码如下:

 // 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)也是一个函数

知道了高阶函数是什么没啥大用处,还需要怎么用,哪里用

高阶函数的作用

笔者认为高阶函数的主要作用在于增加代码的复用性、灵活性、同时让代码更加的简洁,同时让代码具有模块化

  1. 代码复用
    mapfilter的例子可以看出高阶函数的的第一个作用是代码复用,从数组这个对象的角度来说,过滤内容映射内容(这里取map之意,也可以理解为挑选)是再常见不过的操作,那需要针对不同类型的数组都写出不一样的过滤和映射方法吗?这样太复杂了,故而出现了mapfilter这样的高阶函数便于直接操作👉体现了复用、简洁

  2. 接收回调函数(更加进一步处理数据)
    在上面的定时器AJAX请求中可以看到setTimeout.then().catch()作为高阶函数接收了回调函数作为参数,目的是为了进一步的处理数据,作为调用者,我们无需知道其内部是怎么导致延时执行的怎么通过.then()处理结果的错误怎么被捕获的👉体现了简洁,简化了异步编程

  3. 函数组合
    通过mapfilter结合使用的例子可知,高阶函数可以进行组合,让复杂的逻辑变得简单👉简洁、模块化

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中间件,或者是reactHOC?没错,它们都是装饰器模式设计模式

如果学到这里,那么恭喜你,你已经有设计高阶函数的思路了(你已经知道怎么玩了)

为什么需要使用高阶函数(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)); // 缓存中间结果,加速计算

缓存的思路即条件性判断是否执行函数

函数组合

与组合mapfilter类似,将多个小函数组合成一个更复杂的功能

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)

设计高阶函数需要具体场景具体实现,但有以下几点关键:

  1. 确定要做什么,基本上高阶函数的场景在*为什么需要使用高阶函数(why)*中已有覆盖
  2. 设计参数,需要接收什么样的函数,函数需要完成什么功能,假设这个函数为A
  3. 设计返回值参数,一般需要设计的高阶函数的返回值也是个函数,这个函数需要接收什么样的参数?假设这个函数为B、参数为C,一般C是传给A的,执行的也是A
  4. 返回值(函数)的逻辑,是否需要判断?在函数A执行前还是执行后运行其他逻辑?又或是执行前后都要?(例如日志
  5. 错误处理,在函数A执行过程中出现错误如何解决?
  6. 设计时考虑是否能够复用高阶函数的特点就是复用
  7. 对于重复计算的需求,首先考虑缓存用法,优化性能

高阶函数与组件如何选择?(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备注前端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小王hs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值