【js进阶7】函数式编程

JavaScript | 2020-07-27 20:26:44 157次 1次

函数式编程是一种思想,我的理解是甚至脱离了计算机中的思想,纯数学上的一个概念,函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤。还有 react 的大当家 Dan 发过的一篇文章--代数效应,就是从函数编程中演变。

如下介绍的几种方法,是常用的一些概念,因为我并不知道什么才是函数式编程~


一、高阶函数

高阶函数英文叫 Higher-order function,它的定义很简单,就是至少满足下列一个条件的函数:

接受一个或多个函数作为输入

输出一个函数

在现有的 js 语言中提供一些高阶函数,比如 mapreducefilterforEach 等方法这些都是常用的。在 react 框架中,也会经常使用高阶函数(高阶组件)进行一些属性代理和反向劫持等操作。

不知道该如何更好的举例,对于反向劫持这种高阶函数的应用,通过反向劫持将原来组件的数据和方法都由高阶函数来完成操控:

class Base{
   // 基类 
}

// 子类
class Com1 extends Base{
    constructor(){
        super()
        this.state = "Com1的数据"
    }
    mount(){
        console.log('Com1子类已加载...')
    }
    render(){
        console.log('Com1子类:', this.state)
    }
}

//模拟数据和一些方法的劫持
let hoc = (component) => class extends component{
    constructor(){
        super()
        this.state = '被劫持后的数据'
    }
    mount(){
        console.log('被劫持的子类...')
        // 可以通过这种方式来决定是否执行被劫持的类的方法
        super.mount()
    }
    render(){
        super.render()
    }
}

let com1 = new (hoc(Com1))

com1.render() // 'Com1子类:',被劫持后的数据
com1.mount()  // 被劫持的子类...  --- super.mount() ---> Com1子类已加载...

高阶函数代理,这种方式模拟不太准确:

class Base{
	
}

class Com1 extends Base{
    constructor(data){
        super()
        this.state = "Com1的数据"
        this.agent = data
    }
    mount(){
        console.log('Com1子类已加载...')
    }
    render(){
        console.log('Com1子类获取代理的值:', this.agent )
        console.log('Com1子类:', this.state)
    }
}

//属性代理
let hoc = (component) => class NewCom extends Base{
    constructor(){
        super()
        this.state = '代理的数据'
    }
    mount(){
        console.log('代理的子类...')
    }
    render(){
        let inner = new component(this.state)
        inner.render()
        inner.mount()
    }
}

let com1 = new (hoc(Com1))
com1.render() // Com1子类获取代理的值: 代理的数据   // Com1子类: Com1的数据
com1.mount()  // Com1子类已加载...    // 代理的子类...


二、纯函数

对于一个纯函数,需要同时满足如下三个条件:

对于同一参数,返回同一结果

完全取决于传入的参数

不会产生副作用

对于同一参数返回同一结果:

let x = 1;
const add = (y) => x + y
console.log(add(2)) //3
console.log(add(2)) //3

上述虽然看似满足条件一,但是当 x 进行修改时,得到的结果会不一致:

let x = 1;
const add = (y) => x + y
console.log(add(2)) //3
x = 2
console.log(add(2)) //4

修改如下,使其变为一个纯函数,同时满足上述三个条件:

const add = (y) => {
    let x = 1;
    return x + y;
}
console.log(add(2)) //3
console.log(add(2)) //3

再比如数组中的 slice 方法和 splice 方法:

var xs = [1,2,3];

// 纯函数
xs.slice(0,2);
//=> [1,2]

xs.slice(0,2);
//=> [1,2]


// 不纯
xs.splice(0,2);
//=> [1,2]

xs.splice(0,2);
//=> [3,4]

有副作用,就是我们在函数中进行了:

  • 发出 HTTP 调用

  • 改变外部数据或者 Dom 状态

  • console.log()

  • Math.random()

  • 获取的当前时间

在我们平时开发中并非所有函数都需要是纯的, 比如操作 DOM 的事件处理程序就不适合纯函数。使用纯函数的目的是为了更好的进行单元测试,函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同,因此,每一个函数都可以被看做独立单元。纯函数是很严格的,有的时候甚至只能进行数据的操作。

