【精解前端八股手写题】深入浅出柯里化

柯里化模板

  • 柯里化是什么
    • 基本概念
    • 偏函数
  • 柯里化有什么用
    • 场景1:拆分计算
    • 场景2:工厂函数
  • 如何实现柯里化
    • 基础学习版:新人入门
    • 极简精华版:一行代码
    • 魔改升级版:闭包乱炖

柯里化是什么

基本概念

前端中的柯里化(Currying)是一个源自函数式编程的概念。

函数式编程,也叫面向函数编程,之后写一篇 React 的函数式编程思想相关的文章

它指的是将原本接受多个参数的函数转换成一系列接受单个参数函数链的过程。

注意,这里提到了,单个参数!这是个重点,后面要考!

比如对于一个add函数,它原来长这样:

function add(x, y) {return x + y;
}

将其柯里化之后就变成了:

function curryAdd(x) {return function(y) {return x + y;};
}
const res = curryAdd(2)(3);
console.log(res); // 输出5

偏函数

偏函数是一个容易与柯里化混淆的概念,它和柯里化的区别是:

柯里化严格要求每次只能传递一个参数,而偏函数则是可以传递任意参数。

也就是:

add(1)(2)(3) // ✅正宗柯里化
add(1)(2, 3)(4) // ❌ 假的柯里化,实际上是偏函数

所以严格来说,柯里化函数是一种特殊的偏函数

我们前端圈子内,平时口头上都叫柯里化,不需要严格区分。
知晓这个小知识,面试倒是可以多点谈资。

柯里化有什么用

还记得之前面试某个大厂的时候,反问面试官柯里化有什么用,他也愣住了,有点尴尬。

所以我觉得做开发,无论是学什么技术理论,都要结合实际场景,落到实处,不然就只是纸上谈兵。

场景1:拆分计算

试想一下这个场景:这里有个获取用户数据的函数,该函数需要一个IDdataKey作为参数。

先来看看不使用柯里化的方式

