函数式编程中的函数这个术语不是指计算机中的函数;而是指数学中的函数,即自身变量的映射;一个函数的值仅决定于函数参数的值,不依赖其他状态;同时主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果;目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用减少对状态的改变

与命令式的区别

函数式

  • 是面向数学的抽象,将计算描述为一种表达式求值
  • 写表达式的方式来声明我们想干什么,通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。
  • 关心数据的映射
  • 关心类型(代数结构)之间的关系
  • 将程序的描述与求值分离开来。它关注如何用各种表达式来描述程序逻辑
  1. //声明式
  2. var CEOs = companies.map(c => c.CEO);
  3. [0, 1, 2, 3].map(num => Math.pow(num, 2))

命令式

  • 关心解决问题的步骤
  • 指明其控制流或状态关系的变化
  • 通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。命令式代码中频繁使用语句,来完成某个行为。比如 for、if、switch、throw 等这些语句 ```javascript //命令式 var CEOs = []; for(var i = 0; i < companies.length; i++){ CEOs.push(companies[i].CEO) } var array = [0, 1, 2, 3] for(let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2) }

array; // [0, 1, 4, 9]

  1. <a name="b4d3c72e"></a>
  2. ## 特点
  3. <a name="99c21d9a"></a>
  4. ### 一等公民的函数
  5. > 一等公民是指函数跟其它的数据类型一样处于平等地位,可以**赋值**给其他变量,可以作为**参数**传入**另一个函数**,也可以作为别的函数的返回值
  6. ```javascript
  7. // 赋值
  8. var a = function fn1() { }
  9. // 函数作为参数
  10. function fn2(fn) {
  11. fn()
  12. }
  13. // 函数作为返回值
  14. function fn3() {
  15. return function() {}
  16. }

纯函数

相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用

  1. let arr = [1,2,3];
  2. arr.slice(0,3); //是纯函数
  3. arr.splice(0,3); //不是纯函数,对外有影响
  4. function add(x,y){ // 是纯函数
  5. return x + y // 无状态,无副作用,无关时序,幂等
  6. } // 输入参数确定,输出结果是唯一确定
  7. let count = 0; //不是纯函数
  8. function addCount(){ //输出不确定
  9. count++ // 有副作用
  10. }
  11. function random(min,max){ // 不是纯函数
  12. return Math.floor(Math.radom() * ( max - min)) + min // 输出不确定
  13. } // 但注意它没有副作用
  14. function setColor(el,color){ //不是纯函数
  15. el.style.color = color ; //直接操作了DOM,对外有副作用
  16. }

副作用

副作用中的“副”是滋生 bug 的温床;是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

副作用可能包含,但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

函数式编程的哲学就是假定副作用是造成不正当行为的主要原因;也不是非得极致追求“纯”函数;而是说,要让它们在可控的范围内发生

追求“纯”的理由

可缓存性(Cacheable)

纯函数总能够根据输入来做缓存

  1. var squareNumber = memoize(function(x){ return x*x; });
  2. squareNumber(4);
  3. //=> 16
  4. squareNumber(4); // 从缓存中读取输入值为 4 的结果
  5. //=> 16
  6. squareNumber(5);
  7. //=> 25
  8. squareNumber(5); // 从缓存中读取输入值为 5 的结果
  9. //=> 25
  10. var memoize = function(f) {
  11. var cache = {};
  12. return function() {
  13. var arg_str = JSON.stringify(arguments);
  14. cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
  15. return cache[arg_str];
  16. };
  17. };

可移植性/自文档化(Portable / Self-Documenting)

纯函数是完全自给自足的,它需要的所有东西都能轻易获得

可测试性(Testable)

纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了

合理性(Reasonable)

如果一个函数对于相同的输入始终产生相同的结果,那么我们就说它是引用透明

  1. // 非引用透明
  2. var counter = 0
  3. function increment() {
  4. return ++counter
  5. }
  6. // 引用透明
  7. var increment = (counter) => counter + 1

并行代码(Parallel Code)

可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态

应用

高阶函数

  • 函数可以作为参数传递
  • 函数可以作为返回值输出

    函数合成

    两个或多个函数结合起来形成一个新函数;将一节一节的管道连接起来,原始数据经过这一节一节的管道处理之后得到最终结果

  1. var compose = function(f,g) {
  2. return function(x) {
  3. return f(g(x));
  4. };
  5. };
  6. var gen= compose(function(b){console.log(b);return b+"be deal with b";} , function(a){console.log(a);return a+"be deal with a";} )
  7. gen("holle"); //hello hello deal with b;

gen就像一个水管,gen(“holle”);就是往水管里注入了水。
它会从compose左边的函数开始执行,hello是数据流,流过2个函数。第一个函数的返回值会传入到第二个参数继续执行。在函数式编程里,compose是避免命令式的重要一环,fp要做到的每一个函数都是”纯“的,脏的东西交给使用者。把每个纯的函数组合到compose里面,就像gulp的pipe()一样,每个函数就像一个插件,来处理数据流

通用版compose
  1. export default function compose(...funcs) {
  2. if (funcs.length === 0) {
  3. return arg => arg
  4. }
  5. if (funcs.length === 1) {
  6. return funcs[0]
  7. }
  8. return funcs.reduce((a, b) => (...args) => a(b(...args)))
  9. }
  10. // 或者
  11. const pipe = (...functions) => input => functions.reduce(
  12. (acc, fn) => fn(acc),
  13. input
  14. );

