What & Why
- Declarative 声明式
- Imperative 指令式
纯函数的好处:
- 易于测试(上下文无关)
- 可并行计算(时序无关)
- bug 自限性(问题不会扩散)
- 可读性
对于可读性可以看一下这一句话
这里提到的 “乐高片段” 的比喻也很有意思
具体的思考
- 函数的输入
- 函数的组合
- 纯函数、副作用、值的不可变
- 高阶函数
- 列表操作
函数的输入(Input)
- 柯里化(curry)
- 偏函数(partial)
先看定义:
这里的局部应用就是 partial
const curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg) => judge(...args, arg)
const partial =
(fn, ...presetArgs) =>
(...laterArgs) =>
fn( ...presetArgs, ...laterArgs );
在 JavaScript 中,柯里化和偏应用都使用闭包来保存实参,直到收齐所有实参后我们再执行原函数。
用 curry 和 partial 可以做什么?
- 保存函数的参数,对参数做封装,参数复用
- 调整函数签名
看个🌰:
什么是函数签名?接触到了 TS 之后才理解了这个词。
我们使用 curry 和 partial 调整的是入参的数量。
组合
- 实现
- compose 与 pipe
- 结合律
- pointfree
const compose = (...funcs) => {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
一个值要经过多个函数,才能变成另外一个值,通过 compose
将所有中间步骤合并成一个函数。
函数合成就像将管道连起来,让数据一口气从多个管道中穿过。
compose
做的事情就是将上一个函数执行的结果传入作为参数传入下一个函数,入参是单参数,如何将多参数函数进行 compose
?
答案很明显,使用 curry
和 partial
。
看一个🌰:
// 提供该API:ajax( url, data, cb )
var getPerson = partial( ajax, "http://some.api/person" );
var getLastOrder = partial( ajax, "http://some.api/order", { id: -1 } );
// 声明式代码
getLastOrder( function orderFound(order){
getPerson( { id: order.personId }, function personFound(person){
output( person.name );
});
});
var prop =
(name,obj) =>
obj[name];
function setProp(name,obj,val) {
var o = Object.assign( {}, obj );
o[name] = val;
return o;
}
var makeObjProp =
(name,value) =>
setProp( name, {}, value );
var getPerson = partial( ajax, "http://some.api/person" );
var getLastOrder = partial( ajax, "http://some.api/order", { id: -1 } );
var extractName = partial( prop, "name" );
var outputPersonName = compose( output, extractName );
var processPerson = partialRight( getPerson, outputPersonName );
var personData = partial( makeObjProp, "id" );
var extractPersonId = partial( prop, "personId" );
var lookupPerson = compose( processPerson, personData, extractPersonId );
getLastOrder( lookupPerson );
函数式的写法看的不是很习惯,就目前阶段适当对一些函数 compose
,会提高可读性,在业务中使用太 “函数式” 的写法可能并不是很友好。
pipe 函数
compose
函数的执行顺序是从右往左,pipe
函数则是从左往右。
函数的合成遵循结合律
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)
pointfree 风格
无形参风格
顺带提一下参数的英文名:
- arguments 实参
- parameters 形参
// 非 pointfree,因为用到了word
const snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
纯函数、副作用、值的不可变
纯函数必须遵守以下约束。
- 不得改写参数
- 不能调用系统I/O的API
- 不能调用Date.now()或者Math.random()等不纯的方法
只要是跟函数外部环境发生的交互就都是副作用。
我们需要做的不是完全消除副作用,而是让副作用尽可能的少,提高代码的可读性。
当我们在阅读程序的时候,能够清晰明确的识别每一个起因和每一个结果是非常重要的。在某种程度上,通读程序但不能看到因果的直接关系,程序的可读性就会降低。
纯函数的好处
- 能够根据输入来做缓存(memoize)
- 可移植性/self-document
- 可测试性
- 引用透明(如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换)
- 并行代码,不会访问共享内存
引用透明性是指一个函数调用可以被它的输出值所代替,并且整个程序的行为不会改变。换句话说,不可能从程序的执行中分辨出函数调用是被执行的,还是它的返回值是在函数调用的位置上内联的。
function calculateAverage(list) {
var sum = 0;
for (let i = 0; i < list.length; i++) {
sum += list[i];
}
return sum / list.length;
}
var nums = [1,2,4,7,11,16,22];
var avg = calculateAverage( nums );
// 或者用注释的这句 !!!
// var avg = 9;
console.log( "The average is:", avg ); // The average is: 9
JS 中的对象访问有副作用。
function (obj) {
return obj.a;
}
在纯度这个问题上,我们要做到学术与实用主义相平衡。
值的不可变
值的不可变性:当需要改变程序中的状态时,我们不能改变已存在的数据,而是必须创建和跟踪一个新的数据。
常量:一个无法进行重新赋值(reassignment)的变量,无论常量承载何值,该变量都不能使用其他的值被进行重新赋值。但它与值的本质无关。
const x = [ 2 ];
x[0] = 3;
高阶函数
原理是闭包,对函数进行包装
- once
- throttle
- debounce
非常多的应用,具体参考兴趣小组参考资料中的:
- 谈谈我对函数式编程的思考
- 月影的分享-函数式编程:你必须知道的那些事儿
列表操作
对于数组操作相关 API 的掌握需要非常清楚,入参出参牢记。
- map 映射
- filter 过滤
- reduce 归并
根据有无归并的初始值,reduce 分为两种调用行为。