《JavaScript函数式编程》读书笔记

斑点喵 2019-06-20

JavaScript是一门很神奇的语言,作为一门现代化的语言,他有很多很有特色的东西,这些东西,让我们看到了一个十分自由化的未来,你永远都不知道,自己是不是掌握了这门奇葩的要命的语言。本文,可能没有那么多高深的编程技巧,有的更多的是,对编程本身的理解。因为,不知道自己有多白痴,所以,要记录下来,等到自己不白痴的时候,就能缅怀当年那个白痴的自己了。

什么是函数式编程

所谓函数式编程,是把函数本身上升到一等公民的地位,进行编程构建。在书中,作者用了这么一句话来形容函数式编程:

函数式编程通过使用函数来将值转换成抽象单元,接着用于构建软件系统。

额,那么好,我们先回忆一下什么叫函数。

函数

一般的,在一个变化过程中,有两个变量x、y,如果给定一个x值,相应的就确定唯一的一个y,那么就称y是x的函数,其中x是自变量,y是因变量,x的取值范围叫做这个函数的定义域,相应y的取值范围叫做函数的值域。

这是数学中的定义,简单的说,函数就是从A到B的关系映射。在计算机中,我们将多条语句组成的程序段(程序块)叫做函数,一个函数本身应该有一定的意义。和数学定义相当的是,变量的生命周期所在的函数空间,为变量的定义域。

面向函数的编程

所谓函数式编程,我们又可以叫做是面向函数的编程。所谓面向函数就是使用函数来作为我们分析和抽象程序的主要工具。

嗯,首先,我们继续来复习一下

  • 什么叫做面向过程。

“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。“面向过程”也可称之为“面向记录”编程思想,他们不支持丰富的“面向对象”特性(比如继承、多态),并且它们不允许混合持久化状态和域逻辑。

其实,说白了,就是想到什么写什么。

  • 什么叫做面向对象

按人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的办法。通过面向对象的理念使计算机软件系统能与现实世界中的系统一一对应。

在面向对象中,我们都知道对象是两个很重要的概念。

我们知道,所谓的类,其实就是:

具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象,类实际上就是一种数据类型。

而我们所说的对象,其实就是:

对象是人们要进行研究的任何事物,从最简单的整数到复杂的飞机等均可看作对象,它不仅能表示具体的事物,还能表示抽象的规则、计划或事件。

我们不难发现,类和对象,其实都是从数据角度出发来思考和解决问题,以数据本身为运算核心来抽象我们的计算行为。但是,很多时候,我们会发现,其实我们的运算行为远远比数据本身要复杂,而且,我们很多时候,其实并不能很好的去抽象一个对象。

我的数据老师曾经这样教导我们:

所谓程序,就是数据结构加算法。

如果说,面向对象是从数据结构的角度出发的话,面向函数的编程,就是从算法角度出发,也就是从行为的角度出发。

为什么要用函数式编程

数据和行为的关系

在计算机中,数据多数指的是存储结构。行为指的多数是计算操作。比如说这段代码:

function say(){
        let a = 1 + 1; 
        console.log(a)
    }

这段代码里,作为变量存在的asay,是我们所熟知的数据,而function say()则是包含了整个说的行为。
在面向对象的编程中,我们习惯把对象作为行为的核心,也就是说,先有人,然后,人来执行一个动作。而,对象,其实就是某一种变量,亦或是某一种数据类型。而函数式编程中,则认为数据只是行为加工的产品。将行为和数据分离。我们来看一段代码。

// man.php
    class Man {
        function __constructor($sexy){
            $this->sexy = $sexy;
        }
        public function sayHello($string){
            echo "I'm a ".$this->sexy.",I want to say:".$string;
        }
    }
    // male.php
    require_once 'man.php'
    $male = new Man("man");
    $male->sayHello("my name is homker");
    // I'm a man, I want to say: my name is homker

tips:
因为javascript本身是没有类的概念的,为了更好的说明问题,这里使用了php来作为范例语言,当然,你也可以使用javascript面向对象的方式来重新实现这段代码。就像这样。

function Man(sexy){
       var self = this;
       self._sexy = sexy;
       self.sayHello = function(string){
           console.log("I'm a "+self._sexy+",I want to say:"+string);
       }
   }

   var male = new Man("man");
   male.sayHello('my name is homker');

这是一段很简单的面向对象的代码,我们看看同样的功能在函数式中要怎么做。

function Man(sexy){
        return function(string){
            console.log("I'm a "+sexy+",I want to say:"+string);
        }
    }
    
    var sayHello = Man('man');
    sayHello('my name is homker');

我们会发现,在函数式编程中,我们去除掉了主语。你不知道这个动作是由谁发出的。相比于在面向对象编程中,数据是对象的属性,在函数式编程中,我们并不在乎这个数据的内容是什么,而更在乎其变化。

额,当然,严格意义上来说,其实,这个sayHello的原型是Object,在浏览器端,追溯他的原型链,它是挂在window对象下面的。

专注于过程本身

在实际的开发过程中,我们有的时候很难抽象出一个对象来描述我们到底要做什么,或者说,我们其实并不在乎这堆数据里面的内容是什么,我们要关心的,只是把数据经过加工,得出结果,仅此而已。至于这个数据,到底是用来干什么的,我们其实可以并不用关心。

如何使用函数式编程

上面说的都是一些思维上的东西,可能很稚嫩,希望各位大大们能指出其中的错误,切不可吝啬言语。下面就来说说函数式编程的一些具体的东西。

一等公民

所谓一等公民,说的是函数本身可以成为代码构成中的任意部分。具体的来说,函数可以具有以下的特点:

  • 函数可以存储为变量

  • 函数可以成为数组的一个元素

  • 函数可以成为对象的成员变量

  • 函数可以在使用的时被直接创建

1 + (function(){ return 1 })(); //2
  • 函数可以被作为实参传递

  • 函数可以被另一个函数返回

  • 函数可以返回另一个函数

  • 函数可以作为形参

相信大家一看就懂了。

纯函数 (Pure Function)

在函数式编程中,有一个很重要的概念是纯函数。所谓纯函数就是

纯函数(Pure Function)与外界交换数据只有唯一渠道——参数和返回值。其在逻辑上没有副作用

可预见性

简单的说,就是你输入什么,就输出什么。输入和输出是可预见的。比如说像酱:

function add(x,y){
        x = _.isNumber(x)? x : 0;
        y = _.isNumber(y)? y : 0;
        return x+y;
    }
    
    add(1,2); // 3

这样的一个函数,你输入两个变量,你可以很确定的,你得到的一定是两者之和。与之相异的,在javascript编程中很容易出现的,比如说酱:

var x = 10;
    function add10(y){
        return y+x;
    }
    
    add10(1); // 11
    
    x = 11;
    
    add10(1); //12

对于这个函数而言,函数本身是不可控的,如果外部的x发生改变,函数的返回值也随之会发生改变。那么如果想避免,应该怎么写呢:

function add(x){
        return function(y){
            return x+y;
        }
    }
    
    var add10 = add(10);
    
    add10(1); //11

这个时候,将函数所需的变量闭包在函数体的内部,函数的运算是不依赖于外界的变量的,你输入什么,就一定会输出什么。

完整性

为了实现函数的可控性,要保证,函数本省是完整的。函数的完整表现在,函数的运行不依赖于外界的环境变量。同时,函数的逻辑是完整的。比如说,酱:

<!DOCTYPE html>
    <html>
        <head>
            <title>a demo</title>
            <link herf="path/to/style" rel="stylesheet" />
            <script src="path/to/jq"></script>
        </head>
        <body>
            <div class=“container”>
                <span id="display"></span>
                <button id="getJson">获取数据</button>
            </div>
            <script type="appliaction/javascript">
                ;(function(){
                    $('#getJson').addEventListener('click',function(){
                        $.get('path/to/json',function(json){
                            $('#display').text(json);
                        })
                    },false)
                })()
            </script>
        </body>
    </html>

上面是我们经常写的方式,当然啦,如果框架复杂一点,可能会多一点回调嵌套。但是,逻辑不出于此。但是呢,如果要函数完整,应该酱,额,我就写重要的部分啦:

var getJson = function(url,params){
        return $.getJson(url,params);
    }
    
    var display = function(text){
        $('#display').text(text)
    }
    
    var getJsonClickHandle = function(){
        getJson('url',{}).done(display)
    }
    
    var init = function(){
        $('#getJson').click(getJsonClickHandle);
    }
    
    
    init();

这时候,我们抽象了整个行为。

点击 -> 获取数据 -> 显示数据。

酱,我们把每个行为转换成了一个单独的函数行为。这样的,每一个函数都是单独的行为,可以很好的扩展和复制到其他地方。

同时,我们也引出了一个纯函数很重要的部分。

可测试

我们发现,当函数功能变的单一的时候,我们可以很清晰的知道函数输入什么,输出什么的时候,我们发现,这个代码的可测试性,得到了很大的提高。还是用上面的两段代码,前者,根本不知道怎么去写测试,或者说,就是错了,你也不知道哪里错的,因为,所有的逻辑被各种匿名函数包裹了,很难很快的定位到问题的所在,后者,则容易了很多。

可组合(compose)

当函数纯化之后,有一个很鲜明的特点是,这个函数变的可以组合了。我们可以像堆乐高积木一样,把各个我们要用的函数堆起来变成一个更大得函数体。比如说酱:

使用了underscore.js;

function checker(){
        var validators = _.toArray(arguments);
        return function(obj){
            return _.reducer(validators,function(err,check){
                if(check(obj)){
                    return errs;
                }else{
                    return _.chain(errs).push(check.message).value();
                }
            },[])
        }
    }
    
    function validator(message,fun){
        var f = function(){
            return fun.apply(fun,arguments);
        };
        f['message'] = message;
        return f;
    }
    
    function hasKeys(){
        var KEYS = _.toArray(arguments);
        
        var fun = function(obj){
            return _.every(KEYS,function(key){
                return _.has(obj,key);
            });
        };
        
        fun.message = KEYS.concat([",this key is valid"]).join(" ");
        return fun;
    }
    
    var checkCommand = checker(hasKeys('msg','type'));
    
    checkCommand({}); // msg type , this key is valid

