函数式编程在JavaScript下应用实践

在这里插入图片描述

点击此处阅读原文:函数式编程在JavaScript下应用实践

函数式编程在JavaScript下应用实践

前言

在日常前端编程当中,我们习惯使用“面向过程式”思维编写代码。在我们对代码规范建设过程中,这种思维方式往往对代码的可读性、可维护性、可测试性等带来很大的问题。在前端代码review过程中,我们也发现很多代码存在函数划分不合理、难以编写测试、实现过于复杂(可以用很简明代码替代方式实现)等问题。

在这个代码优化的探索过程中,我们发现:函数式编程的一些概念和知识,能够切实地规范我们编程的思维,从设计和理念上切实解决一系列问题。且函数式编程与JavaScript有天然的融合性,比如他们都把“函数”视为“一等公民”。

本文将讲述一些关于函数式编程,在我们实际项目下的应用经验。我们会以一个例子来讲解我们是如何对他进行“重构”的。

本文可能更倾向于“利用”或者说“借用”函数式编程的方法应用于实践中。很多实践其实仍然不符合“纯函数式编程”要求。函数式编程是一套完整的编程范式,如果大家想更深入了解其他函数式编程的知识,可以参考本文后面的其他链接。

从一个实际需求说起

某一天,我们开发同学小叶接到如下需求:

  • 从接口 http://xxx.163.com/api/v1/users 读取一个用户列表
  • 筛选这个列表中,在职的且身份为SRE的同学
  • 按照以下格式输出同学的信息
    • 首先输出所有在职SRE
    • 输出用户的名字、邮箱、电话关键信息
    • 不同项目组可能有不同SRE,要输出用户所在的用户组列表
目前值班SRE: 小王,小刘,小张

小王(xiaowang@163.com) 18812341234: Alpha小组(A)
小刘(xiaoliu@163.com) 18812351236: Beta小组(B)
小张(xiaozhang@163.com) 18812351235: Alpha小组(A)

小叶同学说,没问题,然后调了一下接口,接口返回:

[
    {
        "name": "xiaowang",
        "ch_name": "小王",
        "phone": "18812341234",
        "email": "xiaowang@163.com",
        "status": "working",
        "groups": [
            {
                "code": "A",
                "ch_name": "Alpha小组",
                "role": "sre"
            }
        ]
    },
    {
        "name": "xiaoliu",
        "ch_name": "小刘",
        "phone": "18812351236",
        "email": "xiaoliu@163.com",
        "status": "working",
        "groups": [
            {
                "code": "A",
                "ch_name": "Alpa小組",
                "role": "dev"
            },
            {
                "code": "B",
                "ch_name": "Beta小组",
                "role": "sre"
            }
        ]
    },
    {
        "name": "xiaoming",
        "ch_name": "小明",
        "phone": "18812351237",
        "email": "xiaoming@163.com",
        "status": "vacation",
        "groups": [
            {
                "code": "B",
                "ch_name": "Beta小组",
                "role": "sre"
            }
        ]
    },
    {
        "name": "xiaozhang",
        "ch_name": "小张",
        "phone": "18812351235",
        "email": "xiaozhang@163.com",
        "status": "working",
        "groups": [
            {
                "code": "A",
                "ch_name": "Alpha小组",
                "role": "sre"
            },
                        {
                "code": "B",
                "ch_name": "Beta小组",
                "role": "dev"
            }
        ]
    }
]

小叶同学很认真地阅读了一下后端接口文档:

  • 每个用户有name, ch_name, phone, email 等几个字段,一眼看出啥意思没问题
  • status 字段表示在职状态,working就是在职,vacation等就是休假或者其他不在职状态
  • groups 字段是一个数组,代表这个用户所在的“项目组”。用户可能同时在不同项目组下担任不同角色工作

小叶同学想了想:“no problem!这个需求很简单,1个小时后上线”

一把梭实现

小叶同学拿到需求,巴拉巴拉写了一个小时,终于写完了:

