点击此处阅读原文:函数式编程在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
我们可以很直观、很明显感觉到,这段代码很有不少问题。
我们可以列举这段代码的一些问题:
- 可读性:代码篇幅很长,将一大堆逻辑堆在了Promise的回调函数中,要理解代码很困难
- 可复用性:没有任何的可复用性,需求可能稍微改动,这个代码就得相应进行很多修改
- 可测试性:基本上很难去编写任何测试
- 可维护性:如果出现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));
上述代码被称为反范式设计,在这个极端例子中:
- 很明显,
f1
和f2
两个函数都是非纯的,他们依赖于一个全局变量Steate - 我们很难直观看出这两个函数的输出是什么,2和4,还是4和4?让人非常迷惑
- 将setTimeout替换为异步请求我们不难发现,一些项目代码中存在类似函数的蛛丝马迹
- 显然,数学函数是不会做这种事情的。我们没有见数学函数会去修改全局变量的
相反,我们可以了解到,函数式编程中拥有一些很好的特点:
- 函数式编程中的函数的输入和输出是明确的
- 降低了编程的复杂度
- 测试将变得简单许多
- 代码会更可读,更好理解
接下来我们讲解一些实际应用
Pure Function (纯函数)
纯函数是函数式编程中一个非常重要的概念。
纯函数指的是满足以下条件的函数:
- 无论执行多少次,相同的输入都会产生相同的输出
- 也就是说,函数的输入决定函数的输出
- 函数的输出只依赖于函数的输入
- 如数学函数 f ( x ) = x 2 + 1 f(x) = x^2 + 1 f(x)=x2+1
- 不会产生副作用 (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)是一种更高层次的编程。我们可以这样理解:
声明式编程:
蔬菜.做成菜(蔬菜沙拉)
而命令式编程则更像是
洗干净(蔬菜)
混合(蔬菜,沙拉)
放入盘中(混合物)
- 命令式编程更专注“我们应该怎么做?”
- 声明式编程则更专注“我们要得到什么?”
函数式编程整体风格更倾向于声明式编程。接下来我们介绍的map
、filter
、reduce
也更接近声明式的编程。
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();
上述我们的除了 main
、fetchUsers
、printResult
每一个函数都是纯函数。
我们对原来的代码进行修改,在很大程度上提高了:
- 可读性:入口、每个函数的职责非常明确,比原来循环的可读性要好不少
- 可复用性:一些抽离的函数可以应付后续的一些需求
- 可测试性:除了
main
、fetchUsers
、printResult
每一个函数都是纯函数,我们只需要做一些输入、期望输出的测试用例,即可测试函数的正确性。 - 可维护性:可维护性更好,出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类似库会有更多的常用的方法提供
事实上我们的map
,filter
和 reduce
都是高阶函数。
函数式编程不是教条
自此我们完成了这段代码的一系列改造。但是优化其实还没有结束,比如:
- 我们拆分的函数是否贴合业务?
- 这个实现对于大部分同学来说是否能够完全接受?
- 这些等等问题都是我们需要在实际中考虑的
很多时候我们会舍弃使用一些“函数式编程”style的方式编写代码。转而使用原始的for
循环等。因为有很多地方其实用函数式编程会变得费解,可读性甚至会下降。基于项目管理的思考,我们往往对这些功能进行舍弃。我们最终目的不是将JavaScript写成完全函数式。函数式编程更多的是一种工具和方法。
如果我们能够利用好 纯函数
、map/reduce
等特性,我们就能够比较好的在这些方面做出优化。
函数式编程也拥有其他很好的概念,比如我们常常听说的 科里化
、偏函数
等。本文篇幅下没有介绍这些内容,可以参考Reference中的链接进一步阅读相关内容。
拥抱 es6/es7
上述的改造的示例代码中,我们大量使用es6/es7相关内容,使得代码更加简洁和可读。
es6/es7,是 JavaScript 语言的一种迭代。在实际中发现,很多前端或者从事前端相关开发的同学,他们会畏惧学习前端的一些新知识,或者认为es6是一些高级玩法。其实不然。es6/es7为我们提供了很多很好的语言特性,很多语言特性简化了我们日常一些繁琐的编程操作,同时提供很多函数式编程方面支持,我们应该拥抱并且积极地拥抱这种语言的变化。