pointfree 模式

模式指的是,永远不必说出你的数据

  1. // 非 pointfree,因为提到了数据:name
  2. const pipe = (...functions) => input => functions.reduce(
  3. (acc, fn) => fn(acc),
  4. input
  5. );
  6. pipe("hunter stockton thompson");
  7. // 'H. S. T'

Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。 pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举

函数柯里化

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数;函数式编程的一个过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数

  1. function add(a, b) {
  2. return a + b;
  3. }
  4. console.log(add(1, 2)) // 3
  5. // addCurry 是 add 的柯里化函数
  6. function addCurry(a) {
  7. return function(b) {
  8. return a + b;
  9. }
  10. }
  11. console.log(addCurry(1)(2)); // 3

包含

  1. 编写小模块的代码,可以更轻松的重用和配置
  2. 避免频繁调用具有相同参数的函数
  3. 提前返回

    1. var addEvent = function(el, type, fn, capture) {
    2. if (window.addEventListener) {
    3. el.addEventListener(type, function(e) {
    4. fn.call(el, e);
    5. }, capture);
    6. } else if (window.attachEvent) {
    7. el.attachEvent("on" + type, function(e) {
    8. fn.call(el, e);
    9. });
    10. }
    11. };

    通用curry

    ```javascript function curry(fn, args) { var length = fn.length;

    args = args || [];

    return function() {

    1. var _args = args.slice(0),
    2. arg, i;
    3. for (i = 0; i < arguments.length; i++) {
    4. arg = arguments[i];
    5. _args.push(arg);
    6. }
    7. if (_args.length < length) {
    8. return curry.call(this, fn, _args);
    9. }
    10. else {
    11. return fn.apply(this, _args);
    12. }

    } }

// 或者 function currying(fn,…arg) { if(fn.length === arg.length) { return fn(…arg); } return function (…arg2) { return currying(fn,…arg,…arg2); } }; var fn = curry(function(a, b, c) { console.log([a, b, c]); });

fn(“a”, “b”, “c”) // [“a”, “b”, “c”] fn(“a”, “b”)(“c”) // [“a”, “b”, “c”] fn(“a”)(“b”)(“c”) // [“a”, “b”, “c”] fn(“a”)(“b”, “c”) // [“a”, “b”, “c”]

  1. <a name="03e66149"></a>
  2. ### 函数节流
  3. > 控制函数被触发的频率
  4. ```javascript
  5. function throttle (fn, wait) {
  6. let _fn = fn, // 保存需要被延迟的函数引用
  7. timer,
  8. flags = true; // 是否首次调用
  9. return function() {
  10. let args = arguments,
  11. self = this;
  12. if (flags) { // 如果是第一次调用不用延迟,直接执行即可
  13. _fn.apply(self, args);
  14. flags = false;
  15. return flags;
  16. }
  17. // 如果定时器还在,说明上一次还没执行完,不往下执行
  18. if (timer) return false;s
  19. timer = setTimeout(function() { // 延迟执行
  20. clearTimeout(timer); // 清空上次的定时器
  21. timer = null; // 销毁变量
  22. _fn.apply(self, args);
  23. }, wait);
  24. }
  25. }
  26. window.onscroll = throttle(function() {
  27. console.log('滚动');
  28. }, 500);

分时函数

处理这么多数据的时候,我们可以选择分批进行

  1. function timeChunk(data, fn, count = 1, wait) {
  2. let obj, timer;
  3. function start() {
  4. let len = Math.min(count, data.length);
  5. for (let i = 0; i < len; i++) {
  6. val = data.shift(); // 每次取出一个数据,传给fn当做值来用
  7. fn(val);
  8. }
  9. }
  10. return function() {
  11. timer = setInterval(function() {
  12. if (data.length === 0) { // 如果数据为空了,就清空定时器
  13. return clearInterval(timer);
  14. }
  15. start();
  16. }, wait); // 分批执行的时间间隔
  17. }
  18. }
  19. // 测试用例
  20. let arr = [];
  21. for (let i = 0; i < 100000; i++) { // 这里跑了10万数据
  22. arr.push(i);
  23. }
  24. let render = timeChunk(arr, function(n) { // n为data.shift()取到的数据
  25. let div = document.createElement('div');
  26. div.innerHTML = n;
  27. document.body.appendChild(div);
  28. }, 8, 20);
  29. render();

惰性加载

根据最后的调用的执行返回结果

  1. // 改成
  2. var addEvent = (function(){
  3. if (window.addEventListener) {
  4. return function(el, sType, fn, capture) {
  5. el.addEventListener(sType, function(e) {
  6. fn.call(el, e);
  7. }, (capture));
  8. };
  9. } else if (window.attachEvent) {
  10. return function(el, sType, fn, capture) {
  11. el.attachEvent("on" + sType, function(e) {
  12. fn.call(el, e);
  13. });
  14. };
  15. }
  16. })();

参考

js函数式编程指南
JavaScript 函数式编程(二)
JavaScript专题之函数柯里化
JavaScript专题之函数组合
JavaScript函数柯里化
JavaScript 函数式编程
「译」理解JavaScript的柯里化
高阶函数,你怎么那么漂亮呢
在你身边你左右 —函数式编程别烦恼
什么是函数式编程思维
如何评价阮一峰老师的函数式编程/范畴论教程?