函数式编程中, 常用的函数及使用:
组合函数 (Compose,Pipe)
概念
将需要嵌套执行的函数平铺, 嵌套执行是指将一个函数作为参数传递给另外一个函数, 主要有以下特点:
第一函数接受参数, 其他函数接受的上一个函数的返回值
第一个函数可以接受多个参数, 其他函数只接受一个参数(即上一个函数的返回值)
作用:
实现函数式编程中的 pointfree, 使我们专注于转换而不是数据, 不使用需要的值, 只合成运算
简化代码
设计和抽象功能到具体函数里, 以便复用; 还实现中间件功能, 如 webpack 的 loader,redux 是通过 Compose 函数实现的
Compose 函数实现
自右向左按顺序执行
- let compose=function()
- {
- let args=[].slice.call(arguments);
- return function(s){
- args.reduceRight(function(res, cb){
- return cb(res);
- }, x)
- }
- }
- // 调用
- compose(fn1, fn2, ..)(args); // fn1, 和 fn2 都是函数, args 是第一个函数接受的参数, 最先执行最后边的函数, 依次往左执行
- // ES6 的写法
- const compose= function(...funcs) {
- if (funcs.length === 0) {
- return arg => arg;
- }
- return funcs.reduce((a, b) => (...args) => a(b(...args)));
- }
Pipe 函数实现
Pipe 函数和 Compose 函数几乎一致, 唯一不同的是, Pipe 函数的执行顺序是从左到右
- const pipe= function(...funcs) {
- if(funcs.length==0)
- return args=>args;
- return funcs.reduce((a,b)=> (...args)=> b(a(...args)));
- }
高阶函数
概念
高阶函数时对其他函数进行操作的函数, 可以将它们作为参数或返回值.
简单来说就是
一个函数可以接收另一个函数作为参数
函数可以作为返回值被返回
常用的原生高阶函数介绍
map
在 JavaScript 里, map()方法定义在 Array 中, 它返回一个新的数组, 数组中的元素为原始数组调用函数处理后的值.
语法: array.map(function(currentValue,index,arr), thisValue)
第一个参数 function(currentValue,index,arr)是必须的, 表示要对每个元素操作的函数
- currentValue, 必须. 当前元素的值
- index, 可选. 当前元素的索引值
- arr, 可选. 表示原数组
thisValue 可选. 对象作为该执行回调时使用, 传递给函数, 用作 "this" 的值
举个栗子:
- // 创建一个新数组, 其元素是原数组值的两倍
- let array = [1, 2, 3, 4];
- let newArray = array.map((item) => {
- return item * 2;
- })
- console.log(newArray) // [2, 4, 6, 8]
注意:
map() 不会对空数组进行检测
map() 不会改变原始数组.
reduce
reduce() 方法接收一个函数作为累加器, 数组中的每个值 (从左到右) 开始缩减, 最终计算为一个值.
语法: array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
第一个参数 function(total, currentValue, currentIndex, arr)是必须的, 用于执行每个数组元素的函数
-total 必需. 初始值, 或者计算结束后的返回值
-currentValue, 必需. 当前元素
-currentIndex, 必需. 当前索引
-arr, 可选, 原数组
initialValue, 可选. 传递给函数的初始值
举个栗子: 返回数组元素的累加之和
- let arr = [1, 2, 3, 4];
- const sum = arr.reduce((total, current) => {
- return total + current;
- }, 0);
- console.log(sum); // 10
注意: reduce() 对于空数组是不会执行回调函数的
filter
filter() 方法创建一个新的数组, 新数组中的元素是通过检查指定数组中符合条件的所有元素
语法: array.filter(function(currentValue,index,arr), thisValue)
第一个参数 function(currentValue,index,arr)是必须的, 数组中的每个元素都会执行这个函数
-currentValue, 必需. 当前元素
-currentIndex, 必需. 当前索引
-arr, 可选, 原数组
thisValue, 可选. 对象作为该执行回调时使用, 传递给函数, 用作 "this" 的值. 如果省略了 thisValue ,"this" 的值为 "undefined"
举个栗子: 返回对象数组中 price 高于 10 的元素到一个新数组
- const products = [
- { name: "p1", price: 12 },
- { name: "p2", price: 10 },
- { name: "p3", price: 2 },
- { name: "p4", price: 3 },
- ];
- const newArr = products.filter((f) => f.price> 10);
- console.log(newArr); // [{ name: "p1", price: 12 }]
注意:
filter() 不会对空数组进行检测
filter() 不会改变原始数组.
flat
flat()用于将嵌套的数组拉平, 即数组扁平化, 将多维数组变成一维数组.
语法: array.flat(level);
参数, level 可选, 表示要拉平数组的层数, 默认为 1
举个栗子:
- const arr = [1, 2, 3, [4, 5, [7, 8]]];
- const flatArr = arr.flat(3); // 如果不清楚多维数组有几层, 可使用 Infinity 关键字作为参数
- console.log(flatArr); // [1,2,3,4,5,7,8]
注意:
flat() 不会改变原始数组.
flat 方法尚未在所有浏览器和 node 版本兼容
其他常用函数
缓存函数 memorizion
缓存函数是指将上次的计算结果缓存起来, 下次调用函数时, 如果遇到相同的参数, 就直接返回缓存中的结果, 不必再执行一次运算
原理: 把参数和对应的结果保存到一个对象中, 调用时, 判断参数对应的结果是否存在, 存在就直接返回对象中的数据
实现:
- let memoize = function (func, hasher) {
- const memoize = function (...args) {
- const key = [].slice.call(args);
- var cache = memoize.cache;
- // 参考 Underscore 里 memoize 写法, hasher 也是一个函数, 用于计算 key 的值, 没有就直接使用参数作为 key
- var address = " " + (hasher ? hasher.apply(this, args) : key);
- if (!cache[address]) {
- cache[address] = func.apply(this, args);
- }
- return cache[address];
- };
- memoize.cache = {};
- return memoize;
- };
- // 使用例子
- let add = (a, b) => a + b;
- let adder = memorize(add);
- adder(1, 2);
- adder(1, 2); // 第二次使用相同参数调用时, 将直接返回缓存中的结果
对于重复计算量大的递归调用, 使用缓存函数可以加快速度
柯里化函数 curry
柯里化官方解释是把接受多个参数的函数变换成接受一个单一参数 (最初函数的第一个参数) 的函数, 并且返回接受余下的参数且返回结果的新函数的技术.
简单来说就是: 只传递给函数一部分参数来调用它, 让它返回一个函数去处理剩下的参数. 理解起来比较抽象, 我们来看一个代码示例:
- // 普通函数
- let add = (a, b, c) => {
- return a + b + c;
- };
- // 柯里化函数
- let curryAdd = (a) => {
- return (b) => {
- return (c) => {
- return a + b + c;
- };
- };
- };
- const ret = add(1, 2, 3);
- console.log(ret); // 6
- const ret2 = curryAdd(1)(2)(3);
- console.log(ret2); // 6
以上只是一个非常简单的例子用以说明, 并不通用, 通用的柯里化函数应该达到如下效果:
- curryAdd(1)(2,3)
- curryAdd(1,2,3)
- curryAdd(1,2)(3)
- curryAdd(1)(2)(3)
四种不同调用都得到相同结果
结合上面的例子, 我们可以看出, 柯里化其实是利用闭包将每次传进来的参数储存起来, 然后在最里层的函数里面处理全部逻辑, 做一个简单封装, 就得到如下通用实现:
- // 通过递归来收集参数, 收集完成就执行原始函数本身
- const _curry = (fn, args) => {
- var len = fn.length;
- var args1 = args || [];
- return (...args) => {
- var _args = [].slice.call(args);
- [].push.apply(_args, args1);
- // 如果参数个数小于最初的 fn.length, 则递归调用, 继续收集参数
- if (_args.length < len) {
- return _curry.call(this, fn, _args);
- }
- // 参数收集完毕, 则执行 fn
- return fn.apply(this, _args);
- };
- };
- const curryAdd = _curry(add);
- console.log(curryAdd(1, 2, 3)); // 6
- console.log(curryAdd(1, 2)(3)); // 6
- console.log(curryAdd(1)(2, 3)); // 6
- console.log(curryAdd(1)(2)(3)); // 6
上面 add 的例子太过简单(实际上只是拿来演示, 方便理解柯里化原理), 比较难以体会它实际的好处, 可以再看一个小栗子:
- // 比如前面学习纯函数时举的正则验证的代码, 我们也可以使用柯里化方法来简化, 达到复用参数的目的
- // 这里调换了一下参数顺序, 将可以复用的参数放在前面
- const regValidate = function (reg, value) {
- const v = value.toString();
- return reg.test(v);
- };
- console.log(regValidate(3.22, /^(\+|-)?\d+(\.\d+)?$/ig)); // 验证数字
- const _validate=_curry(regValidate); // 柯里化
- const isNumber=_validate(/^(\+|-)?\d+(\.\d+)?$/ig);
- const isString=_validate(/\s/ig);
- isNumber(3.22); // 使用, 不用每次传入正则表达式, 代码变得更简洁
- isString("sss");
实际工作中, 我们可以使用一些库封装好的柯里化函数, 无需自己去实现, 比如 lodash 库, 已经包含了_.curry()函数
总结
总的来说, 函数式编程是一种编程思维方式, 需要在不断的实践里融会贯通, 像这里的柯里化, 也许我们在工作中很少用到, 通过这些简单的例子也很体会它的作用, 但重要的是学习一种思想, 在遇到复杂问题时, 可以给我们多一种解决思路, 也多提供一种方式帮助我们去优化自己的程序.
前端路漫漫, 每天学一点~~
来源: http://www.jianshu.com/p/cd5da1f1cce7