在函数式编程中,经常遇到需要 链式调用函数 (chaining function calls) 的情况,例如 Express 、Koa 、Redux 的中间件。在 MDN 文档上有一个例子:
const double = (n) => n * 2;
const increment = (n) => n + 1;
double(increment(double(double(5)))); // 42
Compose function
很明显上面这样写就出现了深度嵌套的情形,不利于后期维护。在这个基础上,出现了一个叫 compose-function
的第三方模块。
https://www.npmjs.com/package/compose-function
使用 compose-function
很大程度上可以避免深度嵌套的情形,例如需要按顺序调用下面几个函数,利用 compose-function
就可以这样写:
import compose from 'compose-function'
class Person {
constructor() {
this.name = "";
this.age = "";
}
setName = function(name) {
this.name = name;
}
setAge = function(age) {
this.age = age;
}
}
function nameMiddleWare(person) {
person.setName("dby");
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
function ageMiddleWare(person) {
person.setAge(13);
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
function loggerMiddleWare(person) {
console.log(`My name is ${person.name}, age is ${person.age}`);
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
const middlewares = [
nameMiddleWare,
ageMiddleWare,
loggerMiddleWare
].reverse();
const composeFn = compose(...middlewares);
composeFn(new Person());
使用 reduce 实现 Compose Function
分析了一下上面的逻辑,首先调用 middlewares
里面第一个函数并传递初始值,拿到返回值后继续传递给第二个函数,以此类推。这个逻辑可以通过数组的 reduce
方法实现:
function compose<T>(...middlewares: ((arg: T) => T)[]): (initValue: T) => T {
return (initValue) => {
return middlewares.reduce((accu, cur) => {
return cur(accu);
}, initValue)
}
}
function increment(n: number) {
return n + 1;
}
function double(n: number) {
return n * 2;
}
const middlewares = [
double,
double,
increment,
double
]
const composeFn = compose<number>(...middlewares);
composeFn(5); // 42
管道操作符
此外,ES 在语言层面上也加了一个特性,也就是 管道操作符 (pipeline operator) 。使用管道操作符就可以简化 compose function
的用法,语法如下:
expression |> function
管道运算符左边接收一个表达式,右边接收一个函数。管道运算符可以将表达式的值作为唯一参数通过管道传递给函数,允许以可读的方式创建函数的链式调用,其本质是语法糖。下面两种写法是等效的:
let url = "%21" |> decodeURI;
let url = decodeURI("%21");
有了这个之后,上面那个例子就可以这样写:
class Person {
constructor() {
this.name = "";
this.age = "";
}
setName = function(name) {
this.name = name;
}
setAge = function(age) {
this.age = age;
}
}
function nameMiddleWare(person) {
person.setName("dby");
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
function ageMiddleWare(person) {
person.setAge(13);
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
function loggerMiddleWare(person) {
console.log(`My name is ${person.name}, age is ${person.age}`);
return person; // 中间件在执行完操作后,将 person 实例继续传递下去
}
// 因为中间件每次调用之后都会返回传入的实例,因此可以这样链式调用
new Person() |> nameMiddleWare |> ageMiddleWare |> loggerMiddleWare
但是管道操作符作为 experimental feature,还在 stage 1 阶段,现在所有的浏览器都不兼容:
如果要使用这种语法特性,可以在 Nodejs 环境下,启用 @babel/plugin-proposal-pipeline-operator
插件:
https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator
参考:
compose-function - npm
函数式编程中的compose
Pipeline operator (|>) - MDN
@babel/plugin-proposal-pipeline-operator - babel