checkCommand就是我们最后组合出来的可以进行校验的大城堡了,而且这个城堡可以定制化哦,甚至必要的时候,可以动态定制化。

高阶函数(high-order function)

高阶函数是函数式编程中,很重要的部分,我们先来看看它是怎么定义的。作为一个高阶函数,他要满足以下定义:

  • 高阶函数一定是一等公民

  • 以一个函数为参数

  • 同时返回一个函数作为函数的返回值

举一个简单的例子:

var aFunc = (function(callback){
        return function(){
            callback&&callback();
        }
    })(function(){ console.log("I am a high-order function") });
    
    aFunc();// I am a high-order function;

额,呵呵,这个例子比较无聊哈,我们看个更有意思的例子:

function calc(x){
        return function(y){
            return function(method){
                method&&method(x)(y);
            }
        }
    }
    function add(x){
        return function(y){
            console.log(x+y);
        }
    }
    calc(1)(2)(add);//3

当然,你再无聊点,非要写成这样,也不是不可以。

function calc(x){
        return function(method){
            return function(y){
                method&&method(x)(y);
            }
        }
    }
    calc(1)(add)(2);

其中的add方法是可自定义的。你可以把它换成任何一个你想要的函数。

柯理化函数(curry)

柯理化函数,是函数编程中很重要的一个方法。嗯,我们先来看看定义:

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

上文的add,calc都是柯理化的函数。
在平时的使用中,我们经常如此使用之:

  • 接收一个函数

  • 返回一个只接收一个参数的函数

柯理化函数的定义是函数式编程的基础,我们通过返回一个闭包的方式来使得函数参数变的可以捕捉,可以传递,可以保存。同时也使得,函数的行为变的可以分离,可以组合。

柯理方向

嗯,我们知道运算符是有方向的,函数组合的大函数也是一样的。比如说,下面两个函数就不一样:

var leftdiv(x){
        return function(y){
            return x/y;
        }
    }
    
    var rightdiv(x){
        return function(y){
            return y/x;
        }
    }

部分引用

我们说到柯理化的函数可以保存参数,或者说成是保留运算场景。比如说我们在上文举的add函数:

var add10 = add(10);
    add10(1);//11

其中的add10就是部分引用,add10这个函数保留了上一次函数调用时的运算场景,当下一个参数进来的时候,它能够继续运行,并给出结果。这样的好处是什么呢,我们可以实现核心运算的前置条件校验。

比如说酱:

var add = function(x){
        if(!isNumber(x)) throw Error(' x must be a num');
        return function(y){
            if(!isNumber(y)) throw Error(' y must be a num');
            return function(){
                return x+y;
            }
        }
    }

我们在每一次的调用的时候,我们顺便做了输入参数的校验,当最后函数执行的时候,我们可以确保,最后的函数执行是可靠的,也就是该函数是纯的。

组合

上文在说纯函数的时候,我们就已经说到了组合了,这里,我们再强调的地方是组合函数的管道特性。就是把上一个函数的值作为下一个函数的参数。
就像酱:

var compose = function(f,g){
        return function(x){
            return f(g(x));
        }
    }

基于流的编程

其实,对于函数式编程,我们总结其技巧的时候,发现,其功能是围绕于:

  • 用函数传递函数

  • 用函数构造函数

  • 用函数调用函数

而这三个综合在一起,使得函数式编程能够实现基于数据流或者控制流。

链式编程

这个我们都很熟悉啦,jquery就是这样干的,通过返回一个自身来实现链式调用。就像酱:

$.prototype.next(){
        //do something
        return this;
    }
    
    $('li').next().next();

promise

这个其实单独拿出来,写一本书。所以这里就不详细说了。例子的话,上文也有举getJson,这里就不举了。

链式编程和promise能更好的,让我们按照数据处理的阶段去处理函数,在开始的进行参数校验,在加工的时候,进行数据的加工,在最后的时候,进行函数的显示。

总结

其实,这本翻来覆去的看了好几遍,一直想做一个总结,但是并不能做的出来。因为,我们很容易发现,在实际的操作过程中,我们或多或少的都使用了函数式编程的一部分,我们或多或少的都在践行函数式编程的理论,但是,如果说,我们的代码就是使用函数式编程的时候,我们又会发现,我们的代码中,有很大一部分的逻辑,实在是没办法使用函数式编程进行处理。所以,后面有了响应式编程RXJs,通过订阅和发布模式来实现队列化的事件调度和资源分配,但是在实际使用过程中,要想很快的将代码转化成函数式编程,需要对行为逻辑有很深刻的理解和抽象,对步骤的分解,对函数的分解,对行为的分解,这个才是函数式编程中最难的部分,如何去思考你的数据发生了什么变化,你的状态发生了什么变化,去管理你的数据和你的状态。

相关推荐

86447318 / 0评论 2019-10-23