// 先获取userId,然后获取dataKey
getUserId().then(userId => {getDataKey().then(dataKey => {// 注意看这里:要两个参数都获取到了后才能开始计算 processUserInfo(userId, dataKey);});
});

在这个非柯里化的实现中,我们必须等到dataKey准备好后,才开始根据userId发起获取用户数据的请求。这意味着,在获取dataKey的等待时间内,我们无法利用这段时间来获取用户数据,导致整体执行时间较长。

为了提高效率,我们可以使用柯里化技术

getUserId().then(userId => {// 假设这里把 processUserInfo 柯里化了const next = processUserInfo(userId, dataKey);getDataKey().then(dataKey => {next(dataKey)});

可以看到,processUserInfo函数柯里化后,返回的是一个新的函数next

并且,它们就像是在一场接力赛中,每次执行都可以只完成部分计算,剩下的部分可以交给下一个函数接力。

这样做的好处是,可以先完成部分计算,先实现部分效果(比如先更新部分页面等等),再逐步实现后续效果,整体会相对比较流畅。

就问柯里化厉不厉害吧!

场景2:工厂函数

在KOA框架的中间件工厂函数中,柯里化用的也是比较多。

// 中间件工厂函数
function createMiddlewareFactory(param) {return function middleware(next) {return async function(ctx, nextInner) {// ...await next(ctx, nextInner);// ...};};
}app.use(createMiddlewareFactory(param1)());
app.use(createMiddlewareFactory(param2)());

这里用工厂模式的发挥的作用是:

  1. 可以通过不同参数(param1param2)来创建结构类似但不同的中间件,这样就不需要写多个创建函数了。

  2. 而且即使传递相同的参数,每次调用函数都能返回一个新的实例,不会是原来的引用,保证了每个中间件都是独立的。

另外,我们再看看KOA中间件的回调函数的朴素写法,它是这样的:

app.use(async (ctx, nextInner) => {await next(ctx, nextInner); // 想想 next 函数从哪来的呢
});

再多结合上面的中间件工厂函数看看,我们就可以感知到,柯里化在其中发挥的作用是:

  1. 格式化了参数(ctxnextInner)。

  2. 通过闭包传递了上下文(next)。

如何实现柯里化

虽然说上面已经给出了很多案例代码,但是都还是没有总结沉淀出一套方法论,不能做到一针见血地体现其实现方法。

这里给出几个版本,针对不同基础的群体。

基础学习版:新人入门

柯里化的精髓就是,闭包+判断参数个数。
闭包就是函数返回函数,很好实现。
至于如何获取到参数个数,有两种方法:

  • 第一是arguments对象,这是一个可以直接在函数上下文中获取到的对象,是一个伪数组(JS早期设计缺陷的产物之一),代表实际传入的参数。
function say() {console.log(arguments[0])// 因为是伪数组,要用数组API的话得先转成真数组// 即 const arr = Array.from(arguments)
}
  • 第二是Function.prototype.length,也就是一个函数的length属性其实就是它声明的参数数量。
function say() { console.log(say.length) // 0,因为没有声明参数
}

一般而言,我们更习惯用第二种(毕竟第一种都涉及早期JS黑历史,用着感觉也别扭),代码如下:

// 定义一个柯里化函数
function curry(func) {// ...args代表任意数量的参数,args是一个数组return function curried(...args) {// 实际传参数量 >= 声明参数数量if (args.length >= func.length) {// 正常执行return func(...args);} else {return function(...moreArgs) {// concat拼接一下参数,凑齐了再执行return curried(...args.concat(moreArgs));};}};
}// 下面是使用案例:
function add(...args) {return args.reduce((total, num) => total + num, 0);
}
console.log(sum(1)(2)(3)(4)); // 输出10,相当于调用 add(1, 2, 3, 4)
console.log(sum(1, 2)(3, 4)); // 输出10,同样相当于调用 add(1, 2, 3, 4)

极简精华版:一行代码

原理和上面的一样,但主打一个浓缩和精简,并且通用支持任意形式的传参,足够应付面试场景:

const curry = (fn, ...args) =>args.length >= fn.length ? fn(...args) : (...args) => curry(fn, ...args, ..._args);// 用法示例:
const add = (...nums) => {return nums.reduce((sum, num) => sum + num, 0);
};
const curriedAdd = curry(add);
console.log(curriedAdd(1, 2, 3, 4, 5));
console.log(curriedAdd(1)(2)(3));
console.log(curriedAdd(1)(2)(3)(4)(5)); // 输出:15

魔改升级版:闭包乱炖

还有一种场景的面试场景,就是不只是要单纯地实现柯里化,还要结合更多需求。

考灵活运用也合理,不然手写这些个柯里化又有啥实际意义呢

通常都是围绕着闭包的用法来考,举个简单但是足够经典的例子:

curriedAdd(1)(2)(3)
// 期望它每次调用的时候都能进行输出当前的总和
// 也就是输出三次,分别是:1 3 6

实现的代码如下:

function curry(initial = 0) {let currentSum = initial;const add = (...args) => {return args.reduce((total, num) => total + num, 0);}return function(...args) {currentSum += add(...args);console.log(currentSum); // 输出当前的总和// 如果没有参数传入,返回最终结果;否则返回新的柯里化函数return args.length === 0 ? currentSum : curry(currentSum);};
}// 创建一个柯里化求和并打印中间结果的函数
const curriedAddAndLog = curry();
// 使用示例
curriedAddAndLog(1)(2)(3); // 分别输出:1、3、6
curriedAddAndLog(1)(2, 3)(4) // 1、6、10

如果你不太理解闭包的原理,诸如调用栈、作用域链、outer指针等等概念,也不太了解闭包的实际应用,但又想快速应付面试,那你可以简单地把闭包题目总结为:

  1. 函数套函数。
  2. 两层函数的“夹缝”之间,可以放一些变量,这些变量对于下面那层函数来说,就像是全局变量一般,可以随时用。

按部就班地实现上述两步,再把题目的具体要求往里面一套,一切都变得非常简单而美妙了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/741660.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

逻辑斯特 + 神经网络梯度下降公式推导 + 向量化

全部推导来自吴恩达老师的视频课,下面仅作整理 逻辑斯特 神经网络

Vue+OpenLayers7入门到实战:使用webgl图层叠加超大量Point点要素,解决叠加超过一百万数据量点位导致浏览器卡住变慢的问题

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 本章介绍如何使用OpenLayers7在解决地图上叠加超过几千以上要素点就开始变慢,一万以上的要素点的时候,浏览器页面就开始卡顿或直接卡死,甚至浏览器会弹出是否等待页面加载的提示。 这时候要怎么优化?OpenLayers官…

win10安装Ubuntu22.04LTS及深度学习相关配置详细教学

由于之前Ubuntu系统硬盘空间分配的不够,又去看了一下发现扩容很很麻烦。加以发现自己前面安装的深度学习环境版本与实际要用的不符,所以当机立断决定直接重装系统。 Ubuntu系统安装 参考视频:一看就会!8分钟真机安装【Ubuntu/Wi…

JWT原理

JWT 介绍 JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种简洁的、自包含的方法用于通信双方之间以 JSON 对象的形式安全地传输信息。这种信息可以被验证和信任,因为它是数字签名的。JWT通常用于…

Android Selinux详解[四]--新增服务标签相关

在工作过程中,SElinux常用的有以下几个文件可用于新增标签 可用于加标签的文件名含义对应的声明文件名(一般会声明的地方,根本上放哪里都可以)file_contexts给 文件/目录/节点 新增标签file.tegenfs_contexts给节点新增标签,与上一个不同的是…

SpringBoot总结-基于SpringBoot实现Web开发

原创作者:田超凡(程序员田宝宝) 版权所有,转载请注明原作者,严禁复制转载 3.1、静态资源访问 在我们开发Web应用的时候,需要引用大量的js、css、图片等静态资源。 默认配置 Spring Boot默认提供静态资…

第二证券|沪指窄幅震荡跌0.26%,半导体概念走强,保险板块跌幅居前

13日早盘,沪深两市窄幅震动,三大指数均小幅跌落。盘面上,人工智能方向团体反弹,半导体个股走势活跃。 到午间收盘,沪指跌0.26%,报3047.85点;深成指跌0.15%,报9615.92点;…

ruoyi-vue插件集成websocket

链接:插件集成 | RuoYi WebSocketServer.java:补充代码 /*** 此为广播消息* param message 消息内容*/public void sendAllMessage(String message) {LOGGER.info("【websocket.sendAllMessage】广播消息:"message);try {for(String sessionI…

工作中Promise用法总结

工作中Promise用法总结,后面会持续更新觉得有意义的。 1.构造Promise 一般情况下代码是这样的: async function fn() {return new Promise((resolve, reject) > {let list []resolve(list)} }await fn()最后的返回值,如果promise的状态…

谈谈杭州某小公司面试的经历

#面试#本人bg211本,一段实习,前几天面了杭州某小厂公司,直接给我干无语了! 1、先介绍介绍你自己,我说了我的一个情况。 2、没获奖和竞赛经历吗?我说确实没有呢,面试官叹气了一下,只是…

300分钟吃透分布式缓存-27讲:Redis是如何进行主从复制的?

Redis 复制原理 为了避免单点故障,数据存储需要进行多副本构建。同时由于 Redis 的核心操作是单线程模型的,单个 Redis 实例能处理的请求 TPS 有限。因此 Redis 自面世起,基本就提供了复制功能,而且对复制策略不断进行优化。 通…

基于SWOT的智能手机企业财务战略研究1.62

摘 要 近些年,网络技术日新月异,智能手机深受消费者喜爱,人们通过网络,手机应用,可以极大地方便人们学习,工作等等。由于国家对电信行业的大力支持,中国消费者群体逐步成为最具潜力的手机购买者…

十六、接口隔离原则、反射、依赖注入

接口隔离原则、反射、特性、依赖注入 接口隔离原则 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 五种原则当中的i 上一章中的接口,即契约。 契约就是在说两件事,甲方说自己不会多要,乙方会在…

朴素贝叶斯算法基础——案例:对新闻进行分类

贝叶斯公式 朴素:假设特征与特征之间相互独立 朴素贝叶斯算法:朴素贝叶斯 应用场景:文本分类(单词作为特征) 拉普拉斯平滑系数 Ni:F1词在C类别所有文档中出现的次数 N:所属类别C下的文档所…

《C++游戏编程入门》第2章 真值、分支与游戏循环: Guess My Number

《C游戏编程入门》第2章 真值、分支与游戏循环: Guess My Number 2.1 关系运算符2.2 条件语句02.score_rater.cpp02.score_rater2.cpp02.score_rater3.cpp 2.5 switch语句02.menu_chooser.cpp 2.6 while循环02.play_again.cpp 2.7 do循环02.play_again2.cpp 2.8 break和continu…

AHU 数据库 实验三

《数据库》实验报告 【实验名称】 实验3 数据库的连接查询 【实验目的】 1. 熟悉基本的连接查询的概念和作用; 2. 了解数据库管理系统DBMS 实现连接查询的基本方法; 3. 掌握SQL语言连接查询语句的语法和功能&#…

.NET CORE Aws S3 使用

1.安装指定的包 Install-Package AWSSDK.S3 -Version 3.3.104.10 2.使用帮助类 using System; using System.Collections.Generic; using System.Text; using Amazon; using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; using System.IO; using System.Threadi…

Spring存储基础知识

一、对象存储 1.创建bean对象 public class User {public void sayHi() {System.out.println("hi student");} } 2.bean存入Spring 在spring-config.xml,将 bean(com.spring.demo.User)存到 Spring 容器中,它的名称…

【数据结构学习笔记】选择排序

【数据结构学习笔记】选择排序 参考电子书:排序算法精讲 算法原理 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元…

读取CSV数据并写入MySQL

import pandas as pd #import tushare as ts from sqlalchemy import create_engineimport baostock as bs #### 登陆系统 #### lg bs.login() # 显示登陆返回信息 print(login respond error_code:lg.error_code) print(login respond error_msg:lg.error_msg) #### 获取沪深…