# JS 数据处理
# 分享目标
- JS 数据处理技巧速成
- 让你感叹:JS 还可以这样写
# 基础
# 实践原则
- 数据处理时,尽量避免创建临时变量(特别是 let)、修改参数、改变外部引用、for、if 等
- 分离 数据处理 与 副作用(DOM 操作、存储、网络请求等)代码
数据处理指:数值计算和数据结构变换。
# 第一点:如何避免?
1. 熟悉并灵活使用自带函数
- 类型构造函数
Array, Boolean, String, Number- 简单 Object
Object.assign, Object.keys, Object.values, Object.entries, Object.fromEntries- Array
map, filter, reduce, slice, concat, flat, join
pop, push, shift, unshift, splice, sort, fill
some, every, includes
find, findIndex, indexOf
forEach- Math
Math.random, Math.floor, Math.round, Math.ceil
例:
['1', '2', '3']
.map(Number) // => [1, 2, 3]
[(1, 2, 3)].map(String); // => ['1', '2', '3']
Array(10)
.fill()
.map(Math.random) // => [...] 十个随机数
[(0, 1, null, {}, [])].filter(Boolean) // => [1, {}, []] 过滤假值
[('1', '2', '3')].map(parseInt); // => ? <危>
TIP
使用map、reduce、forEach
等函数代替for
;filter
替代if
。
2. 管道、函数合成
高阶内容,下文介绍
# 第二点:如何分离?
分离并不一定要隔离到不同的函数,也可以在同一个函数中分段。
例:
// 榜单:排序后将前三名插入的容器中
const data = [
{ nickName: '张三', score: 75 },
{ nickName: '李四', score: 10 },
{ nickName: '王五', score: 42 },
/*...*/
];
const container = document.getElementById('rank');
function renderRank(container, data) {
// 计算逻辑
const items = data
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(createRankItem);
// 副作用操作
container.append(...items);
function createRankItem({ nickName, score }) {
const it = document.createElement('li');
it.text = `${nickName} - ${score}`;
return it;
}
}
renderRank(container, data);
# 进阶
# 柯里化
柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
说人话:
const f = (a, b, c) => a + b + c;
currying(f)(1)(2)(3); // => 6
# 自动柯里化
如果接收参数个数满足预期,则求值;否则返回新的可以接受剩余参数的函数。
const f = (a, b, c) => a + b + c;
autoCurrying(f)(1)(2, 3); // => 6
柯里化意义:方便函数的合成、复用。
复用例子:
const discount8 = (price) => price * 0.8;
// x表示乘法,x支持自动柯里化
const discount8 = x(0.8);
合成下文介绍
# lodash/fp
总结几点,让大家形成初步印象
- JS 自带数据结构是有限的,那对数据结构的简单操作也是有限的。
- lodash 提供了几乎所有简单或常用的操作工具函数。
- lodash/fp 则使 lodash 提供的工具函数支持自动柯里化。
类似的库还有Ramda.js (opens new window),甚至可以说 Ramda 在函数式方面更“专业”,但鉴于 lodash 的流行度、较低的学习成本,下文例子采用 lodash/fp
# 例:数组求和实现方法对比
function sum(arr) {
let rs = 0;
for (const v of arr) {
rs += v;
}
return rs;
}
sum([1, 2, 3]); // => 6
const sum = (arr) => arr.reduce((a, b) => a + b);
sum([1, 2, 3]); // => 6
import { reduce, add } from 'lodash/fp';
const sum = reduce(add, 0); // lodash/fp reduce要求三个参数
sum([1, 2, 3]); // => 6
// 等价于 reduce(add, 0)([1, 2, 3])
// Lisp中符号‘+’ 也是一个函数,可以这样写:(+ 1 2 3) 或 (apply + [1 2 3])
# pipe
在使用 Unix 或 Linux 命令行的时候,其实已经接触过管道的概念了(符号|
),把这个概念应用到编程语言中是这样的:
总结:不使用所要处理的值,只合成运算过程。
# 简单合成
// 大甩卖促销逻辑:涨价50%,再打八折
const sales = (price) => discount8(price * 1.5);
// pipe是用来合成函数的工具函数
const sales = pipe(x(1.5), x(0.8));
# 简单示例
找到用户 Scott 的所有未完成任务,并按到期日期升序排列。
const { pipe, prop, filter, matches, sortBy } = require('lodash/fp');
const data = {
result: 'SUCCESS',
interfaceVersion: '1.0.3',
requested: '10/17/2020 15:31:20',
lastUpdated: '10/16/2020 10:52:39',
tasks: [
{
id: 104,
complete: false,
priority: 'high',
dueDate: '2020-11-29',
username: 'Scott',
title: 'Do something',
created: '9/22/2020',
},
{
id: 105,
complete: false,
priority: 'medium',
dueDate: '2020-11-22',
username: 'Lena',
title: 'Do something else',
created: '9/22/2020',
},
{
id: 107,
complete: true,
priority: 'high',
dueDate: '2020-11-22',
username: 'Mike',
title: 'Fix the foo',
created: '9/22/2020',
},
{
id: 108,
complete: false,
priority: 'low',
dueDate: '2020-11-15',
username: 'Punam',
title: 'Adjust the bar',
created: '9/25/2020',
},
{
id: 110,
complete: false,
priority: 'medium',
dueDate: '2020-11-15',
username: 'Scott',
title: 'Rename everything',
created: '10/2/2020',
},
{
id: 112,
complete: true,
priority: 'high',
dueDate: '2020-11-27',
username: 'Lena',
title: 'Alter all quuxes',
created: '10/5/2020',
},
],
};
// lodash/fp
pipe(
prop('tasks'),
filter(matches({ complete: false, username: 'Scott' })),
sortBy('dueDate')
)(data);
// 原生js,没有工具函数,略微繁琐
data.tasks
.filter(({ username, complete }) => username === 'Scott' && !complete)
.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
// => output:
// [
// {
// id: 110,
// complete: false,
// priority: 'medium',
// dueDate: '2020-11-15',
// username: 'Scott',
// title: 'Rename everything',
// created: '10/2/2020'
// },
// {
// id: 104,
// complete: false,
// priority: 'high',
// dueDate: '2020-11-29',
// username: 'Scott',
// title: 'Do something',
// created: '9/22/2020'
// }
// ]
例子来源于:Pointfree 编程风格指南 (opens new window)
# 复杂示例
从 Vuepress Sidebar 中 抽取数据,生成当前博客的 sitemap (opens new window) 文件
const { tap, pipe, flatten, values, map, get, add } = require('lodash/fp');
// 数据结构示例
const vpCfg = {
title: '风痕的博客',
themeConfig: {
sidebar: {
'/': [
{
title: '前端基础课程',
children: [
'/fe-basic-course/js-data-process/',
// ...
],
},
],
},
},
};
const siteMap = pipe(
get('themeConfig.sidebar'),
values,
flatten,
map(get('children')),
flatten,
// 单独 md 没有 .html 结尾,vuepress 会有一次重定向,导致 Google 拒绝收录
map((v) => (v.endsWith('/') ? v : v + '.html')),
map(add('https://fenghen.me'))
)(vpCfg);
TIP
如果有数据变换是一个 pipe 搞不定的,那可能是你对工具函数的熟悉度不够。🐶
优雅流畅地处理数据,除了了解几个简单的基本概念之后(柯里化、管道),还需要花费一些时间来学习、练习常用的工具函数。
# 闲话
学习工具函数的作用,类似于记下编辑器许多快捷键之后,写代码的速度会有较大提升。
JS 就像流行的 VSCode,不会强迫你记快捷键,JS 本身的学习曲线挺平缓的。
Lisp 就像 Vim 编辑器,强迫你记快捷键,学习曲线就非常陡峭。
但 JS 本身非常灵活,我们可以综合两者的优势,就像在 VSCode 中开启 Vim 模式。
建议从现在开始,先实践基础篇中介绍的知识,这样会有一个相对平缓的学习曲线。
# 回顾总结
- 基础篇中分享了数据处理经验,两个实践原则;
- 数据处理时,尽量避免临时变量(特别是 let)、修改参数、改变外部引用、for、if 等
- 分离 数据处理 与 副作用(DOM 操作、存储、网络请求等)代码
- 进阶篇中介绍了柯里化、管道两个概念,然后安利了 lodash/fp。
# 参考
Pointfree 编程风格指南 (opens new window)
柯里化 (opens new window)
FP-Guide (opens new window)
Ramda 函数库参考教程 (opens new window)
Ramda.js (opens new window)