JavaScript 函数式编程

应该不少童鞋都听过函数式编程(Functional programming)的概念吧,可能有的童鞋有据说过函数式编程但并非特别了解,但其实在咱们的开发过程当中,或多或少都已经应用了函数式编程的思想。编程

相对于面向对象编程(Object-oriented programming)关注的是数据而言,函数式编程关注的则是动做,其是一种过程抽象的思惟,就是对当前的动做去进行抽象。数组

好比说我要计算一个数 加上 4 再乘以 4 的值,按照正常写代码的逻辑,咱们可能会这么去实现bash

function calculate(x){
    return (x + 4) * 4;
}

console.log(calculate(1))  // 20
复制代码

这是没有任何问题的,咱们在平时开发的过程当中会常常将须要重复的操做封装成函数以便在不一样的地方可以调用。但从函数式编程的思惟来看的话,咱们关注的则是这一系列操做的动做,先「加上 4」再「乘以 4」。数据结构

如何封装函数才是最佳实践呢?如何封装才能使函数更加通用,使用起来让人感受更加舒服呢?函数式编程或许能给咱们一些启发。app

函数式编程具备两个基本特征。dom

  • 函数是第一等公民
  • 函数是纯函数

函数是第一等公民

第一等公民是指函数跟其它的数据类型同样处于平等地位,能够赋值给其余变量,能够做为参数传入另外一个函数,也能够做为别的函数的返回值。函数式编程

// 赋值
var a = function fn1() {  }
// 函数做为参数
function fn2(fn) {
    fn()
}   
// 函数做为返回值
function fn3() {
    return function() {}
}
复制代码

函数是纯函数

纯函数是指相同的输入总会获得相同的输出,而且不会产生反作用的函数。函数

从纯函数的概念咱们能够知道纯函数具备两个特色:优化

  • 同输入同输出
  • 无反作用

无反作用指的是函数内部的操做不会对外部产生影响(如修改全局变量的值、修改 dom 节点等)。ui

// 是纯函数
function add(x,y){
    return x + y
}
// 输出不肯定,不是纯函数
function random(x){
    return Math.random() * x
}
// 有反作用,不是纯函数
function setColor(el,color){
    el.style.color = color ;
}
// 输出不肯定、有反作用,不是纯函数
var count = 0;
function addCount(x){
    count+=x;
    return count;
}
复制代码

函数式编程具备两个最基本的运算:合成(compose)和柯里化(Currying)。

函数合成(compose)

函数合成指的是将表明各个动做的多个函数合并成一个函数。

上面讲到,函数式编程是对过程的抽象,关注的是动做。以上面计算的例子为例,咱们关注的是它的动做,先「加上 4」再「乘以 4」。那么咱们的代码实现以下

function add4(x) {
    return x + 4
}
function multiply4(x) {
    return x * 4
}

console.log(multiply4(add4(1)))  // 20
复制代码

根据函数合成的定义,咱们可以将上述表明两个动做的两个函数的合成一个函数。咱们将合成的动做抽象为一个函数 compose,这里能够比较容易地知道,函数 compose 的代码以下

function compose(f,g) {
    return function(x) {
        return f(g(x));
    };
}
复制代码

因此咱们能够经过以下的方式获得合成函数

var calculate=compose(multiply4,add4);  //执行动做的顺序是从右往左

console.log(calculate(1))  // 20
复制代码

能够看到,只要往 compose 函数中传入表明各个动做的函数,咱们便能获得最终的合成函数。但上述 compose 函数的局限性是只可以合成两个函数,若是须要合成的函数不止两个呢,因此咱们须要一个通用的 compose 函数。

这里我直接给出通用 compose 函数的代码

function compose() {
  var args = arguments;
  var start = args.length - 1;
  return function () {
    var i = start - 1;
    var result = args[start].apply(this, arguments);
    while (i >= 0){
      result = args[i].call(this, result);
      i--;
    }
    return result;
  };
}
复制代码

让咱们来实践下上述通用的 compose 函数~

function addHello(str){
    return 'hello '+str;
}
function toUpperCase(str) {
    return str.toUpperCase();
}
function reverse(str){
    return str.split('').reverse().join('');
}

var composeFn=compose(reverse,toUpperCase,addHello);

console.log(composeFn('ttsy'));  // YSTT OLLEH
复制代码

上述过程有三个动做,「hello」、「转换大写」、「反转」,能够看到经过 compose 将上述三个动做表明的函数合并成了一个,最终输出了正确的结果。

函数柯里化(Currying)

在维基百科中对柯里化的定义是:在计算机科学中,柯里化,又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。