纯函数的意义(以下内容摘抄函数式编程指北译文

可缓存性(Cacheable)

var squareNumber  = memoize(function(x){ return x*x; });

squareNumber(4);
//=> 16

squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16

squareNumber(5);
//=> 25

squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25
var memoize = function(f) {
  var cache = {};

  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

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

可测试性(Testable)

合理性(Reasonable)

并行代码


三、compose

对于多个函数嵌套执行并且相互之间需要依赖上个函数的结果,也就是数据在 n 个函数中的流传,比如这种多层嵌套导致代码维护起来很复杂:

f(g(m(arg)))

那么可以通过函数组合的形式来优化代码组织:

compose(f,g,m)(arg)

这种形式的运用现在写项目也是非常常见的一种形式,比如获取到的数据是一个树形数组,中间需要先深拷贝,再转为树结构做一些事情,再转为数组排序等等(实际需求中确实遇到了更复杂的情景),那么此时通过组合函数的形式编写代码是再好不过的了,下面通过几种方式实现一下这个方法,其实思路很明确,所以实现起来都是一样的道理。

遍历

function compose(...fns) {
    return function (res) {
        for (var i = fns.length - 1; i > -1; i--) {
            res = fns[i](res)
        }
        return res
    }
}

递归

function compose(...args) {
    let count = args.length - 1
    let result
    return function fun (...arg1) {
        result = args[count].apply(null, arg1)
        if (count <= 0) {
          return result
        }
        count--
        return fun.call(null, result)
    }
}

reduce

function compose(...funcs){
  return funcs.reduce((a, b) => (...args) => a(b(...args)))  
}

reduceRight

function compose(...funcs){
  return funcs.reduceRight((a, b) => (...args) => b(a(...args)))  
}

测试用例:

function num(num) {
    console.log(1);
    return ++num;
}
function add(num) {
    console.log(2);
    return num + num;
}
function minus(num) {
    console.log(3);
    return num - 1;
}

console.log(compose(minus, add, num)(5));


四、Curry

柯里化将多个参数的一个函数转换成一系列使用一个参数的函数的技术。

比如将一个求和的方法转为柯里化的方式:

let curry = (funct) => (a) => (b) => funct(a, b);

let sum = (a, b) => {
    return a+b
}
// 转为柯里化方式
let curryAdd = curry(sum)
// 每次只传入一个参数
curryAdd(1)(2)

目前这种只是传入两个参数,如果相传多个参数,curry 方法中就要扩展对应的函数返回,那么需要封装为一个公共方法:

function curry(fn, ...args) {
    // 原始函数的参数个数
    const len = fn.length;
    const context = this;

    return function(...innerArg) {
        // 参数收集
        args.push(...innerArg)

        // 参数不到位,继续收集参数
        if (args.length < len) {
            return curry.call(context, fn, ...args);
        }

        // 最终执行
        return fn.apply(this, args);
    }
}

let sum = (a, b, c, d) => {
    return a+b+c+d
}

let curryAdd = curry(sum)
// 每次只传入一个参数
curryAdd(1)(2)(3)(4) // 累加的结果:10

其实这种我个人理解并不是一个通用的逻辑,因为柯里化并没有什么通用的逻辑,而是一种技术手段,必要的时候需要按照这种思路手动实现满足自己场景的一些 curry 方法。

上面这个方式是一次性调用并执行,下面再实现一个延迟调用的例子:

function curry(funct) {
    const args = [];
    return function inner(...rest) {
        if (rest.length === 0) {
            return funct(...args);
        } else {
            args.push(...rest);
            return inner;
        }
    }
}

// 还是上面那个 sum 方法
const add = curry(sum);

add(1,2)(3); // 通过闭包将参数保存记录
add(4)();    // 10

再列举一个常见的例子,对于这种类型的方案以后项目中完全可以采取这种方式,比如判断函数的监听:

const addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();

再比如公共的 ajax 请求有时候需要传入 head 参数,需要从缓存中取或者其他请求,这种写法也避免了多次计算请求:

function header (){
  getStorage({
    key: 'loginUser',
    success (res) {
      //重写此方法
      header = () => ({
        'session-key': XXX,
        'openid': XXX,
        "Content-Type": "application/json"
      });
    }
  });
  return {
    "Content-Type": "application/json"
  };
}

最后当然少不了这道经典的求和题目:

function add(...arg) {
    
    let _adder = function(...innerArg) {
        arg.push(...innerArg);
        return _adder;
    };

    // toString隐式转换  函数参与计算时会执行这个
    _adder.toString = () => arg.reduce((a, b) => a + b);
    return _adder;
}

let s = add(1)(2)(3,4)()
console.log(s(10)) // 20

toString 或者 valueOf 方法都可以实现函数的计算,并且参与计算时如果有 valueOf 优先选择:

function fn() { return 20; }
fn.valueOf = function() { return 50 }
fn.toString = function() { return 30 }

console.log(fn+''); // 50


五、偏函数

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

比如现有的 bind、compose 方法都是属于偏函数的一种。

1人赞

分享到: