组团学

ES5进阶-函数式编程

阅读 (355)

一、函数式编程介绍

函数式编程:又称泛函编程,是一种编程范式,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象

函数式编程是一种如今比较流行的编程范式,它主张将函数作为参数进行传递,然后返回一个没有副作用的函数,说白了,就是希望一个函数只做一件事情

这种编程思想涵盖了几个重要的概念:

  • 纯函数
  • 合成(compose)
  • 柯里化
  • 高阶函数

什么是函数式编程?(基本层次回答)

  • 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式
  • 最主要的特征是,函数是第一等公民
  • 强调将计算过程分解成可复用的函数,典型例子就是map方法和reduce方法组合而成 MapReduce 算法
  • 只有纯的、没有副作用的函数,才是合格的函数

什么是函数式编程?(深层次回答)

  • 理解函数式编程的关键,就是理解范畴论。它是一门很复杂的数学,认为世界上所有的概念体系,都可以抽象成一个个的"范畴"(category)
  • 范畴论是集合论更上层的抽象,简单的理解就是"集合 + 函数",理论上通过函数,就可以从范畴的一个成员,算出其他所有成员
  • 我们可以把"范畴"想象成是一个容器,里面包含两样东西[值(value)和值的变形关系,也就是函数]
  • 伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的"函数式编程"
  • 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序
  • 函数式编程要求函数必须是纯的,不能有副作用。因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了
  • 在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用

开发中的优势

  • 可读性强:声明式(Declarative)使得代码即使没有注释也有很高的可读性(这也是 React 的一大卖点)
  • 代码量少:项目越大,越节省代码量(大量的函数被复用)
  • 代码稳定可靠:纯函数的功劳
  • 容易维护性:符合「单一职能原则」,使得代码的维护与迭代更加容易

二、纯函数

  • 介绍

    所谓的纯函数,其实也就是我们说的引用透明,稳定输出,可预测的函数

    纯函数是一种其返回值仅由其参数决定,没有任何副作用的函数。这意味着如果你在整个应用程序中的不同的一百个地放调用一个纯函数相同的参数一百次,该函数始终返回相同的值,纯函数不会更改或读取外部状态

  • 纯函数的特点

    • 无副作用

    是指调用函数的时候不会修改外部的状态,即一个函数调用n次之后依然返回同样的结果

    函数内做了与本身运算无关的事,比如修改某个全局变量的值,或发送 HTTP 请求,甚至函数体内执行 console.log 都算是副作用。函数式编程强调函数不能有副作用,也就是函数要保持纯粹,只执行相关运算并返回值,没有其他额外的行为

    • 透明引用

      一个函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量(外部变量,没有依赖外部变量的值),这个特性与无副作用的特性相呼应

    • 不可变变量

      一个变量一旦创建完成之后,就不能再被修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全,多个线程可以同时访问同一个不可变变量,而不用担心状态不一致的问题,使得并行变得容易实现

      但是由于JavaScript原生不支持不可变变量,需要通过第三方库(比如Immutable.js和Mori等)来实现

      <script type="text/javascript"> // npm install immutable var obj1 = Immutable({a:1}) // obj1.a = 2;//注意不可变 console.log(obj1); // Immutable({a: 1}) </script>
    • 函数是一等公民(函数也是一种数据)

      函数与其他数据类型一样处于平等地位。可以将函数赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。JavaScript中的闭包、高阶函数、函数柯里化和函数组合都是围绕这一特性的应用

  • 实例

    const func = (number) => number * 2; console.log(func(5));
  • 非纯函数实例

    var a = 1 const func = (number) => number * a; console.log(func(5));
    Math.random(); // => 0.3384159509502669 Math.random(); // => 0.9498302571942787 Math.random(); // => 0.9860841663478281
  • 总结

    为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便

三、合成(compose)

函数式编程的一个特点是通过串联函数来求值,然而随着串联函数数量的增加,代码的可读性会不断下降,函数组合就是用来解决这个问题的一个方案

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"

合成也是函数必须是纯的一个原因。因为一个不纯的函数,怎么跟其他函数合成?怎么保证各种合成以后,它会达到预期的行为

截屏2020033116.43.13.png

上图中,XY之间的变形关系是函数fYZ之间的变形关系是函数g,那么XZ之间的关系,就是gf的合成函数gof

合成之前

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <script type="text/javascript"> var X = 1; function f(x){ return x + 1; } function g(y){ return y + 1; } var Y = f(X); var Z = g(Y); console.log("Z =", Z);//Z = 3 </script> </body> </html>

合成之后

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <script type="text/javascript"> var X = 1; var Y = null; var Z = null; function f(x){ return x + 1; } function g(y){ return y + 1; } function compose(f, g){ return function(x){ return g(f(x)) } } fg = compose(f, g); Z = fg(X); console.log("Z =", Z);//Z = 3 </script> </body> </html>

注意:函数的合成还必须满足结合律

截屏2020040109.44.48.png

compose(f, compose(g, h)) // 等同于 compose(compose(f, g), h) // 等同于 compose(f, g, h)

四、柯里化

  • 概念

    在何种里的f(x)g(x)合成为f(g(x)),有一个隐藏的前提,就是fg都只能接受一个参数。如果可以接受多个参数,比如f(x, y)g(a, b, c),函数合成就非常麻烦,这时就需要函数柯里化了。所谓"柯里化",就是把一个多参数的函数,转化为单参数函数,有了柯里化以后,我们就能做到,所有函数只接受一个参数

    柯里化前

    function add(a, b, c) { return a + b + c; } ret = add(1, 2, 3); console.log("ret = %d", ret);

    柯里化后(利用闭包的思想,里层调用外层函数中的变量)

    function curryAdd(a){ return function(b){ return function(c){ return a + b + c; } } } ret = curryAdd(1)(2)(3); console.log("ret = %d", ret);
  • 核心思想(目的)

    降低通用性,提高适用性,从而提高程序性能

