目录基本概念函数式编程要素函数是一等公民纯函数副作用不可变值拷贝带来的问题解决拷贝的性能问题: 持久化数据结构js 中三种编程范式函数式编程的优缺点优点缺点偏函数柯里化(curry)
函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式。它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ 演算为该语言最重要的基础。而且,λ 演算的函数可以接受函数作为输入参数和输出返回值。
以上是维基百科对于函数式编程的定义,用简单的话总结就是“强调以函数使用为主的软件开发风格”。
在抽象的定义之外,从实际出发,JS 的函数式编程有以下几个特点:
我们经常听到这句话,”在 JS 中函数是一等公民“,其具体的含义是,函数具有以下特征:
函数式一等公民的特点是所有函数式编程语言所必须具有的,另一个必备特点则是支持闭包(上面的第二点其实很多时候都利用了闭包)
有且仅有显示数据流:
一个函数要是纯函数,要符合以下几点:
函数内部不能有副作用
对于同样的输入(参数),必定得到同样的输出。
这意味着纯函数不能依赖外部作用域的变量
参考纯函数“仅有显示数据流”的定义,副作用的定义即拥有“隐式数据流”。或者说:
当函数参数为引用类型时,对参数的改变将作用将映射到其本身。
const arr = [1, 2, 3];
const reverse = (arr) => {
arr.reverse();
};
reverse(arr);
console.log(arr); // [3,2,1]
这种操作符合“副作用”的定义:修改了外部变量。破坏了纯函数的显示数据流。
如果真的需要设计对数据的修改,则应该:
const reverse = (arr) => {
const temp = JSON.parse(JSON.stringify(arr));
return temp.reverse();
};
arr = reverse(arr);
通过拷贝实现对外部数据的只读直观且简单,代价则是性能。
对于一个大对象,每次的修改可能只是其中的一个属性,那么每次的拷贝会带来大量的冗余操作。当数据规模大,操作频率高时,会带来严重的性能问题。
拷贝模式的问题根源在于:一个大对象只有一小部分有改变,却要对整个对象做拷贝。
这个情况其实和另一个场景很相似,就是 git。一个项目有很多文件,但我一次可能只修改了其中一个。那么我本次的提交记录是怎样的呢?其处理逻辑就是:将改变部分和不变部分进行分离。
**Git 快照保存文件索引,而不会保存文件本身。变化的文件将拥有新的存储空间+新的索引,不变的文件将永远呆在原地。**而在持久化数据结构中,则是变化的属性的索引,和不变的属性的索引
持久化数据结构最常用的库是 Immutable.js,其详解见下文。
JS 是一种多范式语言,而从前端的发展历史来看,各时段的主流框架,也正对应了三种编程范式:
偏函数的定义简单来说就是,将函数转换为参数更少的函数,也就是为其预设参数。
从 fn(arg1, arg2) 到 fn(arg1)
柯里化函数在偏函数的基础上,不仅减少了函数入参个数,还改变了函数执行次数。其含义就是将一个接收 N 个入参的函数,改写为接受一个入参,并返回接受剩余 N-1 个参数的函数。也就是:
fn(1,2,3) => fn(1)(2)(3)
实现一个柯里化函数也是面试高频内容,其实如果规定了函数入参个数,那么是很容易实现的。例如对于入参个数为 3 的函数,实现如下
const curry = (fn) => (arg1) => (arg2) => (arg3) => fn(arg1, arg2, arg3);
const fn = (a, b, c) => console.log(a, b, c);
curry(fn)(1)(2)(3); // 1 2 3
那么实现通用的 curry 函数的关键就在于:
const curry = (fn) => {
const argLen = fn.length; // 原函数的入参个数
const recursion = (args) =>
args.length >= argLen
? fn(...args)
: (newArg) => recursion([...args, newArg]);
return recursion([]);
};
compose 和 pipe 同样是很常见的工具,一些开源库中也都有自己针对特定场景的实现(如 Redux、koa-compose)。而要实现一个通用的 compose 函数其实很简单,借助数组的 reduce 方法就好
const compose = (funcs) => {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
funcs.reduce(
(pre, cur) =>
(...args) =>
pre(cur(...args))
);
};
const fn1 = (x) => x * 2;
const fn2 = (x) => x + 2;
const fn3 = (x) => x * 3;
const compute = compose([fn1, fn2, fn3]);
// compute = (...args) => fn1(fn2(fn3(...args)))
console.log(compute(1)); // 10
而 pipe
函数与 compose
的区别则是其执行顺序相反,正如其字面含义,就像 linux 中的管道操作符,前一个函数的结果流向下一个函数的入参,所以把 reduce
方法改为 reduceRight
即可:
const pipe = (funcs) => {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
funcs.reduceRight(
(pre, cur) =>
(...args) =>
pre(cur(...args))
);
};
const compute = pipe([fn1, fn2, fn3]);
// compute = (...args) => fn3(fn2(fn1(...args)))
console.log(compute(1)); // 12
在最新的 React 文档中,函数式组件 + hook 写法已经成为官方的首推风格。而这正是基于函数式编程的理念。React 的核心特征是“数据驱动视图”,即UI = render(data)
。
UI 的更新是一定需要副作用的,那么如何保证组件函数的“纯”呢?答案是将副作用在组件之外进行管理,所有的副作用都交由 hooks,组件可以使用 state,但并不拥有 state。
Hooks 相比类组件的优点:
Immutable
是用于达成函数式编程三要素中的“不可变值”。我的初次接触是在 Redux 中使用到,Redux 要求 reducer 中不能修改 state 而是应该返回新的 state,但这仅是一种“规范上的约定”,而不是“代码层面的限制”,而 Immutable 正是用于提供 JS 原生不存在的不可修改的数据结构。
Immutable 提供了一系列自定义数据结构,并提供相应的更新 api,而这些 API 将通过返回新值的方式执行更新。
let map1 = Immutable.Map({});
map1 = map1.set("name", "youky");
console.log(map1);
Immutable 内部的存储参考 字典树(Trie)
实现,在每次修改时,不变的属性将用索引指向原来的值,只对改变的值赋值新的索引。这样更新的效率会比整体拷贝高很多。
Redux 中体现函数式编程模式的也有很多地方:
compose
函数,这是函数式编程中非常基本的工具函数Redux 源码中的 compose 函数实现如下:
export default function compose(): <R>(a: R) => R;
export default function compose<F extends Function>(f: F): F;
export default function compose<A, T extends any[], R>(
f1: (a: A) => R,
f2: Func<T, A>
): Func<T, R>;
export default function compose<A, B, T extends any[], R>(
f1: (b: B) => R,
f2: (a: A) => B,
f3: Func<T, A>
): Func<T, R>;
export default function compose<A, B, C, T extends any[], R>(
f1: (c: C) => R,
f2: (b: B) => C,
f3: (a: A) => B,
f4: Func<T, A>
): Func<T, R>;
export default function compose<R>(
f1: (a: any) => R,
...funcs: Function[]
): (...args: any[]) => R;
export default function compose<R>(...funcs: Function[]): (...args: any[]) => R;
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
);
}
首先是用函数重载来进行类型声明。
在实现其实非常简单:
在 Koa 的洋葱模型中,通过 app.use
添加中间件,会将中间件函数存储于this.middleware
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
通过 koa-compose
模块将所有的中间件组合为一个函数 fn,在每次处理请求时调用
// callback 就是 app.listen 时绑定的处理函数
callback () {
const fn = this.compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
这里的 compose 决定了多个中间件之间的调用顺序,用户可以通过 option 传入自定义的 compose 函数,或默认使用 koa-compose
模块。其源码如下:
function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
同样是先对参数进行判断。与 redux 中的 compose 不同的是,koa 中的中间件是异步的,需要手动调用 next 方法将执行权交给下一个中间件。通过代码可知,中间件中接收的 next 参数实际就是 dispatch.bind(null, i + 1))
也就是 dispatch 方法,以达到递归执行的目的。
这里使用 bind
实际上就是创建了一个偏函数。根据 bind 的定义,在 this 之后传入的若干个参数会在返回函数调用时插入参数列表的最前面。也就是说
const next = dispatch.bind(null, i + 1))
next() // 等价于dispatch(i+1)
函数并不是计算机领域的专有名词。实际上,函数一词最早由莱布尼兹在 1694 年开始使用。
函数式编程的思想背后,其实蕴含了范畴论、群论等数学原理的思想。
以上就是从Immutable.js到Redux函数式编程的详细内容,更多关于Immutable.js Redux函数式编程的资料请关注编程网其它相关文章!
--结束END--
本文标题: 从Immutable.js到Redux函数式编程
本文链接: https://lsjlt.com/news/202968.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0