JavaScript ES6函数式编程(二):柯里化、偏应用、组合、管道
<p>上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心。这一篇来看看高阶函数的实战场景。</p><p>首先强调两点:</p>
<ul>
<li><strong>注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量</strong></li>
<li><strong>高阶函数思想:以变量作用域作为根基,以闭包为工具来实现各种功能</strong></li>
</ul>
<h3 id="柯里化curry">柯里化(curry)</h3>
<p>定义:<strong>柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程</strong>。</p>
<p>先看个简单的例子,这是一个名为 add 的函数:<code>const add = (x, y) => x + y;</code>调用该函数 <code>add(1, 1)、add(1, 2)、add(1, 3)</code>...很普通,缺乏灵活性。</p>
<p>下面是柯里化实现版本:</p>
<pre><code>const addCurried = x => y => x + y;
</code></pre>
<p>如果我们用一个单一的参数调用 addCurried,<code>const add1 = addCurried(1)</code>它返回一个函数<code>fn = y => 1 + y</code>,在其中 x 值通过闭包缓存下来。接下来,我们继续传参<code>add1(1); add1(2); add1(3)</code>,有没有感觉比上面的 add 灵活。</p>
<p>上面的实现只是针对接收两个参数相加的柯里化函数,接下来正是开始实现个基础的通用的接收两个参数的柯里化函数:</p>
<pre><code>const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn (firstArg, secondArg) ;// 为啥要嵌套那么多呢?基于什么思路呢?思考一下...
};
};
};
</code></pre>
<p>现在可以用如下方式通过 curry 函数把 add 函数转换成一个柯里化版本:</p>
<pre><code>const autoCurriedAdd = curry(add)
autoCurriedAdd(1)(1)// 2
</code></pre>
<p>这里我们已经体会到柯里化的好处了,那么柯里化是怎样实现的呢?看上面 curry 的实现很容易发现,先传入一个接受二元函数,然后返回一个一元函数,当这个一元函数执行后,再返回一个一元函数,再次执行返回的一元函数时,触发最开始那个二元函数的执行。</p>
<p>这里有一个点很重要——执行时机,接收够两个参数(add 函数接收的参数数量)立即执行,也就是说<strong>接收够被柯里化函数的参数数量时触发执行</strong>。</p>
<p>好的,我们已经实现了一个基础的柯里化函数。不过,这个 柯里化函数有很大的局限性——只能用于接收两个参数的函数。我们需要的是被柯里化函数的参数可以任意数量,怎么办呢?还好我们已经知道了被柯里化函数的执行时机——<strong>接收够被柯里化函数的参数数量时触发执行</strong>。下面我们来实现更复杂的柯里化:</p>
<pre><code>// 柯里化函数
const curry = (fn) => {
if (typeof fn !== 'function') {
throw Error('No function provided')
}
return function curriedFn (...args) {
if (fn.length > args.length) {// 未达到触发条件,继续收集参数
return function () {
return curriedFn.apply(null, args.concat([].slice.call(arguments)))
}
}
return fn.apply(null, args)
}
}
</code></pre>
<p>这样,我们就能处理多个参数的函数了。比如:</p>
<pre><code>const multiply = (x, y, z) => x*y*z;
const curryMul = curry(multiply);
const result = curryMul(1)(2)(3); // 1*2*3 = 6
</code></pre>
<h3 id="偏应用partial">偏应用(partial)</h3>
<p>偏应用,又称作部分应用,它允许开发者部分地应用函数参数。实际上,<strong>偏应用是为一个多元函数预先提供部分参数,从而在调用时可以省略这些参数</strong>。</p>
<p>比如我们要在每10ms做一组操作。可以通过 setTimeout 函数以如下方式实现:</p>
<pre><code>setTimeout( () => console.log("Do X task"), 10);
setTimeout( () => console.log("Do Y tash"), 10);
</code></pre>
<p>很显然,我们可以用上面的 curry 函数包装成柯里化函数,实现灵活调用:</p>
<pre><code>// 实现一个二元函数,用于柯里化
const setTimeoutWrapper = (time, fn) => {
setTimeout(fn, time);
}
// 使用 curry 函数封装 setTimeout 来实现一个10ms延迟
const delayTenMs = curry(setTimeoutWrapper)
delayTenMs( () => console.log("Do X task") );
delayTenMs( () => console.log("Do Y task") );
</code></pre>
<p>很棒,也能实现灵活调用。但问题是我们不得不创建 setTimeoutWrapper 一样的封装器,这也是一种开销。下面我们看看偏应用的实现:</p>
<pre><code>// 偏应用函数
const partial = (fn, ...partialArgs) => {
let args = partialArgs
return (...fullArguments) => {
let count = 0
for (let i = 0; i < args.length && count < fullArguments; i++) {
if (args === undefined) {
args = fullArguments
}
}
return fn.apply(null, args)
}
}
</code></pre>
<p>下面用偏应用解决上面的延时10ms问题:</p>
<pre><code>let delayTenMs = partial(setTimeout, undefined, 10);// 注意此处,让我们少创建了一个 setTimeoutWrapper 封装器
delayTenMs( () => console.log("Do X task") )
delayTenMs( () => console.log("Do Y task") );
</code></pre>
<p>现在我们对柯里化有了更清晰的认识。创建偏应用函数时,第一个参数接收一个函数,剩余参数是第一个传入函数所需参数。剩余参数待传入的用<code>undefined</code>占位,执行偏应用函数时填充<code>undefined</code>。</p>
<h3 id="组合compose">组合(compose)</h3>
<p>在了解什么是函数式组合之前,让我们理解组合的概念。</p>
<p>符合“|”被称为管道,它允许我们通过组合一些函数去创建一个能够解决问题的新函数。大致来说,“|”将最左侧的函数输出作为输入发送给最右侧的函数!从技术上讲,该处理过程称为“管道”。</p>
<p>compose 函数:</p>
<pre><code>const compose = (a, b) => (c) => a(b(c))
</code></pre>
<p>compose 函数会首先执行 b 函数,并将 b 的返回值作为参数传递给 a。该函数调用的方向是<strong>从右至左的</strong>(先执行 b,再执行 a)。</p>
<p>可以看到,组合函数 compose 就是传入一些函数。对于传入的函数,我们要求<strong>一个函数只做一件事</strong>。</p>
<p>下面看下如何应用 compose 函数:</p>
<pre><code>// 通过组合计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
let count = (array) => array.length;// 计算长度
const countWords = compose(count, splitIntoSpaces);
countWord("hello your reading about composition"); // 5
</code></pre>
<p>上面的 compose 只能实现两个函数的组合。如何组合更多个函数呢?这就需要借助<code>reduce</code>的威力了:</p>
<pre><code>// 组合多个函数 composeN
const composeN = (...fns) =>
(value) =>
fns.reverse().reduce((acc, fn) => fn(acc), value);
</code></pre>
<h3 id="管道序列pipe">管道/序列(pipe)</h3>
<p>管道和组合的概念很类似,都是串行处理数据。唯一区别就是执行方向:<strong>组合从右向左执行,管道从左向右执行。</strong></p>
<pre><code>// 组合多个函数 pipe
const pipe= (...fns) =>
(value) =>
fns.reduce((acc, fn) => fn(acc), value);
</code></pre>
<p>下面看下如何应用 pipe 函数:</p>
<pre><code>// 通过管道计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
let count = (array) => array.length;// 计算长度
const countWords = pipe(splitIntoSpaces, count);// 注意此处的传参顺序
countWord("hello your reading about composition"); // 5
</code></pre>
<h3 id="总结">总结</h3>
<p>通过这一节的学习,我们知道了高阶函数的一些应用——柯里化、偏应用、组合和管道,每种应用都有特定的应用场景。</p>
<p>其中,柯里化是最常用的一种场景,它的作用是<strong>把一个多参数函数转换为一个嵌套的一元函数的过程</strong>。随着闭包的产生,我们可以灵活的调用。</p>
<p>组合和管道类似,都是<strong>串行处理数据</strong>。传入一个初始数据,通过一系列特定顺序的纯函数处理成我们希望得到的数据。</p>
<p>参考链接:<br>
简明 JavaScript 函数式编程——入门篇</p><br><br>
来源:https://www.cnblogs.com/chenwenhao/p/11708105.html
頁:
[1]