柯里化函数则是将函数柯里化以后获得的一个新函数。由上述定义可知,柯里化函数有以下两个特性:

  • 接受一个单一参数
  • 返回接受余下的参数并且返回结果的新函数

举个例子~

function add(a, b) {
    return a + b;
}

console.log(add(1, 2)) // 3
复制代码

假设函数 add 的柯里化函数是 addCurry,那么从上述定义可知,addCurry(1)(2) 应该实现与上述代码相同的效果,输出 3 。这里咱们能够比较容易的知道,addCurry 的代码以下

// addCurry 是 add 的柯里化函数
function addCurry(a) {
    return function(b) {
        return a + b;
    }
}

console.log(addCurry(1)(2));  // 3
复制代码

但假设若是有一个函数 createCurry 可以实现柯里化,那么咱们即可以经过下述的方式来得出相同的结果

// createCurry 返回一个柯里化函数
var addCurry=createCurry(add);

console.log(addCurry(1)(2));  // 3
复制代码

能够看到,函数 createCurry 传入一个函数 add 做为参数,返回一个柯里化函数 addCurry,函数 addCurry 可以处理 add 中的剩余参数。这个过程称为函数柯里化,咱们称 addCurry 是 add 的柯里化函数。

那么,怎么获得实现柯里化的函数 createCurry 呢?这里我直接给出 createCurry 的代码

// 参数只能从左到右传递
function createCurry(func, arrArgs) {
    var args=arguments;
    var funcLength = func.length;
    var arrArgs = arrArgs || [];

    return function(param) {
        var allArrArgs=arrArgs.concat([param])

        // 若是参数个数小于最初的func.length,则递归调用,继续收集参数
        if (allArrArgs.length < funcLength) {
            return args.callee.call(this, func, allArrArgs);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, allArrArgs);
    }
}
复制代码

咱们能够经过以下方式去调用

// createCurry 返回一个柯里化函数
var addCurry=createCurry(function(a, b, c) {
    return a + b + c;
});

console.log(addCurry(1)(2)(3));  // 6
复制代码

上述 createCurry 函数已经可以实现柯里化的过程,可是其并无那么完美,若是我但愿以 addCurry(1, 2)(3) 的方式来调用呢?则上述代码并不能给出咱们想要的结果,因此咱们要对 createCurry 作一个优化,优化后的 createCurry 代码以下

// 参数只能从左到右传递
function createCurry(func, arrArgs) {
    var args=arguments;
    var funcLength = func.length;
    var arrArgs = arrArgs || [];

    return function() {
        var _arrArgs = Array.prototype.slice.call(arguments);
        var allArrArgs=arrArgs.concat(_arrArgs)

        // 若是参数个数小于最初的func.length,则递归调用,继续收集参数
        if (allArrArgs.length < funcLength) {
            return args.callee.call(this, func, allArrArgs);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, allArrArgs);
    }
}
复制代码

优化以后的 createCurry 函数则显得更增强大

// createCurry 返回一个柯里化函数
var addCurry=createCurry(function(a, b, c) {
    return a + b + c;
});

console.log(addCurry(1)(2)(3));  // 6
console.log(addCurry(1, 2, 3));  // 6
console.log(addCurry(1, 2)(3));  // 6
console.log(addCurry(1)(2, 3));  // 6
复制代码

柯里化其实是把简答的问题复杂化了,可是复杂化的同时,咱们在使用函数时拥有了更加多的自由度。

那么,柯里化有什么用途呢?举个例子~

如今咱们须要实现一个功能,将一个全是数字的数组中的数字转换成百分数的形式。按照正常的逻辑,咱们能够按以下代码实现

function getNewArray(array) {
    return array.map(function(item) {
        return item * 100 + '%'
    })
}

console.log(getNewArray([1, 0.2, 3, 0.4]));   // ['100%', '20%', '300%', '40%']
复制代码

而若是经过柯里化的方式来实现

function map(func, array) {
    return array.map(func);
}
var mapCurry = createCurry(map);
var getNewArray = mapCurry(function(item) {
    return item * 100 + '%'
})

console.log(getNewArray([1, 0.2, 3, 0.4]));   // ['100%', '20%', '300%', '40%']
复制代码

上述例子可能太简单以至不能表现出柯里化的强大,具体柯里化的使用还须要结合具体的场景,我的以为没有必要为了柯里化而柯里化,咱们最终的目的是为了更好地解决问题,不是么?

在函数式编程中,还有一个很重要的概念是函子。

函子

在前面函数合成的例子中,执行了先「加上 4」再「乘以 4」的动做,咱们能够看到代码中是经过 multiply4(add4(1)) 这种形式来实现的,若是经过 compose 函数,则是相似于 compose(multiply4,add4)(1) 这种形式来实现代码。

