loading...

5

JavaScript 函数式编程

javascript读完大概需要23分钟

  • 发布时间:2018-07-26 10:50 星期四
  • 刘伟波
  • 262
  • 更新于2018-07-26 10:50 星期四

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

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

函数是第一等公民

第一等公民是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。

// 赋值
var a = function fn1() {  }
// 函数作为参数
function fn2(fn) {
    fn()
}   
// 函数作为返回值
function fn3() {
    return function() {}
}

函数是纯函数

纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。

从纯函数的概念我们可以知道纯函数具有两个特点:

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

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

// 是纯函数
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)
函数合成指的是将代表各个动作的多个函数合并成一个函数。
这里我直接给出通用 compose 函数的代码

function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        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)

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

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

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

优化后的 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

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

函子

在前面函数合成的例子中,执行了先「加上 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' }

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

摘自掘金:https://juejin.im/post/5b4ac0d0f265da0fa959a785

作者:刘伟波

链接:http://www.liuweibo.cn/p/207

来源:刘伟波博客

本文原创版权属于刘伟波 ,转载请注明出处,谢谢合作

你可能感兴趣的文章

    发表评论

    评论支持markdown,评论内容不能超过500字符,如果内容过多或者要及时回复,建议去 平台,一般一天之内就会回复。
    关于技术问题或者有啥不懂的都可以留言,我会定期回复答 疑,推荐最新仓库 前端知识体系, 感謝支持!