# JS 数据处理

# 分享目标

  1. JS 数据处理技巧速成
  2. 让你感叹:JS 还可以这样写

# 基础

# 实践原则

  1. 数据处理时,尽量避免创建临时变量(特别是 let)、修改参数、改变外部引用、for、if 等
  2. 分离 数据处理 与 副作用(DOM 操作、存储、网络请求等)代码

数据处理指:数值计算和数据结构变换。

# 第一点:如何避免?

1. 熟悉并灵活使用自带函数

  1. 类型构造函数
    Array, Boolean, String, Number
  2. 简单 Object
    Object.assign, Object.keys, Object.values, Object.entries, Object.fromEntries
  3. Array
    map, filter, reduce, slice, concat, flat, join
    pop, push, shift, unshift, splice, sort, fill
    some, every, includes
    find, findIndex, indexOf
    forEach
  4. 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等函数代替forfilter替代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

总结几点,让大家形成初步印象

  1. JS 自带数据结构是有限的,那对数据结构的简单操作也是有限的。
  2. lodash 提供了几乎所有简单或常用的操作工具函数。
  3. lodash/fp 则使 lodash 提供的工具函数支持自动柯里化。

FP-Guide (opens new window)

类似的库还有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 模式。
建议从现在开始,先实践基础篇中介绍的知识,这样会有一个相对平缓的学习曲线。

主流编辑器学习曲线图

# 回顾总结

  1. 基础篇中分享了数据处理经验,两个实践原则;
    • 数据处理时,尽量避免临时变量(特别是 let)、修改参数、改变外部引用、for、if 等
    • 分离 数据处理 与 副作用(DOM 操作、存储、网络请求等)代码
  2. 进阶篇中介绍了柯里化、管道两个概念,然后安利了 lodash/fp。

# 参考

Pointfree 编程风格指南 (opens new window)
柯里化 (opens new window)
FP-Guide (opens new window)
Ramda 函数库参考教程 (opens new window)
Ramda.js (opens new window)

💗 博主正处于裸辞待业状态,欢迎 商务合作 💗

Loading comments...


相关文章

从 React 看前端 UI 代码范式革命

alt text 前言 本来打算写的主题是“我为什么讨厌 React Hooks API”,展开聊聊“小甜甜”是如何变成“牛夫人”的,没想到越写越严肃:) React 是两次前端范式革命的引领者,至今仍有繁荣的社区和旺盛的创造力; React 多次天才又激进的创新,一些想法被借鉴改良、一些引发广泛质疑,大部分是被认同和接受的; ...

一句话总结:TS 何时选择 interface 或 type

用 interface 描述类型的结构,用 type 描述类型关系。 有点编程基础中数据结构与算法的味道。 结构即是类型的属性集合 // 如 Point3D 的属性集合: x, y, z。 interface Poi ...

JS 定时器时长控制细节

背景 JS 最常使用 setTimeout、setInterval 来延迟或定时循环执行函数,通常会传递第二个参数来控制延迟或间隔执行的时间。 但开发者必须意识到函数执行时间并非精确地符合预期,在以下场景中它会超出你的预期 CPU 繁忙(主线程被长时间占用),JS 无法按开发者设定的预期时间延迟函数 定时器过于频繁地执行(第二个参数 < 4),达到一定 ...

JS 多线程并发

[[toc]] 为什么需要并发 我们常听说 JS 是单线程模型,即所有代码都在主线程中执行的。 如果某些任务计算量较大,将阻塞主线程,UI 界面轻则掉帧、重则卡死。 // 提示:本文所有代理均可复制到浏览器控制台中执行,验证效果 // ...

系统化学习 TS 类型系统

目的:快速、系统性的入门 TS 类型系统 [[toc]] 前言 TS 是什么? TS 是 JS 的超集, TS = JS + 类型系统 为了描述如此复杂(由于 JS 语言的灵活性/复杂性)的类型信息,类型系统表现出非常明显的编程语言特性。 以学习编程语言的方式,来学习 TS 类型系统 关键字/符号 类型: boolean, number, stri ...

跨域(Options)请求介绍及解决方法

介绍 OPTIONS请求指method为OPTIONS的http请求。 通俗来说:它的作用是用于WEB服务器是否支持某些 header,也可以叫做预检请求(顾名思义:预先检测)。 程序员:跨域发送 http get { headers { xxx: abc } } 浏览器:等等,你这个请求有点奇怪,我去跟服务器确认下 浏览器:发送 http options ...

单测(Unit Test)技巧

前言 本文目的是提高编写单测的效率,适合于有一定单测编写经验,但被单测困扰的同学。 后文的示例都在 unit-test-examples 仓库中。 单测的意义与价值 单测本质:将测试行为及结果固化下来,自动检查被测试代码的运行结果是否符合期望。 单测是一 ...

compilerOptions字段详解

{ "compilerOptions": { /* 基本选项 */ "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' "module": ...

JS 正则表达式基础

前言 个人经验,正则是一个前期少量投入,回报超高的技能点。 其适用范围非常广泛,如批量文本处理、源码替换、程序中逻辑判断等等。 本文只介绍常用的基础知识、技巧,让初学者快速掌握大部分日常所需的正则知识。目标是 5 分钟内可逐字读完,10 分钟内可把例子都动手实践一 ...

JS优缺点

回顾上两期 优点(简单) 对象 链(原型链 & 作用域链) 一切都是对象(包括函数),构建世界的原料,越少越简单、灵活。 jimu 观察者模式,例: class Observer { constructor() { this.subscribe ...