而在函数式编程的思惟中,除了将动做抽象出来外,还但愿动做执行的顺序更加清晰,因此对于上面的例子来讲,更但愿是经过以下的形式来执行咱们的动做

fn(1).add4().multiply4()
复制代码

这时咱们须要用到函子的概念。

function Functor(val){
    this.val = val;
}
Functor.prototype.map=function(f){
    return new Functor(f(this.val));
}
复制代码

函子能够简单地理解为有用到 map 方法的数据结构。如上 Functor 的实例就是一个函子。

在函子的 map 方法中接受一个函数参数,而后返回一个新的函子,新的函子中包含的值是被函数参数处理事后返回的值。该方法将函子里面的每个值,映射到另外一个函子。

经过 Functor 函子,咱们能够经过以下的方式调用

console.log((new Functor(1)).map(add4).map(multiply4))  // Functor { val: 20 }
复制代码

上述调用的方式是 (new Calculate(1)).map(add4).map(multiply4) ,跟咱们想要的效果已经差很少了,可是咱们不但愿有 new 的存在,因此咱们在 Functor 函子挂载上 of 方法

function Functor(val){
    this.val = val;
}
Functor.prototype.map=function(f){
    return new Functor(f(this.val));
}
Functor.of = function(val) {
    return new Functor(val);
}
复制代码

最终咱们能够经过以下方式调用

console.log(Functor.of(1).map(add4).map(multiply4))  // Functor { val: 20 }
复制代码

接下来介绍各类常见的函子。

Maybe 函子

Maybe 函子是指在 map 方法中增长了对空值的判断的函子。

因为函子中的 map 方法中的函数参数会对函子内部的值进行处理,因此当传入函子中的值为空(如 null)时,则可能会产生错误。

function toUpperCase(str) {
    return str.toUpperCase();
}

console.log(Functor.of(null).map(toUpperCase));  // TypeError
复制代码

Maybe 函子则在 map 方法中增长了对空值的判断,如果函子内部的值为空,则直接返回一个内部值为空的函子。

function Maybe(val){
    this.val = val;
}
Maybe.prototype.map=function(f){
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
Maybe.of = function(val) {
    return new Maybe(val);
}
复制代码

当使用 Maybe 函子时传入空值则不会报错

console.log(Maybe.of(null).map(toUpperCase));  // Maybe { val: null }
复制代码

Either 函子

Either 函子是指内部有分别有左值(left)和右值(right),正常状况下会使用右值,而当右值不存在的时候会使用左值的函子。

function Either(left,right){
    this.left = left;
    this.right = right;
}
Either.prototype.map=function(f){
    return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right);
}
Either.of = function(left,right) {
    return new Either(left,right);
}
复制代码

以下当左右值都存在的时候则以右值为函子的默认值,当右值不存在是则以左值为函子的默认值。

function addOne(x) {
    return x+1;
}

console.log(Either.of(1,2).map(addOne));  // Either { left: 1, right: 3 }
console.log(Either.of(3,null).map(addOne));  // Either { left: 4, right: null }
复制代码

Monad 函子

Monad 函子是指可以将函子多层嵌套解除的函子。

咱们往函子传入的值不只仅能够是普通的数据类型,也能够是其它函子,当往函子内部传其它函子的时候,则会出现函子的多层嵌套。以下

var functor = Functor.of(Functor.of(Functor.of('ttsy')))

console.log(functor);  // Functor { val: Functor { val: Functor { val: 'ttsy' } } }
console.log(functor.val);  // Functor { val: Functor { val: 'ttsy' } }
console.log(functor.val.val);  // Functor { val: 'ttsy' }
复制代码

Monad 函子中新增了 join 和 flatMap 方法,经过 flatMap 咱们可以在每一次传入函子的时候都将嵌套解除。

Monad.prototype.map=function(f){
    return Monad.of(f(this.val))
}
Monad.prototype.join=function(){
    return this.val;
}
Monad.prototype.flatMap=function(f){
    return this.map(f).join();
}
Monad.of = function(val) {
    return new Monad(val);
}
复制代码

经过 Monad 函子,咱们最终获得的都是只有一层的函子。

console.log(Monad.of('ttsy').flatMap(Monad.of).flatMap(Monad.of));  // Monad { val: 'TTSY' }
复制代码

在咱们平时的开发过程当中,要根据不一样的场景去实现不一样功能的函数,而函数式编程则让咱们从不一样的角度去让咱们可以以最佳的方式去实现函数功能,但函数式编程不是非此即彼的,而是要根据不一样的应用场景去选择不一样的实现方式。

以为还不错的小伙伴,能够关注一波公众号哦。

相关文章
相关标签/搜索