JavaScript 函数式编程入门(2)--运用

967 阅读6分钟

转载请保留这部分内容,注明出处。
另外,头条号前端团队非常 期待你的加入

之前介绍了函数式里面引用透明, 不可变,惰性求值,闭包,高阶函数等特性 ,下面我们就看看在 js 中的一些运用:

1. 在 js 中运用

- 柯里化(curry)

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数: 一个简单的加法实现:

const add = function(x) {
  return function(y) {
    return x + y;
  };
};

const increment = add(1);
const addTen = add(10);
increment(2);

// 3
addTen(2);
// 12

curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。 比如我们有这样一段数据:

const person = [{name: 'kevin'}, {name: 'daisy'}]

如果我们要获取所有的 name 值,我们可以这样做:

const name = person.map(function (item) {
    return item.name;
})

不过如果我们有 curry 函数:

const prop = curry(function (key, obj) {
    return obj[key]
});

const name = person.map(prop('name'))

我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些? 但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了? person.map(prop('name')) 就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。

一版简单curry 的实现:

function curry(fn, args) {
    const length = fn.length;
    args = args || [];
    return function() {
        let _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

- 代码组合(compose)

从一个简单例子开始:

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

var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = function(x){
    return hello(toUpperCase(x));
};

greet('kevin');

把对参数 x 的每一步操作都区分开,然后进行嵌套调用,这样子的场景还是比较过的,所以需要对这个过程进行提取,写一个 compose 函数,如下:

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

上面的 greet 函数就可以优化成更简洁易懂的形式:

var greet = compose(hello, toUpperCase);
greet('kevin');

利用 compose 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。在链式执行的代码代码里面,compose 也是比较常用的优化技巧,解决了嵌套过深的问题,同时,使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。

能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。

一个更加具体的例子: 假设我们从服务器获取这样的数据:

var data = {
    result: "SUCCESS",
    tasks: [
        {id: 104, complete: false,            priority: "high",
                  dueDate: "2013-11-29",      username: "Scott",
                  title: "Do something",      created: "9/22/2013"},
        {id: 105, complete: false,            priority: "medium",
                  dueDate: "2013-11-22",      username: "Lena",
                  title: "Do something else", created: "9/22/2013"},
        {id: 107, complete: true,             priority: "high",
                  dueDate: "2013-11-22",      username: "Mike",
                  title: "Fix the foo",       created: "9/22/2013"},
        {id: 108, complete: false,            priority: "low",
                  dueDate: "2013-11-15",      username: "Punam",
                  title: "Adjust the bar",    created: "9/25/2013"},
        {id: 110, complete: false,            priority: "medium",
                  dueDate: "2013-11-15",      username: "Scott",
                  title: "Rename everything", created: "10/2/2013"},
        {id: 112, complete: true,             priority: "high",
                  dueDate: "2013-11-27",      username: "Lena",
                  title: "Alter all quuxes",  created: "10/5/2013"}
    ]
};

任务:需要写一个名为 getIncompleteTaskSummaries 的函数,接收一个 username 作为参数,从服务器获取数据,然后筛选出这个用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并且按照日期升序排序。 以 Scott 为例,最终筛选出的数据为:

[
    {id: 110, title: "Rename everything", 
        dueDate: "2013-11-15", priority: "medium"},
    {id: 104, title: "Do something", 
        dueDate: "2013-11-29", priority: "high"}
]

一般都会这样子的实现:

// 第一版 过程式编程
var fetchData = function() {
    // 模拟
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
     return fetchData()
         .then(function(data) {
             return data.tasks;
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return task.username == membername
             })
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return !task.complete
             })
         })
         .then(function(tasks) {
             return tasks.map(function(task) {
                 return {
                     id: task.id,
                     dueDate: task.dueDate,
                     title: task.title,
                     priority: task.priority
                 }
             })
         })
         .then(function(tasks) {
             return tasks.sort(function(first, second) {
                 var a = first.dueDate,
                     b = second.dueDate;
                 return a < b ? -1 : a > b ? 1 : 0;
             });
         })
         .then(function(task) {
             console.log(task)
         })
};

getIncompleteTaskSummaries('Scott')

这样子,在别人理解代码时,其实是不方便的,要保证主流程的简洁直观,可以进行下面的优化:

var fetchData = function() {
    return Promise.resolve(data)
};

// 编写基本函数
var prop = curry(function(name, obj) {
    return obj[name];
});

var propEq = curry(function(name, val, obj) {
    return obj[name] === val;
});

var filter = curry(function(fn, arr) {
    return arr.filter(fn)
});

var map = curry(function(fn, arr) {
    return arr.map(fn)
});

