JS函数式编程:深入理解组合函数与抽象封装
本文深入解析JavaScript函数式编程中的核心概念——组合函数。通过乐高积木类比,阐述如何将多个函数拼接为单一黑盒,利用compose实现任意数量函数的自动组合。文章还探讨了基于递归的compose实现、参数反转技巧,以及如何通过DRY原则进行代码抽象,将命令式逻辑转化为声明式风格,从而提升代码的可读性与可维护性。
前言
这是【JS如何函数式编程】系列文章的第三部分。欢迎点赞和关注,以便持续追踪后续内容。
前两篇文章的链接如下:
《XDM,JS如何函数式编程?看这就够了!(一)》
《XDM,JS如何函数式编程?看这就够了!(二)》
在第二篇中,我们讨论了函数式编程的基础——“偏函数”。偏函数通过封装函数,实现了减少传递参数数量的目的,从而解决了手动指定实参的繁琐问题。
更重要的是:
当函数仅包含一个形参时,我们更容易将它们进行组合。这种单元函数(一元函数)便于后续进行组合函数的构建;
没错,本篇的主题正是“组合函数”。它是函数式编程中极其重要的核心内容。
组合函数
含义
函数编程的过程类似于拼搭乐高积木!
乐高拥有各式各样的零部件,我们将它们组装拼接,可以构建出更大的组件或模型。
函数编程同样拥有各种具备特定功能的函数,我们将它们组装拼接,用于实现某个特定的业务功能。
下面来看一个具体的例子,假设我们要使用这两个函数来分析文本字符串:
function words(str) {
return String( str )
.toLowerCase()
.split( /\s|\b/ )
.filter( function alpha(v){
return /^[\w]+$/.test( v );
} );
}
function unique(list) {
var uniqList = [];
for (let i = 0; i < list.length; i++) {
if (uniqList.indexOf( list[i] ) === -1 ) {
uniqList.push( list[i] );
}
}
return uniqList;
}
var text = "To compose two functions together";
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
wordsUsed;
// ["to", "compose", "two", "functions", "together"]
无需关注细节,只需了解:我们首先使用 words 函数处理 text,随后使用 unique 函数处理前一步处理得到的 wordsFound;
这样的处理过程类似于生产线上的商品加工,即流水线作业。

试想一下,如果你是工厂老板,你会如何优化流程以节约成本?
这里作者提供了一种解决思路:去掉传送带!

即减少中间变量,我们可以这样调用:
var wordsUsed = unique( words( text ) );
wordsUsed
确实,减少了中间变量,代码更加清晰,是否还能进一步优化?
我们还可以进一步将整个处理流程封装到一个函数内部:
function uniqueWords(str) {
return unique( words( str ) );
}
uniqueWords(text)
这样就像一个黑盒,无需关心内部流程,只需知晓这个盒子的输入是什么、输出是什么!输入输出清晰,功能明确,非常“干净”!如图:

与此同时,它还能被复用或继续组装。
我们回到 uniqueWords() 函数的内部,它的数据流也是清晰的:
uniqueWords <-- unique <-- words <-- text
封装盒子
上面的 uniqueWords 封装盒子非常完美,如果要不断封装类似 uniqueWords 的盒子,我们要一个一个地去写吗?
function uniqueWords(str) {
return unique( words( str ) );
}
function uniqueWords_A(str) {
return unique_A( words_A( str ) );
}
function uniqueWords_B(str) {
return unique_B( words_B( str ) );
}
...
因此,一切为了偷懒,我们可以编写一个功能更强大的函数来实现自动封装盒子:
function compose2(fn2,fn1) {
return function composed(origValue){
return fn2( fn1( origValue ) );
};
}
// ES6 箭头函数形式写法
var compose2 =
(fn2,fn1) =>
origValue =>
fn2( fn1( origValue ) );

接着,调用方式就变成了这样:
var uniqueWords = compose2( unique, words );
var uniqueWords_A = compose2( unique_A, words_A );
var uniqueWords_B = compose2( unique_B, words_B );
太清晰了!
任意组合
上面,我们组合了两个函数,实际上我们也可以组合 N 个函数;
finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue

比如用一个 compose 函数来实现(敲重点):
function compose(...fns) {
return function composed(result){
// 拷贝一份保存函数的数组
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
}
// ES6 箭头函数形式写法
var compose =
(...fns) =>
result => {
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
基于前面 uniqueWords(..) 的例子,我们进一步再增加一个函数来处理(过滤掉长度小于等于4的字符串):
function skipShortWords(list) {
var filteredList = [];
for (let i = 0; i < list.length; i++) {
if (list[i].length > 4) {
filteredList.push( list[i] );
}
}
return filteredList;
}
var text = "To compose two functions together";
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
wordsUsed;
// ["compose", "functions", "together"]
这样 compose 函数就有三个入参且都是函数了。我们还可以利用偏函数的特性实现更多:
function skipLongWords(list) { /* .. */ }
var filterWords = partialRight( compose, unique, words ); // 固定 unique 函数 和 words 函数
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
shorterWords( text );
filterWords 函数是一个更具特定功能的变体(根据第一个函数的功能来过滤字符串)。
compose 变体
compose(..)函数非常重要,但我们可能不会在生产环境中直接使用自己写的 compose(..),而更倾向于使用某个库所提供的方案。了解其底层工作的原理,对我们强化理解函数式编程也非常有用。
我们理解下 compose(..) 的另一种变体 —— 递归的方式实现:
function compose(...fns) {
// 拿出最后两个参数
var [ fn1, fn2, ...rest ] = fns.reverse();
var composedFn = function composed(...args){
return fn2( fn1( ...args ) );
};
if (rest.length == 0) return composedFn;
return compose( ...rest.reverse(), composedFn );
}
// ES6 箭头函数形式写法
var compose =
(...fns) => {
// 拿出最后两个参数
var [ fn1, fn2, ...rest ] = fns.reverse();
var composedFn =
(...args) =>
fn2( fn1( ...args ) );
if (rest.length == 0) return composedFn;
return compose( ...rest.reverse(), composedFn );
};
通过递归进行重复的动作比在循环中跟踪运行结果更易懂,这可能需要更多时间去体会;
基于之前的例子,如果我们想让参数反转:
var biggerWords = compose( skipShortWords, unique, words );
// 变成
var biggerWords = pipe( words, unique, skipShortWords );
只需要更改 compose(..) 内部实现这一句就行:
...
while (list.length > 0) {
// 从列表中取第一个函数并执行
result = list.shift()( result );
}
...
虽然只是颠倒参数顺序,这二者没有本质上的区别。
抽象能力
你是否会疑问:什么情况下可以封装成上述的“盒子”呢?
这就很考验 —— 抽象的能力了!
实际上,有两个或多个任务存在公共部分,我们就可以进行封装了。
比如:
function saveComment(txt) {
if (txt != "") {
comments[comments.length] = txt;
}
}
function trackEvent(evt) {
if (evt.name !== undefined) {
events[evt.name] = evt;
}
}
就可以抽象封装为:
function storeData(store,location,value) {
store[location] = value;
}
function saveComment(txt) {
if (txt != "") {
storeData( comments, comments.length, txt );
}
}
function trackEvent(evt) {
if (evt.name !== undefined) {
storeData( events, evt.name, evt );
}
}
在做这类抽象时,有一个原则是,通常被称作 DRY(don't repeat yourself),即便我们要花时间做这些非必要的工作。
抽象能让你的代码走得更远! 比如上例,还能进一步升级:
function conditionallyStoreData(store,location,value,checkFn) {
if (checkFn( value, store, location )) {
store[location] = value;
}
}
function notEmpty(val) { return val != ""; }
function isUndefined(val) { return val === undefined; }
function isPropUndefined(val,obj,prop) {
return isUndefined( obj[prop] );
}
function saveComment(txt) {
conditionallyStoreData( comments, comments.length, txt, notEmpty );
}
function trackEvent(evt) {
conditionallyStoreData( events, evt.name, evt, isPropUndefined );
}
这样 if 语句也被抽象封装了。
抽象是一个过程,程序员将一个名字与潜在的复杂程序片段关联起来,这样该名字就能够被认为代表函数的目的,而不是代表函数如何实现的。通过隐藏无关的细节,抽象降低了概念复杂度,让程序员在任意时间都可以集中注意力在程序内容中的可维护子集上。—— 《程序设计语言》
我们在本系列初始提到:“一切为了创造更可读、更易理解的代码。”
从另一个角度,抽象就是将命令式代码变成声明式代码的过程。从“怎么做”转化成“是什么”。
命令式代码主要关心的是描述怎么做来准确完成一项任务。声明式代码则是描述输出应该是什么,并将具体实现交给其它部分。
比如 ES6 增加的结构语法:
function getData() {
return [1,2,3,4,5];
}
// 命令式
var tmp = getData();
var a = tmp[0];
var b = tmp[3];
// 声明式
var [ a ,,, b ] = getData();
开发者需要对他们程序中每个部分使用恰当的抽象级别保持谨慎,不能太过,也不能不够。
阶段小结
函数组合是为了符合“声明式编程风格”,即关注“是什么”,而非具体“做什么”。
它能将一个函数调用的输出路由跳转到另一个函数的调用上,然后一直进行下去,它借助 compose(..) 或它的变体实现。。
我们期望组合中的函数是一元的(输入输出尽量是一个),这个也是前篇有提到的很重要的一个点。
组合 ———— 声明式数据流 ———— 是支撑函数式编程其他特性的最重要的工具之一!
以上就是JS轻量级函数式编程实现XDM三的全部内容,更多关于JS轻量级函数式编程XDM的资料请关注本站其它相关文章!
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
