zoukankan      html  css  js  c++  java
  • 函数式编程入门

    一、 面向过程编程、面向对象编程、函数式编程概要

    1.命令式编程:即过程式编程。强调怎么做。

    2.面向对象编程: 通过对一类事物的抽象,即class,其中对象是基本单元。常用的是继承方式。 平时会看到生命周期、链式调用。比如react中的类组件。

    3.函数式编程:即声明式编程。强调做什么。更加符合自然语言。常用的是组合的方式。平时看到的数据驱动(响应式编程)。比如react的函数组件+hooks。

    二、函数式编程特性

    1.纯函数:相同的输入,永远会得到相同的输出。即也就是数学函数。

    具体理解两点: 没有副作用(数据不可变): 不修改全局变量,不修改入参。最常见的副作用就是随意操纵外部变量,由于js对象是引用类型。不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。如下:

    // 非纯函数
    const curUser = {
      name: 'Peter'
    }
    
    const saySth = str => curUser.name + ': ' + str; // 引用了全局变量
    const changeName = (obj, name) => obj.name = name; // 修改了输入参数
    changeName(curUser, 'Jay'); // { name: 'Jay' }
    saySth('hello!'); // Jay: hello!
    
    // 纯函数
    const curUser = {
      name: 'Peter'
    }
    

    const saySth = (user, str) => user.name + ': ' + str; // 不依赖外部变量
    const changeName = (user, name) => ({...user, name }); // 未修改外部变量
    const newUser = changeName(curUser, 'Jay'); // { name: 'Jay' }
    saySth(curUser, 'hello!'); // Peter: hello!

    2.通过事例进一步加深对纯函数的理解

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

     

    3.强调使用纯函数的意义是什么,也就是说函数式编程的特性是什么。

     

    更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。

    可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。

    自文档化:由于纯函数没有副作用,所以其依赖很明确,因此更易于观察和理解。配合类型签名(一种注释)更清晰。

    便于测试和优化: 相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。

     

    4.在实际的react开发中,也会看到纯函数的应用。

    三、函数式编程理解

    1.数学函数和范畴学

     

    数学函数:在数学上,学习一次函数、二次函数等一个特点就是输入x通过函数都会返回有且只有一个输出值y。这里的函数充当映射关系的桥梁。这也正是函数式编程要求必须是纯的原因

    范畴:包括值 和 值的变形关系(函数)。即范畴好似一个容器包含这两样东西。

    2.js中的函数

         

           JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。

           

          包括 普通函数 和 剪头函数 。剪头函数(简洁同时this指向定义的时候已经确定)也是函数式编程中的一个使用体现。

    3.命令式与声明式

     前面提到说函数式编程更加符合自然语言,就是因为其用了声明式。举个例子对比一下

    // 命令式-----强调怎么做
    var makes = [];
    for (i = 0; i < cars.length; i++) {
      makes.push(cars[i].make);
    }
    
    // 声明式-----做什么
    var makes = cars.map(function(car){ return car.make; });

    4.高阶函数

       

     高阶函数:参数或返回值为函数的函数。比如可以用于拦截和监控,比如防抖和节流的实际开发中的应用(下面的例子是不考虑this的情况)

     

     防抖的例子:在time的时间内,你连续点击几次,会先清除计时器clearTimeout,当你停下来的时候,根据最后一次点击等待time时间执行func

    function debounce(func, time) {
      let timeout = null;
      return function() {
        if (timeout) {
          clearTimeout(timeout)
        }
    
        timeout = setTimeout(() => {
          timeout = null;
          func.apply(null, arguments) // 假如不考虑this
        }, time);
      }
    }

     

     节流的例子:if语句的执行,取决与setTimeout中对flag值的恢复。time时间到了,就执行一次。这个里面setTimeout()和 func(),一个在前,一个在后,但是执行顺序并不是同步执行。这里的setTimeout是异步的。前面的《理解JS异步》有介绍。

    function throtle(func, time) {
      let flag = true;
      return function () {
        if (flag) {
          flag = false;
          setTimeout(() => {
            flag = true
          }, time);
    
          func.apply(null, arguments) // 假如不考虑this
        }
      }

     

    所以呢,高阶函数的这两个例子也就是声明式的,可以当做工具函数,我们用到的时候调用这两个函数就行。因此这样的函数的名字起的语义化就由为重要。

    5.纯函数

       

       再次提起纯函数:没有副作用(不修改外部变量)+无状态(不依赖外部变量)。具体的跳会前面的介绍查看。


    当了解完函数式编程的基本概念和要点后,然而让我们利用函数式编程可能还无法下手。这时候我们用上几个好的方法规范自己编程更加高效。所以下面的6-11可以说成都是编写函数式程序的方法or工具。

    6.函数柯里化(curry)

     

      a. 什么是柯里化

           

     柯里化指的是将一个多参数的函数拆分成一系列单参数函数。

    // 柯里化之前
    function add(x, y) {
      return x + y;
    }
    
    add(1, 2) // 3
    
    // 柯里化之后----ES5写法
    function add(x) {
      return function (y) {
        return x + y;
      };
    }
    
    add(1)(2) // 3
    
    // 柯里化之后----ES6的写法
    const add = x => y => x + y;

      b. 柯里化的应用

      eg1:获取数组对象的某个属性用柯里化优化

    比如我们有这样一段数据:
    let person = [{name: 'kevin'}, {name: 'daisy'}];
    
    如果我们要获取所有的 name 值,我们可以这样做
    let name = person.map(function (item) {
        return item.name;
    })
    
    不过如果我们有 curry 函数:
    let prop = curry(function (key, obj) {
        return obj[key]
    });
    
    let name = person.map(prop('name'))
    
    我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?
    
    但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了。

    eg2:高级柯里化,可以看看 ramda库中提供的curry,它提供的好像和我们上面提到的概念不一致,原因参数不需要一次只传入一个 &  占位符值R.__  _表示R.__,表示还未传入的参数 。所以可以称作为高级柯里化

    const addFourNumbers = (a, b, c, d) => a + b + c + d;
    
    const g = R.curry(addFourNumbers);
    
    // 每次都单参数也可以
    g(1)(2)(3)
    // 多参数也可以
    g(1)(2, 3)
    g(1, 2)(3)
    g(1, 2, 3)
    g(_, 2, 3)(1)
    g(_, _, 3)(1)(2)
    g(_, _, 3)(1, 2)
    g(_, 2)(1)(3)
    g(_, 2)(1, 3)
    g(_, 2)(_, 3)(1)

      c. 柯里化的实现

    function curry(func) {
      return function curried(...args) {
        if (args.length >= func.length) { // 通过函数的length属性,来获取函数的形参个数
          return func.apply(this, args);
        } else {
          return function (...args2) {
            return curried.apply(this, args.concat(args2));
          };
        }
      }
    }

    7.偏函数

    a.偏函数是什么

    偏函数:固定任意元参数,在平时开发中用到的如下:

    // 假设一个通用的请求 API
    
    const request = (type, url, options) => ...
    
    // GET 请求
    
    request('GET', 'http://a....')
    request('GET', 'http://b....')
    request('GET', 'http://c....')
    
    // POST 请求
    request('POST', 'http://....')
    
    
    // 但是通过部分调用后,我们可以抽出特定 type 的 request
    const get = request('GET');
    get('http://', {..})

    b.柯里化与偏函数的区别:

    • 柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
    • 偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

    c.偏函数的实现

    function partial(fn) {
      let args = [].slice.call(arguments, 1);
      return function () {
        const newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
      };
    }

    8.惰性函数

     

    a. 惰性函数是什么

    惰性函数解决每次都要进行判断的这个问题,解决办法是重写函数.

     

    eg:我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

    // 普通方法 : 一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断
    let t;
    function foo() {
        if (t) return t;
        t = new Date()
        return t;
    }
    
    // 闭包 : 没有解决调用时都必须进行一次判断的问题
    let foo = (function() {
        let t;
        return function() {
            if (t) return t;
            t = new Date();
            return t;
        }
    })();
    
    // 函数对象: 依旧没有解决调用时都必须进行一次判断的问题
    function foo() {
        if (foo.t) return foo.t;
        foo.t = new Date();
        return foo.t;
    }
    
    // 惰性函数 : 以上两个存在问题都解决了 (只需要判断一次)
    let foo = function() {
        let t = new Date();
        // 重写 foo函数
        foo = function() {
            return t;
        };
        return foo();
    };

    b.惰性函数的应用

     

    eg:DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断。

    // 普通写法----问题在于我们每当使用一次 addEvent 时都会进行一次判断
    function addEvent (type, el, fn) {
        if (window.addEventListener) {
            el.addEventListener(type, fn, false);
        }
        else if(window.attachEvent){
            el.attachEvent('on' + type, fn);
        }
    }
    
    // 惰性函数写法----判断一次
    function addEvent (type, el, fn) {
        if (window.addEventListener) {
            addEvent = function (type, el, fn) {
                el.addEventListener(type, fn, false);
            }
        }
        else if(window.attachEvent){
            addEvent = function (type, el, fn) {
                el.attachEvent('on' + type, fn);
            }
        }
        addEvent(type, el, fn);
    }
    
    // 或者使用闭包-----判断一次
    let addEvent = (function(){
        if (window.addEventListener) {
            return function (type, el, fn) {
                el.addEventListener(type, fn, false);
            }
        }
        else if(window.attachEvent){
            return function (type, el, fn) {
                el.attachEvent('on' + type, fn);
            }
        }
    })();

    9.函数组合

    a.什么是函数组合

    函数组合将多个函数合成一个函数,同时也遵循数学上的结合律。

    const compose = (f, g) => x => f(g(x));
    
    const f = x => x + 1;
    const g = x => x * 2;
    
    const fg = compose(f, g);
    fg(1) //3
    
    // 函数组合满足结合律
    compose(f, compose(g, t)) = compose(compose(f, g), t) = f(g(t(x)));

    b. 函数组合的应用

    eg:我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'

    // 使用组合前
    let toUpperCase = function(x) { return x.toUpperCase(); };
    let hello = function(x) { return 'HELLO, ' + x; };
    let greet = function(x){
        return hello(toUpperCase(x));
    };
    greet('kevin');
    
    // 使用组合后----代码从右向左运行
    let compose = function(f,g) {
        return function(x) {
            return f(g(x));
        };
    };
    let greet = compose(hello, toUpperCase);
    greet('kevin');

     

    eg:   比如将数组最后一个元素大写,假设 log, head,reverse,toUpperCase 函数存在

    const upperLastItem = compose(log, toUpperCase, head, reverse);
    
    // 也可以如下组合:
    
    // 组合方式 1
    const last = compose(head, reverse);
    const shout = compose(log, toUpperCase);
    const shoutLast = compose(shout, last);
    
    // 组合方式 2
    const lastUppder = compose(toUpperCase, head, reverse);
    const logLastUpper = compose(log, lastUppder);


    c. 函数组合的优点

       

    通过上面的例子看出,不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。

    d. 函数组合的实现

    // 从右向左结合
    const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args);

    e.pointfree是什么

    pointfree 指的是函数无须提及将要操作的数据是什么样的。pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用

    // 非 pointfree,因为提到了数据:name
    let initials = function (name) {
      return name.split(' ').map(compose(toUpperCase, head)).join('. ');
    };
    
    // pointfree
    let initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));
    
    initials("hunter stockton thompson");

    10.函数记忆

    a. 函数记忆是什么

       

    函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

    function add(a, b) {
        return a + b;
    }
    
    // 假设 memorize 可以实现函数记忆
    let memorizedAdd = memoize(add);
    
    memorizedAdd(1, 2) // 3
    memorizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次,这样可以优化性能

    b. 函数记忆的应用

     eg:  以斐波那契数列为例(如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆)

    // 未使用前
    let count = 0;
    let fibonacci = function(n){
        count++;
        return n < 2? n : fibonacci(n-1) + fibonacci(n-2);
    };
    for (let i = 0; i <= 10; i++){
        fibonacci(i)
    }
    console.log(count) // 453
    
    // 使用函数记忆后
    let count = 0;
    let fibonacci = function(n) {
        count++;
        return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
    };
    
    fibonacci = memorize(fibonacci);
    
    for (let i = 0; i <= 10; i++) {
        fibonacci(i)
    }
    console.log(count) // 12

    c. 函数记忆的实现

    function memorize(fn) {
      const cache = Object.create(null); // 存储缓存数据的对象
      return function (...args) {
        const _args = JSON.stringify(args);
        return cache[_args] || (cache[_args] = fn.apply(fn, args));
      };
    };

    11.函子

    Functor函子;Maybe函子;Monad函子;基础概念的理解参考此博文

    最后,更多学习可以结合Ramda库

  • 相关阅读:
    3.19 DAY2
    3.18 DAY1
    MySql Scaffolding an Existing Database in EF Core
    asp.net core 2.0 后台定时自动执行任务
    c#中枚举类型 显示中文
    fullCalendar使用经验总结
    Web APP 日期选择控件
    【转】剖析异步编程语法糖: async和await
    【转】Entity Framework 复杂类型
    【转】EF Code First 学习笔记:约定配置
  • 原文地址:https://www.cnblogs.com/xiaoeshuang/p/14452485.html
Copyright © 2011-2022 走看看