var pick = curry(function(args, obj){
    var result = {};
    for (var i = 0; i < args.length; i++) {
        result[args[i]] = obj[args[i]]
    }
    return result;
});

var sortBy = curry(function(fn, arr) {
    return arr.sort(function(a, b){
        var a = fn(a),
            b = fn(b);
        return a < b ? -1 : a > b ? 1 : 0;
    })
});

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(prop('tasks'))
        .then(filter(propEq('username', membername)))
        .then(filter(propEq('complete', false)))
        .then(map(pick(['id', 'dueDate', 'title', 'priority'])))
        .then(sortBy(prop('dueDate')))
        .then(console.log)
};

getIncompleteTaskSummaries('Scott')

然后可以,利用上 compose:

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(compose(
            console.log,
            sortBy(prop('dueDate')),
            map(pick(['id', 'dueDate', 'title', 'priority'])
            ),
            filter(propEq('complete', false)),
            filter(propEq('username', membername)),
            prop('tasks'),
        ))
};

getIncompleteTaskSummaries('Scott')

可以看出流程是非常清晰的,而且把简单的函数封装起来提高了复用,避免每次相同的对数据的处理,还能够降低整体的代码量,当然,上面函数的执行顺序可能不太符合直观上的理解,可以用 ramda.js 中提供的 R.pipe 函数,对于参数能从左到右进行执行:

// 第五版 使用 R.pipe
var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.pipe(
            R.prop('tasks'),
            R.filter(R.propEq('username', membername)),
            R.filter(R.propEq('complete', false)),
            R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
            R.sortBy(R.prop('dueDate')),
            console.log,
        ))
};

有可能你会想到,如果都是封装好的,出了问题如果方便定位呢? 如果在 debug 组合的时候遇到了困难,可以使用下面不纯的 trace 函数来追踪代码的执行情况。

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});

var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined
这里报错了,来 trace 下:
var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]

toLower 的参数是一个数组,所以需要先用 map 调用一下它。

var dasherize = compose(join('-'), map(toLower), split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');

// 'the-world-is-a-vampire'

trace 函数允许我们在某个特定的点观察数据以便 debug。像 haskell 和 purescript 之类的语言出于开发的方便,也都提供了类似的函数。

一个简单的 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;
    };
};

一个函数式的 flickr

例子中主要体现了对主流程的优化,极大的提高的可维护性

2. 在 react 中运用

A JavaScript library for building user interfaces.

在 react 的设计上是亲近函数式的,像在 redux 中使用--数据不变性

  • 在React中,组件的render函数应该是一个纯函数,只有这样,组件渲染的结果才只和state/props有关系,遵循 UI= f(state) 这个公式。

  • 在React中,强调一个组件不能去修改传入的prop值,也是遵循Immutable的原则。

  • 在Redux中,更是强调Immutable的作用,每个reducer不能够修改state,只能返回一个新的state。

Immutable是个好原则,可以让代码更容易维护,当你看到一个变量的时候,可以放心假设这个变量代表的值不会被篡改,否则,你就会很操心。

提起 redux ,一定都知道 Container Component (容器组件)和 Presentational Container (展示组件), 其中展示组件就应该是一个纯函数,所有的副作用应该在容器组件中完成。 比如一个表单渲染的例子(并非 redux 实践):

import React, { Component } from 'react';
import { fetchPosts } from 'path/to/api';

export default class PostListContainer extends Component {
    constructor() {
        this.state = {
            posts: [],
        };
    }
    componentDidMount() {
        fetchPosts().then(posts => {
            this.setState({
                posts,
            });
        });
    }
    render() {
        return (
            ****** other code ******
            <PostList posts={this.state.posts} toggleActive={this.toggleActive}/>
            ****** other code ******
        );
    }
    toggleActive() {
        //
    }
}

export const PostList = (props) => {
    return (<ul>{ props.posts.map(post => <li key={post.id} onClick={props.toggleActive}>{ post.title }</li>) }</ul>);
}

上面代码,其实就是把数据和UI展示分离成两个职责明确的组件,更容易的复用组件,而且也容易定位问题的所在。

总结: JavaScript不是一个严格意义上的函数式编程语言,但是 如果我们不把“函数式编程”当做一个语言特性,只是当做一种“风格”,那么JavaScript就非常适合使用这种风格 ,现实中存在的程序(比如React和Redux)已经证明了这一点。

参考:

  1. github.com/mqyqingfeng…

  2. juejin.cn/post/684490…

  3. github.com/stoeffel/aw…

  4. zhuanlan.zhihu.com/p/26174525

  5. llh911001.gitbooks.io/mostly-adeq…