Axios.get('http://xxx.163.com/api/v1/users').then(function (res) {
    // 所有用户
    var users = res.data;
    // 头部提示语
    var headerInfoStr = "目前值班SRE: ";
    // 所有值班SRE的中文名
    var sreUserChNames = "";
    // 所有SRE的详情列表
    var sreDetailStr = "";
    for (var i = 0; i < users.length; i++) {
        var user = users[i];
        // 如果用户不是在 working 则跳过不处理
        if (user['status'] !== 'working') {
            continue;
        }
        var groups = user.groups;
        // 用户是否为SRE,如果有任意一个组的角色是SRE则是SRE
        var isSre = false;
        for (var j = 0; j < groups.length; j++) {
            var group = groups[j]
            // 有一个职位为SRE则他是SRE
            if (group['role'] === 'sre') {
                isSre = true;
            }
        }
        // 如果不是SRE,则跳过该用户
        if (!isSre) {
            continue;
        }
        // 保存SRE的名字
        sreUserChNames += user.ch_name;
        // 处理字符串连接
        if (i < users.length - 1) {
            sreUserChNames += ",";
        }
        // 生成SRE的信息字符串
        var infoStr = user.ch_name + '(' + user.email + ') ' + user.phone + ': ';
        // sre细节信息加上infoStr
        sreDetailStr += infoStr;
        // 处理用户的分组列表
        for (var j = 0; j < groups.length; j++) {
            var group = groups[j];
            // 如果这个分组不是SRE则跳过
            if (group.role !== 'sre') {
                continue;
            }
            // 分隔符,如果用户有多个分组则用逗号分开
            if (j > 0) {
                infoStr += ',';
            }
            // 将这个分组的的字符串加到结果中
            sreDetailStr += group.ch_name + "(" + group.code + ")";
        }
        // 每个SRE详情后面添加一个换行符
        sreDetailStr += "\n";
    }
    console.log(headerInfoStr + sreUserChNames + '\n\n' + sreDetailStr);
}, function (err) {
    console
    // 所有SRE的详情列表.error(err);
    // TODO: 这里可能需要更详细的错误处理
});

测了一下,好像输出是正确的,小叶同学于是提交了代码到 code review…

Code Review

我们可以很直观、很明显感觉到,这段代码很有不少问题。

我们可以列举这段代码的一些问题:

  1. 可读性:代码篇幅很长,将一大堆逻辑堆在了Promise的回调函数中,要理解代码很困难
  2. 可复用性:没有任何的可复用性,需求可能稍微改动,这个代码就得相应进行很多修改
  3. 可测试性:基本上很难去编写任何测试
  4. 可维护性:如果出现bug,很难找原因

是否能进行优化?

答案是肯定的。问题是我们采用什么方法去对其进行优化。我们一般思路会将其抽成几个函数,那应该怎么抽比较好呢?

接下来,将向大家介绍如何使用一些“函数式编程”的方法,对这段代码进行一个好的优化。

函数式编程

我们首先看(或复习一下)一下函数式编程概念:

  • 在计算机科学中,函数式编程是一种编程“范式”。和“面向过程”、“面向对象”一样
  • 函数式编程将计算机运算视为函数运算
  • 函数式编程会避免使用程序状态和易变对象(Matuable Data)
  • 函数式编程起源于数学中的“范畴论”,这种理论恰巧能够应用于计算机编程

来源: 维基百科

以前很多同学对函数式编程有不少误解,会认为函数式编程是“把一个大的程序拆分成一些比较小粒度的函数”。显然这个概念是有很大的偏差的,这样的做法得到的程序,很多时候,只能算作是“面向过程式”程序,这种理解下的函数是一种过程调用,他不带有一些限制或约束。

而函数式编程,更关注的是类似数学函数的性质,比如函数是否是“纯”的、数据是否是可变的等。

复习一下我们在高中时候学过的函数概念,比如二次函数:

f ( x ) = x 2 + 1 f(x) = x^2 + 1 f(x)=x2+1

我们根据函数的定义会发现,每一个输入的x都会有一个确定f(x)与之对应:

  • f ( 1 ) = 1 2 + 1 = 2 f(1) = 1^2 + 1 = 2 f(1)=12+1=2
  • f ( 2 ) = 2 2 + 1 = 5 f(2) = 2^2 + 1 = 5 f(2)=22+1=5
  • f ( 3 ) = 3 2 + 1 = 10 f(3) = 3^2 + 1 = 10 f(3)=32+1=10
  • f ( 4 ) = 4 2 + 1 = 17 f(4) = 4^2 + 1 = 17 f(4)=42+1=17

所谓函数式编程之“函数式”,指的就是更贴近“数学函数”的性质。“函数”就是应该用来计算,而不是用于状态修改。

相反的,在一些项目的JavaScript代码中,我们发现有一些类似这样的代码:

var state = 1;

function f1 () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            state = state * 2
            resolve(state)
        }, 1100)
    })
}

function f2 () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            state = state * 2
            resolve(state)
        }, 1000)
    })
}

f1().then(res => console.log(res));
f2().then(res => console.log(res));

上述代码被称为反范式设计,在这个极端例子中:

  • 很明显,f1f2两个函数都是非纯的,他们依赖于一个全局变量Steate
  • 我们很难直观看出这两个函数的输出是什么,2和4,还是4和4?让人非常迷惑
  • 将setTimeout替换为异步请求我们不难发现,一些项目代码中存在类似函数的蛛丝马迹
  • 显然,数学函数是不会做这种事情的。我们没有见数学函数会去修改全局变量的

相反,我们可以了解到,函数式编程中拥有一些很好的特点:

  • 函数式编程中的函数的输入和输出是明确的
  • 降低了编程的复杂度
  • 测试将变得简单许多
  • 代码会更可读,更好理解

接下来我们讲解一些实际应用

Pure Function (纯函数)

纯函数是函数式编程中一个非常重要的概念。

纯函数指的是满足以下条件的函数:

  1. 无论执行多少次,相同的输入都会产生相同的输出
    • 也就是说,函数的输入决定函数的输出
    • 函数的输出只依赖于函数的输入
    • 如数学函数 f ( x ) = x 2 + 1 f(x) = x^2 + 1 f(x)=x2+1
  2. 不会产生副作用 (Side Effect)
    • 函数不会去修改函数外的变量
    • 函数不会进行IO操作
    • 在JavaScript中,函数不会修改传入的参数

比如:

// pure function
const fx = (x) => x * x + 1;
// pure function
const add = (x, y) => x + y;

// not pure,返回值不是由参数决定的
const rand = () => Math.random();
// not pure,返回值不是由参数决定的
const time = () => new Date();


let a = 1;
// not pure,返回值不是有参数决定,而是由闭包变量(全局变量)a决定的
const getA = () => a;
// not pure,返回值不是有参数决定,而是由闭包变量(全局变量)a决定的,且还修改了a的值
const getA2 = () => a++;

// 产生IO,这是一个非纯函数
const output = () => console.log('xxx');

引理:如果使用一个函数时候,不使用他的返回值但有意义的话,说明这个函数是非纯函数

比如我们很多“00年代风”JavaScript代码喜欢修改函数传入的参数,以达到“分离函数”目的。

 function isSre(user) {
    for (var i = 0; i < user.groups.length; i++) {
        if (user.groups[i].role === 'sre') {
            user.isSre = true;  // 注意这里
            return;
        }
    }
    user.isSre = false;
    return;
}

这种写法:

  • 首先isSre这个函数是不明确的,他实质上对user对象产生了副作用
  • 暗中修改了传入的user对象的数据,我们很可能不知道他做了这样的修改
  • 如果项目中这样的函数多起来,然后通过复合等操作之后,项目的复杂度会变得非常高,导致难以维护,比如 isSre(user); isDev(user); someMethod(user); 一系列骚操作之后,这个user将面目全非

而如果是:

