Jacque Schrag 原做,受权 New Frontend 翻译。数组
在 #DevDiscuss 聊“开发者忏悔”这个话题时,我认可 3 年前开始个人第一份开发工做时,根本不知道本身在作什么。题图中的代码是一个例子,展现了我当时是如何写代码的。浏览器
我收到太多分享相似经历的回应。咱们中的大多数人都写过让本身羞愧的糟糕代码(硬写一些愚蠢代码,虽然能够完成任务所需,但本能够写得更高效)。可是当咱们回顾过去的代码时,若是能意识到咱们能够如何写得更好,乃至以为当初作的选择很好笑,那么这是一个成长的标志。秉承持续学习的精神,我想要分享一些如今的我会怎么来写这段代码的方式。安全
在重构任何陈年代码以前,评估当初写代码时的上下文是极为关键的一步。开发者当初作的某个疯狂决策背后可能有一个重要的缘由,源自你不了解的上下文(或者源自你不记得的上下文,若是代码是你写的)。个人这个例子,则单纯是由于缺少经验,因此我能够安全地重构代码。ide
这段代码是为两张数据可视化图表写的。它们的数据和功能类似,主要目标是让用户能够根据类型、年份、区域过滤查看数据集。性能
咱们假定改变过滤器会返回如下数值:学习
let currentType = 'in' // 或 'out'
let currentYear = 2017
let currentRegions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
复制代码
最后,下面是一个从 CSV 加载数据的简化例子:ui
const data = [
{ country: "Name", type: "in", value: 100, region: "Asia", year: 2000 },
{ country: "Name", type: "out", value: 200, region: "Asia", year: 2000 },
...
]
// 数组中总共约有 2400 条数据
复制代码
除了硬编码以外,我本来的代码彻底违反了 DRY 原则。固然有些状况下重复是有意义的,但在这个不断重复一样属性的状况下,动态建立对象是更明智的选择。这还能够下降数据集新增年份的工做量,同时下降输入错误的风险。编码
这里有好几种选择:for
、.forEach
、.reduce
。我将使用 .reduce
方法处理数组,将数组转化为其余东西(在咱们的例子中是对象)。咱们使用三次 .reduce
,每一个类别一次。spa
咱们首先声明类别常量。这样将来咱们只需在 years
数组中加上新的年份,咱们将要编写的代码会处理剩下的部分。翻译
const types = ['in', 'out']
const years = [2000, 2005, 2010, 2015, 2016, 2017]
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
复制代码
咱们想要逆转 types → years → regions 的顺序,从 regions 开始。一旦 regions
转换为对象,就能够将它赋值给 years 属性。years 和 types 同理。尽管咱们能够少写几行代码,但我选择更清晰而不是更聪明的写法。
const types = ['in', 'out']
const years = [2000, 2005, 2010, 2015, 2016, 2017]
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']
// 将 regions 转换为对象,每一个 region 是一个属性,值为一个空数组。
const regionsObj = regions.reduce((acc, region) => {
acc[region] = []
return acc
}, {}) // 累加器(`acc`)的初始值设为 `{}`
console.log(regionsObj)
// {Africa: [], Americas: [], Asia: [], Europe: [], Oceania: []}
复制代码
既然已经有了区域对象,年份和类型也能够照此处理。只不过它们的值不是像区域同样设为空数组,而是以前说的类别对象。
function copyObj(obj) {
return JSON.parse(JSON.stringify(obj))
}
// 和 regions 同样处理 years,但将每一个年份的值设为 region 对象。
const yearsObj = years.reduce((acc, year) => {
acc[year] = copyObj(regionsObj)
return acc
}, {})
// type 也同样。返回最终对象。
const dataset = types.reduce((acc, type) => {
acc[type] = copyObj(yearsObj)
return acc
}, {}
console.log(dataset)
// {
// in: {2000: {Africa: [], Americas: [],...}, ...},
// out: {2000: {Africa: [], Americas: [], ...}, ...}
// }
复制代码
咱们如今获得的效果和我最初的代码是一致的,然而咱们成功地将它重构成可读性更强、更容易重构的代码!须要在数据集中新增年份时不再须要复制粘贴了!
不过还有一个问题:咱们仍然须要手工更新年份列表。并且既然咱们将在对象中加载数据,没理由单独初始化一个空对象。下面的两个重构选项彻底脱离了我最先的代码,展现了如何直接使用数据。
附注:老实说,若是我在 3 年前尝试重构,我大概会用 3 层嵌套的 for
循环,并对此表示满意。就像 Stephen Holdaway 在评论中给出的写法:
const types = ['in', 'out'];
const years = [2000, 2005, 2010, 2015, 2016, 2017];
const regions = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania'];
var dataset = {};
for (let typ of types) {
dataset[typ] = {};
for (let year of years) {
dataset[typ][year] = {};
for (let region of regions) {
dataset[typ][year][region] = [];
}
}
}
复制代码
我以前使用 reduce
的写法避免了过深的嵌套。
有些读者大概想知道为何咱们要把数据按类型分组。咱们本可使用 .filter
根据 currentType
(当前类型)、currentYear
(当前年份)、currentRegion
(当前区域) 返回所需数据,就像这样:
/* `.filter` 会建立一个新数组,其中全部的成员均匹配 `currentType` 和 `currentYear`。 `includes` 根据 `currentRegions` 是否包含条目的 region 返回真假。 */
let currentData = data.filter(d => d.type === currentType && d.year === currentYear && currentRegion.includes(d.region))
复制代码
尽管这一行代码效果不错,但我不建议在咱们的例子中使用它,缘由有两个:
没错,我当年硬编码了选项。每次新增一个年份,我须要记住同时更新 JS 和 HTML。
咱们能够将前两个选项组合一下,获得第三种重构方式。这种方式的目标是在更新数据集时彻底不须要修改代码,直接根据数据肯定类别。
一样,要作到这一点,技术上有多种方法。不过,我将继续使用 .reduce
。
const dataset = data.reduce((acc, curr) => {
// 若是累加器的属性中已存在当前类型,将其设为自身,不然初始化为空对象。
acc[curr.type] = acc[curr.type] || {}
// 年份同理
acc[curr.type][curr.year] = acc[curr.type][curr.year] || []
acc[curr.type][curr.year].push(curr)
return acc
}, {})
复制代码
注意上面的代码中不包括区域。这是由于,和类型、年份不一样,能够同时选中多个区域。这使得预先根据区域分组毫无做用,要是这么作了,咱们还得合并它们。
考虑到这一点,下面是新版的根据选定类型、年份、区域获取 currentData
的一行代码。因为咱们将数据的查找范围限定于当前类型和当前年份,咱们知道数组中数据项数目的最大值等于国家数(小于 200),这就比选项二中的 .filter
实现要高效不少。
let currentData = dataset[currentType][currentYear].filter(d => currentRegions.includes(d.region))
复制代码
最后一步是获取不一样类型、年份、区域的数组。为此我将使用 .map
和集合。下面是一个例子,获取一个数组,包含数据中全部不一样区域。
// `.map` 将提取特定对象属性值(例如,区域)到新数组
let regions = data.map(d => d.region)
// 根据定义,集合中的值是惟一的。重复值将被剔除。
regions = new Set(regions)
// Array.from 根据集合建立数组。
regions = Array.from(regions)
// 单行版本
regions = Array.from(new Set(data.map(d => d.region)))
// 或者使用 ... 操做符
regions = [...new Set(data.map(d => d.region))]
复制代码
使用一样的方法处理类型和年份。接着就能够根据数组的值动态建立过滤界面。
最终咱们获得了以下的重构代码,将来数据集新增年份无需手工改动。
// 类型、年份、区域
const types = Array.from(new Set(data.map(d => d.type)))
const years = Array.from(new Set(data.map(d => d.year)))
const regions = Array.from(new Set(data.map(d => d.region)))
// 根据类型和年份分组数据
const dataset = data.reduce((acc, curr) => {
acc[curr.type] = acc[curr.type] || {}
acc[curr.type][curr.year] = acc[curr.type][curr.year] || []
acc[curr.type][curr.year].push(curr)
return acc
}, {})
// 根据选中内容获取数据
let currentData = dataset[currentType][currentYear].filter(d => currentRegions.includes(d.region))
复制代码
调整格式仅仅是重构的一小部分,“重构代码”经常意味着从新构想实现和不一样部分之间的关系。解决问题有多种方式,因此重构不容易。一旦找到有效的解决方案,可能不太容易去考虑不一样作法。肯定哪一种解决方案更好并不老是显而易见的,可能很大程度上取决于代码的上下文,甚至,我的偏好。
想要更好地重构代码,我有一条简单的建议:阅读更多代码。若是你在团队里,积极参与代码审阅。若是有人让你重构代码,问下为何而且尝试去理解其余人处理问题的方式。若是你单独工做(就像我刚开始工做时同样),留意同一问题的不一样解决方案,同时搜寻最佳实践指南。我强烈推荐阅读 Jason McCreary 的 BaseCode,编写更简单、更可读代码的指南,其中包含不少真实世界的例子。
最重要的是,承认这一事实,有时你会写下糟糕的代码,重构(让它变得更好)是成长的标志,值得庆祝。