截屏2020040110.05.32.png

  • 特点

    • 参数复用:如果是相同的参数,在计算之后不需要再次重新传参计算,提高程序运行效率

截屏2020040110_24_50.png

  • 提前返回:多次调用多次内部判断,可以直接把第一次判断的结果返回外部接收

    每次执行都去判断是什么浏览器

    function addEvent(el, type, fn, capture) { if (window.addEventListener) { //判断是否支持 el.addEventListener(type, fn, capture); } } // 问题,调用四次addEvent,是否会浏览器判断4次??是 addEvent(div, "click", fn, false);//第一次判断的时候已经知道是什么浏览器 addEvent(p, "click", fn, false);//第二次还要判断是什么浏览器 addEvent(span, "click", fn, false);//第三次还要判断是什么浏览器 addEvent(a, "click", fn, false);//第四次还要判断是什么浏览器

    柯里化改造

    function addEvent(el, type, fn, capture) { if (window.addEventListener) { //判断是否支持 return function(el, type, fn, capture) { el.addEventListener(type, fn, capture); } } } var elBind = addEvent(); //判断浏览器,只需判断一次 // 问题,调用四次elBind,是否会浏览器判断4次??否 elBind(div, "click", fn, false); elBind(p, "click", fn, false); elBind(span, "click", fn, false); elBind(a, "click", fn, false);
  • 延迟执行:避免重复的去执行程序,等真正需要结构的时候再去执行,解决多次计算带来的性能问题

    游戏中的成就系统,最后在计算并展示而不是实时显示

    function curryScore(fn) { var __allScore = []; return function() { if (arguments.length === 0) {//不传参数去计算值 fn.apply(null, __allScore); } else {//传参数不计算,把参数都放到数组里面 // arguments本身不具有slice方法,所以借用list的slice方法 __allScore = __allScore.concat([].slice.call(arguments)); } } } var score = 0; var addScore = curryScore(function() { //该函数在没有串参时被回调执行 for (var i = 0; i < arguments.length; i++) { score += arguments[i]; } }); addScore(3); console.log(score); //0 addScore(2); addScore(1); console.log(score); //0 addScore(4); addScore(5); addScore(); //只有调用不传参的方法后才计算得分 console.log(score); //15

    柯里化后

    function curryScore(fn) { var __allScore = []; return function cb(num) { if (!num){ return fn(__allScore); } __allScore.push(num); return cb; } } var score = 0; var addScore = curryScore(function(allScore) { for (var i = 0; i < allScore.length; i++) { score += allScore[i]; } }); addScore(1)(2)(3)(4)(); console.log(score); //10

五、高阶函数

函数式编程倾向于复用一组通用的函数功能来处理数据,它通过使用高阶函数来实现。高阶函数指的是一个函数以函数为参数,或以函数为返回值,或者既以函数为参数又以函数为返回值的函数

使用场景:

  1. 抽象或隔离行为、作用、异步控制流程作为回调函数,比如promises和monads等
  2. 创建可以泛用于各种数据类型的功能
  3. 部分应用于函数参数(偏函数应用)或创建一个柯里化的函数,用于复用或函数复合
  4. 接受一个函数列表并返回一些由这个列表中的函数组合的复合函数

JavaScript是原生支持高阶函数的,例如Array.prototype.map、Array.prototype.filter和Array.prototype.reduce。使用这些高阶函数能让我们的代码变得清晰简洁

map&reduce

Google大名鼎鼎的论文:“MapReduce: Simplified Data Processing on Large Clusters”

截屏2020042315.44.23.png

  • map()函数

    原型:Array.prototype.map(fn)

    作用:将传输的函数fn依次作用到调用该函数的数组中的每个元素,并把结果作为新的数组返回

    var arr = ["1", "3", "4", "6", "9"] function fn(x){ return parseInt(x) } var ret = arr.map(fn) console.log(ret) console.log(ret.length)
  • reduce()函数

    原型:Array.prototype.reduce(fn)

    说明:fn函数必须接受两个参数,reduce把结果继续和序列的下一个元素在fn函数中作累计计算

    var arr = [1, 3, 4, 5, 6] function fn(accumulator, currentValue) { return accumulator * 10 + currentValue } var ret = arr.reduce(fn) console.log(ret) console.log(typeof ret)
  • reduce配合map实现str2int

    function str2int(str){ var arr = new Array(str) function fn1(x) { return parseInt(x) } function fn2(x, y) { return x * 10 + y } return arr.map(fn1).reduce(fn2) } var ret = str2int("1342") console.log(ret, typeof ret)

filter

原型:Array.prototype.filter(fn)

作用:将传输的函数fn依次作用到调用该函数的数组中的每个元素,会返回一个新数组,其中包含所有通过回调函数测试的元素。fn返回true表示该元素通过测试,保留该元素,false则不保留

注意:不会改变原数组,返回过滤后的新数组

var arr = [1, 3, 4, 5, 6] function fn(x){ if (x < 5) { return true } else { return false } } var ret = arr.filter(fn) console.log(ret)

sort

原型:Array.prototype.sort(fn)

作用:fn函数必须接受两个参数,sort会按顺序将挨着的元素放入fn函数中,如果fn函数返回true则交换两个元素的位置,返回false则不交换

var arr = ["sfe", "sghgere", "iuygfcdswd", "fweo", "swegget"] var ret1 = arr.sort() console.log(ret1) // 根据字符串长度排序 function fn(x, y) { if (x.length > y.length) { return true } else { return false } } var ret2 = arr.sort(fn) console.log(ret2)
需要 登录 才可以提问哦