相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)javascript
根据学术上函数的定义,函数便是一种描述集合和集合之间的转换关系,输入经过函数都会返回有且只有一个输出值。函数其实是一个关系,或者说是一种映射,而这种映射关系是能够组合的。前端
在咱们的编程世界中,咱们须要处理的其实也只有“数据”和“关系”,而关系就是函数。咱们所谓的编程工做也不过就是在找一种映射关系,一旦关系找到了,问题就解决了,剩下的事情,就是让数据流过这种关系,而后转换成另外一个数据。java
你能够像对待任何其余数据类型同样对待它们——把它们存在数组里,看成参数传递,赋值给变量...等等。使用总有返回值的表达式而不是语句git
// 函数式编程-函数做为返回参数
const add = (x) => {
return plus = (y) => {
return x + y;
}
};
let plus1 = add(1);
let plus2 = add(2);
console.log(plus1(1)); // 2
console.log(plus2(1)); // 3
复制代码
再也不指示计算机如何工做,而是指出咱们明确但愿获得的结果。与命令式不一样,声明式意味着咱们要写表达式,而不是一步一步的指示。github
以 SQL 为例,它就没有“先作这个,再作那个”的命令,有的只是一个指明咱们想要从数据库取什么数据的表达式。至于如何取数据则是由它本身决定的。之后数据库升级也好,SQL 引擎优化也好,根本不须要更改查询语句。web
这是函数式编程的核心概念:面试
// 比较 Array 中的 slice 和 splice
let test = [1, 2, 3, 4, 5];
// slice 为纯函数,返回一个新的数组
console.log(test.slice(0, 3)); // [1, 2, 3]
console.log(test); // [1, 2, 3, 4, 5]
// splice则会修改参数数组
console.log(test.splice(0, 3)); // [1, 2, 3]
console.log(test); // [4, 5]
复制代码
纯函数是这样一种函数,即相同的输入,永远会获得相同的输出,并且没有任何可观察的反作用。数据库
反作用是指,函数内部与外部互动,产生运算之外的其余结果。 例如在函数调用的过程当中,利用并修改到了外部的变量,那么就是一个有反作用的函数。编程
反作用可能包含,但不限于:segmentfault
可缓存性。
纯函数可以根据输入来作缓存。
可移植性/自文档化。
****
可测试性(Testable)
纯函数让测试更加容易。咱们不须要伪造一个“真实的”支付网关,或者每一次测试以前都要配置、以后都要断言状态(assert the state)。只需简单地给函数一个输入,而后断言输出就行了。
合理性(Reasonable)
不少人相信使用纯函数最大的好处是_引用透明性_(referential transparency)。若是一段代码能够替换成它执行所得的结果,并且是在不改变整个程序行为的前提下替换的,那么咱们就说这段代码是引用透明的。
因为纯函数老是可以根据相同的输入返回相同的输出,因此它们就可以保证老是返回同一个结果,这也就保证了引用透明性。
并行代码
咱们能够并行运行任意纯函数。由于纯函数根本不须要访问共享的内存,并且根据其定义,纯函数也不会因反作用而进入竞争态(race condition)。
面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只须要一个香蕉,但却获得一个拿着香蕉的大猩猩...以及整个丛林
函数只在须要的时候执行,不产生无心义的中间变量。从头至尾都在写函数,只有在最后的时候才经过调用 产生实际的结果。
函数式编程中有两种操做是必不可少的:柯里化(Currying)和函数组合(Compose)
把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
函数式编程 + 柯里化,将提取成柯里化的函数部分配置好以后,可做为参数传入,简化操做流程。
// 给 list 中每一个元素先加 1,再加 5,再减 1
let list = [1, 2, 3, 4, 5];
//正常作法
let list1 = list.map((value) => {
return value + 1;
});
let list2 = list1.map((value) => {
return value + 5;
});
let list3 = list2.map((value) => {
return value - 1;
});
console.log(list3); // [6, 7, 8, 9, 10]
// 柯里化
const changeList = (num) => {
return (data) => {
return data + num
}
};
let list1 = list.map(changeList(1)).map(changeList(5)).map(changeList(-1));
console.log(list1); // [6, 7, 8, 9, 10]
复制代码
返回的函数就经过闭包的方式记住了传入的第一个参数
一次次地调用它实在是有点繁琐,咱们可使用一个特殊的 curry
帮助函数(helper function)使这类函数的定义和调用更加容易。
var curry = require('lodash').curry;
var match = curry(function(what, str) {
return str.match(what);
});
var replace = curry(function(what, replacement, str) {
return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
return ary.filter(f);
});
var map = curry(function(f, ary) {
return ary.map(f);
});
复制代码
上面的代码中遵循的是一种简单,同时也很是重要的模式。即策略性地把要操做的数据(String, Array)放到最后一个参数里。
你能够一次性地调用 curry 函数,也能够每次只传一个参数分屡次调用。
match(/\s+/g, "hello world");
// [ ' ' ]
match(/\s+/g)("hello world");
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
复制代码
这里代表的是一种“预加载”函数的能力,经过传递一到两个参数调用函数,就能获得一个记住了这些参数的新函数。
curry 的用处很是普遍,就像在 hasSpaces
、findSpaces
和 censored
看到的那样,只需传给函数一些参数,就能获得一个新函数。
用 map
简单地把参数是单个元素的函数包裹一下,就能把它转换成参数为数组的函数。
var getChildren = function(x) {
return x.childNodes;
};
var allTheChildren = map(getChildren);
复制代码
只传给函数一部分参数一般也叫作_局部调用_(partial application),可以大量减小样板文件代码(boilerplate code)。
当咱们谈论_纯函数_的时候,咱们说它们接受一个输入返回一个输出。curry 函数所作的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。哪怕输出是另外一个函数,它也是纯函数。
函数组合的目的是将多个函数组合成一个函数。
const compose = (f, g) => {
return (x) => {
return f(g(x));
};
};
复制代码
在 compose
的定义中,g
将先于 f
执行,所以就建立了一个从右到左的数据流。组合的概念直接来自于数学课本,从右向左执行更加可以反映数学上的含义。
全部的组合都有一个特性
// 结合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true
复制代码
因此,若是咱们想把字符串变为大写(假设head
,reverse
,toUpperCase
函数存在),能够这么写:
compose(toUpperCase, compose(head, reverse));
// 或者
compose(compose(toUpperCase, head), reverse);
复制代码
结合律的一大好处是任何一个函数分组均可以被拆开来,而后再以它们本身的组合方式打包在一块儿。关于如何组合,并无标准的答案——咱们只是以本身喜欢的方式搭乐高积木罢了。
pointfree 模式指的是,函数无须说起将要操做的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协做起来很是有助于实现这种模式。
// 非 pointfree,由于提到了数据:word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
复制代码
利用 curry,咱们可以作到让每一个函数都先接收数据,而后操做数据,最后再把数据传递到下一个函数那里去。另外注意在 pointfree 版本中,不须要 word
参数就能构造函数;而在非 pointfree 的版本中,必需要有 word
才能进行一切操做。pointfree 模式可以帮助咱们减小没必要要的命名,让代码保持简洁和通用。
若是在 debug 组合的时候遇到了困难,那么可使用下面这个实用的,可是不纯的 trace
函数来追踪代码的执行状况。
var trace = curry(function(tag, x){
console.log(tag, x);
return x;
});
复制代码
前端领域,咱们能看到不少函数式编程的影子:ES6 中加入了箭头函数,Redux 引入 Elm 思路下降 Flux 的复杂性,React16.6 开始推出 React.memo(),使得 pure functional components 成为可能,16.8 开始主推 Hook,建议使用 pure function 进行组件编写……
若是你收获了新知识,请在作侧边栏第一个按钮用力点一下~
参考文档: