哈喽,我是老鱼!
最近对一个工具库的使用上瘾了!这个给大家分享下。这是每个 JavaScript 程序员都应该掌握的工具:Ramda
简介
Ramda
是一款实用的 JavaScript 函数式编程库。类似的库中,大家最为熟悉的有Underscore、 Lodash等。
这时大家可能会问:
既然 Underscore 和 Lodash 已经这么流行了,为什么还要学习好像雷同的 Ramda 呢?
回答是,Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
Ramda 的目标更为专注:专门为函数式编程风格而设计,更容易创建函数式 pipeline、且从不改变用户已有数据。
对比区分
Underscore 和 Lodash的参数位置不对
,把处理的数据放到了第一个参数。
var square = n => n * n;
_.map([4, 8], square) // [16, 64]
上面代码中,_.map的第一个参数[4, 8]是要处理的数据,第二个参数square是数据要执行的运算。
Ramda 的数据一律放在最后一个参数,理念是"function first,data last"
。
Ramda应该是目前最符合函数式编程的工具库,它需要操作的数据参数都是放在最后的。
var R = require('ramda');
R.map(square, [4, 8]) // [16, 64]
把要操作的数据放在最后一个参数,作为参数排列顺序,不仅满足 Ramda 的核心设计,同时使得所有方法都支持柯里化
。
安装
使用 node:
$ npm install ramda
或从 CDN 上获取:
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.0/ramda.min.js"></script>
然后即可使用:
import * as R from 'ramda'
// OR
const R = require('ramda');
也就是说,所有多参数的函数,默认都可以单参数使用。也可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
// 写法一
R.map(square, [4, 8])
// 写法二
R.map(square)([4, 8])
// 或者
var mapSquare = R.map(square);
mapSquare([4, 8]);
上面代码中,写法一是多参数版本,写法二是柯里化以后的单参数版本。Ramda 都支持,并且推荐使用第二种写法。
今天,接下来是我总结的Ramda的几种常见的使用场景,展示怎样用 Ramda 写出既简洁易读,又方便扩展复用的代码。
使用集合迭代函数代替循环
.forEach
不必写显式for循环,而是用 forEach 函数代替循环。示例如下:
// Replace this:
for (const value of myArray) {
console.log(value+5)
}
// with:
const printXPlusFive = x => console.log(x + 5);
R.forEach(printXPlusFive, [1, 2, 3]); //=> [1, 2, 3]
// logs 6
// logs 7
// logs 8
forEach 接受一个函数和一个数组,然后将函数作用于数组的每个元素。虽然 forEach 是这些函数中最简单的,但在函数式编程中它可能是最少用到的一个。forEach 没有返回值,所以只能用在有副作用的函数调用中。
.map
其实最常用的函数是 map。类似于 forEach,map 也是将函数作用于数组的每个元素。但与 forEach 不同的是,map 将函数的每个返回值组成一个新数组,并将其返回。示例如下:
R.map(x => x * 2, [1, 2, 3])
//=> [2, 4, 6]
//这里使用了匿名函数,但我们也可以在这里使用具名函数:
const double = x => x * 2
R.map(double, [1, 2, 3])
R.map(double, {x: 1, y: 2, z: 3}); //=> {x: 2, y: 4, z: 6}
.filter/ reject
.reject接下来,我们来看看 filter 和 reject。就像名字所示,filter 会根据函数的返回值从数组中选择元素,例如:
const isEven = n => n % 2 === 0;
R.filter(isEven, [1, 2, 3, 4]); //=> [2, 4]
R.filter(isEven, {a: 1, b: 2, c: 3, d: 4}); //=> {b: 2, d: 4}
.filter将函数(本例中为 isEven)作用于数组中的每个元素。每当函数返回 "true" 时,相应的元素将包含到结果中;反之当断言函数返回为 "falsy" 值时,相应的元素将从结果数组中排除(过滤掉)。
reject 是 filter 的补操作。 它保留使断言函数返回 "falsy" 的元素,排除使断言函数返回 "truthy" 的元素。
const isOdd = (n) => n % 2 === 1;
R.reject(isOdd, [1, 2, 3, 4]); //=> [2, 4]
R.reject(isOdd, {a: 1, b: 2, c: 3, d: 4}); //=> {b: 2, d: 4}
.find
find 将函数作用于数组中的每个元素,并返回第一个使函数返回真值的元素。
R.find(isEven, [1, 2, 3, 4]) //=> 2
const xs = [{a: 1}, {a: 2}, {a: 3}];
R.find(R.propEq('a', 2))(xs); //=> {a: 2}
R.find(R.propEq('a', 4))(xs); //=> undefined
.reduce
reduce 比之前遇到的其他函数要复杂一些。
reduce 接受一个二元函数(reducing function)、一个初始值和待处理的数组。
归约函数的第一个参数称为 "accumulator" (累加值),第二个参数取自数组中的元素;返回值为一个新的 "accumulator"。
先来看一个示例,然后看看会发生什么。
const subtract= (accum, value) => accum - value
R.reduce(subtract, 0, [1, 2, 3, 4]) // => ((((0 - 1) - 2) - 3) - 4) = -10
// - -10
// / \ / \
// - 4 -6 4
// / \ / \
// - 3 ==> -3 3
// / \ / \
// - 2 -1 2
// / \ / \
// 0 1 0 1
- reduce 首先将初始值 5 和 数组中的首个元素 1 传入归约函数 subtract,subtract 返回一个新的累加值:0- 1 = -1。
- reduce 再次调用subtract,这次使用新的累加值 -1 和 数组中的下一个元素 2 作为参数subtract返回 -3。
- reduce 再次使用 -3和 数组中的下个元素 3 来调用 subtract,输出 -6。
- reduce 最后一次调用subtract,使用 -6 和 数组中的最后一个元素 4 ,输出 -10。
- reduce 将最终累加值 -10作为结果返回以上关于集合的处理,是大多数库都或多或少涵盖了。这里主要是告知大家 Ramda 使用方法在参数排列的差异。
Ramda更重要的是接下来的这些内容。
函数的组合
Ramda 为简单的函数组合提供了一些函数。这使得我们能操作一些较为复杂的逻辑。
函数的合成
compose:将多个函数合并成一个函数,从右到左执行。
R.compose(Math.abs, R.add(1), R.multiply(2))(-4) // 7
pipe:将多个函数合并成一个函数,从左到右执行。
var negative = x => -1 * x;
var increaseOne = x => x + 1;
var f = R.pipe(Math.pow, negative, increaseOne);
f(3, 4) // -80 => -(3^4) + 1
converge:接受两个参数,第一个参数是函数,第二个参数是函数数组。传入的值先使用第二个参数包含的函数分别处理以后,再用第一个参数处理前一步生成的结果。
var sumOfArr = arr => {
var sum = 0;
arr.forEach(i => sum += i);
return sum;
};
var lengthOfArr = arr => arr.length;
var average = R.converge(R.divide, [sumOfArr, lengthOfArr])
average([1, 2, 3, 4, 5, 6, 7])
// 4
// 相当于 28 除以 7
var toUpperCase = s => s.toUpperCase();
var toLowerCase = s => s.toLowerCase();
var strangeConcat = R.converge(R.concat, [toUpperCase, toLowerCase])
strangeConcat("Yodel")
// "YODELyodel"
// 相当于 R.concat('YODEL', 'yodel')
柯里化
curry:将多参数的函数,转换成单参数的形式。
var addFourNumbers = (a, b, c, d) => a + b + c + d;
var curriedAddFourNumbers = R.curry(addFourNumbers);
var f = curriedAddFourNumbers(1, 2);
var g = f(3);
g(4) // 10
partial:允许多参数的函数接受一个数组,指定最左边的部分参数。
var multiply2 = (a, b) => a * b;
var double = R.partial(multiply2, [2]);
double(2) // 4
var greet = (salutation, title, firstName, lastName) =>
salutation + ', ' + title + ' ' + firstName + ' ' + lastName + '!';
var sayHello = R.partial(greet, ['Hello']);
var sayHelloToMs = R.partial(sayHello, ['Ms.']);
sayHelloToMs('Jane', 'Jones'); //=> 'Hello, Ms. Jane Jones!'
complement:返回一个新函数,如果原函数返回true,该函数返回false;如果原函数返回false,该函数返回true。
var gt10 = x => x > 10;
var lte10 = R.complement(gt10);
gt10(7) // false
lte10(7) // true
函数的执行
binary:参数函数执行时,只传入最前面两个参数。
var takesThreeArgs = function(a, b, c) {
return [a, b, c];
};
var takesTwoArgs = R.binary(takesThreeArgs);
takesTwoArgs(1, 2, 3) // [1, 2, undefined]
tap:将一个值传入指定函数,并返回该值。
var sayX = x => console.log('x is ' + x);
R.tap(sayX)(100) // 100
R.pipe(
R.assoc('a', 2),
R.tap(console.log),
R.assoc('a', 3)
)({a: 1})
// {a: 3}
zipWith:将两个数组对应位置的值,一起作为参数传入某个函数。
var f = (x, y) => {
// ...
};
R.zipWith(f, [1, 2, 3])(['a', 'b', 'c'])
// [f(1, 'a'), f(2, 'b'), f(3, 'c')]
apply:将数组转成参数序列,传入指定函数。
var nums = [1, 2, 3, -99, 42, 6, 7];
R.apply(Math.max)(nums) // 42
还有诸如ascend升序排列、descend降序排列的方法等。
其他
Ramda 还提供了比较运算、数学运算、逻辑运算、字符串、数组、对象等的实用方法。
比如eqBy:比较两个值传入指定函数的运算结果是否相等。
R.eqBy(Math.abs, 5)(-5)
// true
比如divide:返回第一个参数除以第二个参数的商。
R.divide(71)(100) // 0.71
比如test:判断一个字符串是否匹配给定的正则表达式。
R.test(/^x/)('xyz')
// true
R.test(/^y/)('xyz')
// false
比如omit:过滤对象指定属性。
R.omit(['a', 'd'])({a: 1, b: 2, c: 3, d: 4})
// {b: 2, c: 3}
Ramda 提供的方法远不止这些。欢迎大家参阅官方链接。
官方地址:https://ramda.cn/
写在最后
我是老鱼,一名致力于在技术道路上的终身学习者、实践者、分享者!如果我的内容能对你有所帮助,一个赞同和喜欢鼓励一下下~
网友评论