function isSre(user) {
    for (var i = 0; i < user.groups.length; i++) {
        if (user.groups[i].role === 'sre') {
			return true;
        }
    }
    return false;
}

这样则会好很多。我们可以看到我们后面实现的这个函数是“纯的”

  • 输入输出非常明确
  • 不会修改传入的参数

接下来我们利用纯函数对我们的程序进行改写。

分离非纯IO操作和纯函数

从上述定义我们知道,IO操作的函数是非纯函数的。但是在一个应用中必定会有IO操作,如果一个应用全是纯函数,那么这个应用很可能没有意义。IO操作是不可避免的。

我们常说的IO操作有这些:

  • Http 请求
  • Dom 操作
  • 获取用户输入
  • 查询数据库
  • 读写文件
  • 等等

你可能会疑惑,完全使用纯函数编程我们真的可行吗?真的有想象中那么美好吗?

其实不然。函数式编程并没有禁止我们的程序有副作用函数,而是说这些副作用,应该能够被我们合理地控制。在纯函数式编程中,可能会使用类似monad等方式进行处理。

这里我们不去深究这部分,我们可以暂时先把IO这类“恶魔操作”抽离出来,比如:

import Axios from 'axios';

async function fetchUsers() {
    let result = [];
    const url = 'http://xxx.163.com/api/v1/users';
    try {
        result = await Axios.get(url);
    } catch (err) {
        console.error(error);
        // TODO: TODO是程序员最大的谎言
    }
    return result;
}

async function printResult(content) {
    console.log(content);
}

function processUsers(users) {
    // ...
}

async function main() {
    const users = fetchUsers();
    const result = processUsers(users);
    printResult(result);
}

main();

这样我们将程序分成4个函数:

  • 请求接口获取数据 —— 非纯
  • 输出函数 printResult - 非纯
  • 数据处理逻辑 - 纯函数
  • 主函数 - 非纯函数

我们终于得到一个处理数据处理逻辑的纯函数,他已经可以具备一些函数式编程中的特性。但是还是远远不够。

我们发现最复杂的逻辑就是在数据处理逻辑上。我们应该怎么做合理拆分比较好?

首先一点是,这个纯函数无论怎么拆分,他拆出来的函数一定得是纯函数

接下来我们进行下一步优化操作。

map、filter、reduce等应用

我们经常听说这些操作,即使你以前没有接触过函数式编程。其实这些操作是来源于函数式编程的。

刺眼的循环

一些专业的JavaScript同学可能会有一只这样的直觉。他们很不喜欢看到这样的代码:

for (let i = 0; i < user.length; i++) {
    // var user = users[i];
    let user = users[i];
}

这是一个很标准的JavaScript循环。我们一开始学习编程的时候,比如c语言时候,也会大量使用这样的循环结构,但是我们会发现一些问题:

这里的循环的i,i=0,length,i++,这些表达式,其实在我们这个过程中,处理取对应下标的user对象,其实是没有其他意义的。

也就是说这样的迭代不够抽象,我们更多需要列表迭代而不是循环。

有同学可能会说,我们可以使用类似

for (let user of users) {
    // do something with user
}

是一种比较体面的方法。但是我们很多时候进行一些操作,比如,获取所有用户的ch_name

let userChNames = [];
for (let user of users) {
    userChNames.push(user.ch_name);
}

我们发现仍然不够抽象,我们仍然要去理解这段循环。因为这里做的更多是“变形”操作。这种场景之下,我们看到for循环更多的是在进行“命令的执行”,我们称之为命令形编程。而在函数式编程之下,更多的我们可能会使用声明式的方式编写代码。

声明式编程(Declarative Programming)是一种更高层次的编程。我们可以这样理解:

声明式编程:

蔬菜.做成菜(蔬菜沙拉)

而命令式编程则更像是

洗干净(蔬菜)
混合(蔬菜,沙拉)
放入盘中(混合物)
  • 命令式编程更专注“我们应该怎么做?”
  • 声明式编程则更专注“我们要得到什么?”

函数式编程整体风格更倾向于声明式编程。接下来我们介绍的mapfilterreduce也更接近声明式的编程。

map

map方法会对列表的每个元素运行一个传入的函数,然后得到一个新的列表。

完整语法:

let newArr = arr.map(function callback(currentValue[, index[, array]]) {
    // Return element for new_array
}[, thisArg])

来源: MDN

我们尝试使用map来简化:

const userChNames = users.map(u => u.ch_name);

可以发现,我们大量简化了循环的逻辑,而且代码非常可读。

filter

filter 方法用于过滤容器中的元素。他通过传入一个谓词(Predicate),通俗地说就是一个接收容器元素并返回true和false的函数。

比如需求,我们需要过滤出所有的SRE

let sreUsers = [];
for (let user of users) {
	// 假设我们有函数isSre判断是否是SRE
    if (isSre(user)) {
        sreUsers.push(user);
    }
}

等价的,如果我们使用filter

const sreUsers = users.filter(u => isSre(u));

同样发现我们可以大幅度减少代码,并且增加可读性

reduce

reduce 抽象来说是一个类型转换的方法。他可以把一种类型转换成另外一种类型,最经典的例子就是做统计,比如我们需要统计有多少个SRE:

完整语法:

arr.reduce(callback[, initialValue])
let sreNum = 0;
for (let user of users) {
	// 假设我们有函数isSre判断是否是SRE
    if (isSre(user)) {
        sreNum++;
    }
}

等价的,我们使用reduce

const sreNum = users.reduce((acc,x) => isSre(user) ? acc + 1: acc, 0);

同样发现我们可以大幅度减少代码,并且增加可读性。

这里要注意,reduce的处理函数也应该是纯函数的!每次迭代过程中acc + x 总返回一个新的对象,我们不应该直接去修改acc!否则会在一定程度上违反函数式编程原则。

some 和 every

除了map, filter, reduce 之外,我们很常用一些判断方法。

比如判断是否全员是sre

let isAnySre = false;
for (let user of users) {
    if(isSre(user)) {
        isAnySre = true;
        break;
    }
}

我们可以写成

const isAnySre = user.some(u => isSre(u));

类似的,如果需要判断全员都是SRE,则我们可以把代码

let isAllSre = true;
for (let user of users) {
    if (!isSre(user)) {
        isAllSre = false;
        break;
    }
}

可以改写为:

const isAllSre = users.every(u => isSre(u));

关于 for 和 map、reduce、filter 性能问题

有同学可能会担心 map、reduce 和 filter 的性能,其实:

  • 性能 for > for each > map
  • 如果作为库的开发者,你可能需要关心这部分内容,并且了解v8在这方面的优化
  • 如果作为一个普通开发者,则更应该关心代码可读性、可维护性等。特别是在多人合作项目中,一定不能陷入“过渡强调优化”的陷阱当中。我们更需要关注“技术债务”(Technical Debt)
  • 前端列表的数据往往数据量不大。更大更影响体验的性能问题,往往发生在IO(http请求)或者是现代前端框架的change detection

组合拳

回忆最开始小叶同学实现的那个代码,结合我们现在得到的函数式编程知识,我们可以轻松将代码进行一些抽象。

利用这个方法我们可以将原来的代码业务改写为:

import Axios from 'axios';

async function fetchUsers() {
    let result = [];
    const url = 'http://xxx.163.com/api/v1/users';
    try {
        result = await Axios.get(url);
        result = result.data;
    } catch (err) {
        console.error(error);
        // TODO: TODO是程序员最大的谎言
    }
    return result;
}

async function printResult(content) {
    console.log(content);
}

/**
 * 获取在职中用户
 * @param {list of users} users
 * @return {list of users}
 */
function getWorkingUsers(users) {
    return users.filter(u => u.status === 'working');
}

/**
 * 获取SRE用户
 * @param {list of users} users
 * @return {list of users}
 */
function getSreUsers(users) {
    return users.filter(u => u.groups.some(g => g.role === 'sre'));
}

/**
 * 获取用户中文名
 * @param {list of user} users
 * @return {list of strings}
 */
function getUsersChName(users) {
    return users.map(u => u.ch_name);
}

/**
 * 获取某个用户的字符串信息数据
 * @param {user} user
 * @return {string}
 */
function getUserInfoStr(user) {
    const userGroupsInfoStr = user.groups
        .filter(g => g.role === 'sre')
        .map(g => `${g.ch_name}(${g.code})`).join(',')
    ;
    return `${user.ch_name} (${user.email}) ${user.phone}: ${userGroupsInfoStr}`;
}

/**
 * 获取所有SRE的全名
 * @param {list of users} user
 * @return {string}
 */
function getSreUsersInfoStr(users) {
    return `${getUsersChName(users).join('、')}`;
}

/**
 * 获取用户列表的字符串信息数据
 * @param {list of users} users
 * @return {string}
 */
function getUsersInfoStr(users) {
    return users.map(u => getUserInfoStr(u)).join('\n');
}

/**
 * 处理列表需求,返回数据
 * @param {list of users} users
 * @return {string}
 */
function processUsers(users) {
    // 获取在值班的用户
    const workingUsers = getWorkingUsers(users);
    // 获取在值班的SRE
    const workingSreUsers = getSreUsers(workingUsers);
    // 获取要输出的第一行SRE全部ch_names
    const sreUsersInfo = getSreUsersInfoStr(workingSreUsers);
    // 获取要输出的后面的SRE详细信息
    const sreUsersDetailInfo = getUsersInfoStr(workingSreUsers);
    // 返回信息
    return `目前值班SRE: ${sreUsersInfo}\n\n${sreUsersDetailInfo}`
}

async function main() {
    const users = await fetchUsers();
    const result = processUsers(users);
    printResult(result);
}

main();

上述我们的除了 mainfetchUsersprintResult 每一个函数都是纯函数。

我们对原来的代码进行修改,在很大程度上提高了:

  1. 可读性:入口、每个函数的职责非常明确,比原来循环的可读性要好不少
  2. 可复用性:一些抽离的函数可以应付后续的一些需求
  3. 可测试性:除了 mainfetchUsersprintResult 每一个函数都是纯函数,我们只需要做一些输入、期望输出的测试用例,即可测试函数的正确性。
  4. 可维护性:可维护性更好,出bug的概率减少,即使出bug,也能比较好的排查是哪部分代码有问题

这一套组合拳,大大优化了我们原先编写的代码。

高阶函数

我们可以发现在我们这一版代码上面拆分的函数仍然不够“抽象”。比如:

function getSreUsers(users) {
    return users.filter(u => u.groups.some(g => g.role === 'sre'));
}

function getUserInfoStr(user) {
    const userGroupsInfoStr = user.groups
        .filter(g => g.role === 'sre')
        .map(g => `${g.ch_name}(${g.code})`).join(',')
    ;
    return `${user.ch_name} (${user.email}) ${user.phone}: ${userGroupsInfoStr}`;
}

这里都用到了g => g.role === 'sre'这样一个函数。我们能否将这样的场景更加抽象地描述出来?答案是肯定的,考虑函数

function ifUserAtStatus(status) {
    return (user) => user.status == status;
}

我们这里使用了一个“高阶函数”(Higher Order Function)

高阶函数是一个返回函数的函数,抑或是把函数当做处理参数的函数。

在这里我们定义了一个ifUserAtStatus函数,假设我们调用这个函数

const ifUserAtStatusWorking = ifUserAtStatus('working');

此时我们得到一个predicate函数,我们将ifUserAtStatusWorking传入users列表,即可得到“在职状态的用户”列表,也就是

const workingUsers = users.filter(ifUserAtStatusWorking);

假设我们后面又有需求过滤其他类型用户,我们都能够应付过来

const ifUserAtStatusVacation = ifUserAtStatus('vacation');

我们对实现的代码再进行一下处理:

import Axios from 'axios';

async function fetchUsers() {
    let result = [];
    const url = 'http://xxx.163.com/api/v1/users';
    try {
        result = await Axios.get(url);
        result = result.data;
    } catch (err) {
        console.error(error);
        // TODO: TODO是程序员最大的谎言
    }
    return result;
}

async function printResult(content) {
    console.log(content);
}

function ifUserAtStatus(status) {
    return user => user.status == status;
}

function ifUserHasRole(role) {
    return user => user.groups.some(isAttributeEquals('role', role));
}

function isAttributeEquals(attr, value) {
    return obj => obj[attr] == value;
}

function getAttribute(attr) {
    return obj => obj[attr];
}

/**
 * 获取某个用户的字符串信息数据
 * @param {user} user
 * @return {string}
 */
function getUserInfoStr(user) {
    const GROUP_SEPARATOR = ',';
    const userGroupsInfoStr = user.groups
        .filter(isAttributeEquals('role', 'sre'))
        .map(g => `${g.ch_name}(${g.code})`).join(GROUP_SEPARATOR)
    ;
    return `${user.ch_name} (${user.email}) ${user.phone}: ${userGroupsInfoStr}`;
}

/**
 * 获取用户列表的字符串信息数据
 * @param {list of users} users
 * @return {string}
 */
function getUsersInfoStr(users) {
    return users.map(u => getUserInfoStr(u)).join('\n');
}

/**
 * 处理列表需求,返回数据
 * @param {list of users} users
 * @return {string}
 */
function processUsers(users) {
    // 获取在值班的用户
    const workingSreUsers = users.filter(ifUserAtStatus('working'))
                                .filter(ifUserHasRole('sre'));
    // 获取要输出的第一行SRE全部ch_names
    const sreUsersInfo = workingSreUsers.map(getAttribute('ch_name')).join('、');
    // 获取要输出的后面的SRE详细信息
    const sreUsersDetailInfo = getUsersInfoStr(workingSreUsers);
    // 返回信息
    return `目前值班SRE: ${sreUsersInfo}\n\n${sreUsersDetailInfo}`
}

async function main() {
    const users = await fetchUsers();
    const result = processUsers(users);
    printResult(result);
}

main();
  • 这样做我们可以得到更加抽象的代码,日后复用起来会更加的简单
  • 这种写法其实更接近于函数式编程的风格
  • 上述很多方法,其实可以通过库来实现,比如lodash类似库会有更多的常用的方法提供

事实上我们的mapfilterreduce 都是高阶函数。

函数式编程不是教条

自此我们完成了这段代码的一系列改造。但是优化其实还没有结束,比如:

  • 我们拆分的函数是否贴合业务?
  • 这个实现对于大部分同学来说是否能够完全接受?
  • 这些等等问题都是我们需要在实际中考虑的

很多时候我们会舍弃使用一些“函数式编程”style的方式编写代码。转而使用原始的for循环等。因为有很多地方其实用函数式编程会变得费解,可读性甚至会下降。基于项目管理的思考,我们往往对这些功能进行舍弃。我们最终目的不是将JavaScript写成完全函数式。函数式编程更多的是一种工具和方法。

如果我们能够利用好 纯函数map/reduce 等特性,我们就能够比较好的在这些方面做出优化。

函数式编程也拥有其他很好的概念,比如我们常常听说的 科里化偏函数 等。本文篇幅下没有介绍这些内容,可以参考Reference中的链接进一步阅读相关内容。

拥抱 es6/es7

上述的改造的示例代码中,我们大量使用es6/es7相关内容,使得代码更加简洁和可读。

es6/es7,是 JavaScript 语言的一种迭代。在实际中发现,很多前端或者从事前端相关开发的同学,他们会畏惧学习前端的一些新知识,或者认为es6是一些高级玩法。其实不然。es6/es7为我们提供了很多很好的语言特性,很多语言特性简化了我们日常一些繁琐的编程操作,同时提供很多函数式编程方面支持,我们应该拥抱并且积极地拥抱这种语言的变化。

一些推荐的库

Reference & Further Reading

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值