常见问题
打个比方,你有一组数据,需要根据拼音首字母字母索引显示在页面上,如图:
这种索引性的展示已经随处可见。对于我们前端开发来说这种展示最喜欢的数据格式可能是这样的:
var cardList = [ { "key":"p", "cars":[ { "id":"888888888888888888", "name":"帕加尼", "logo":"http://...", "initial":"p" } ] }, { "key":"q", "cars":[ { "id":"888888888888888889", "name":"起亚", "logo":"http://...", "initial":"q" }, { "id":"888888888888888890", "name":"奇瑞", "logo":"http://...", "initial":"q" }, ... ] }, ... ];
或者这样:
var cars = { "p":[ { "id":"888888888888888888", "name":"帕加尼", "logo":"http://...", "initial":"p" } ], "q":[ { "id":"888888888888888889", "name":"起亚", "logo":"http://...", "initial":"q" },{ "id":"888888888888888890", "name":"奇瑞", "logo":"http://...", "initial":"q" }, ... ] ... };
相信对于大多数的前端来说,这样的数据去格式化模板都是非常受欢迎的。相比之下,对于上面的例子来说,第一种数据格式更加方便前端大多数的模板使用,而第二种方式更加直观。然而,我们后端的同事吐给我们的很可能是这样的数据格式:
var cars = [ { "id":"888888888888888888", "name":"帕加尼", "logo":"http://...", "initial":"p" },{ "id":"888888888888888889", "name":"起亚", "logo":"http://...", "initial":"q" }, { "id":"888888888888888890", "name":"奇瑞", "logo":"http://...", "initial":"q" } ... ];
相信大家都多次遇到过类似的问题,除了转换数据格式外我找不到一个真正满意的解决方案。对于前端来说最简单办法可能是叫后端转换成你想要的数据格式。不过这肯定不是这篇文章想要说明的,我们需要自己动手将数据格式转换成我们想要的。根据上面的例子,做直观的做法是将数据按首字母分组,可惜的是 Javascript 的 Array 对象中没有原生的 groupBy()
方法,但我们自己添加一个不是太难。
groupBy()
Javascript 提供了许多其他功能强大的方法,比如 map()
、filter()
、sort()
等等。今天我们用一个 ES5 的方法 reduce()
。
reduce()
方法的工作原理是 将累加器和数组中的每个元素 (从左到右)应用一个函数, 本质上它的目的是将一个数组 “减少” 成一个对象。详细说明可以查看 MDN 文档。下面就用 reduce()
方法来实现 groupBy()
:
Array.prototype.groupBy = function(prop) { return this.reduce(function(groups, item) { var val = item[prop].toUpperCase(); groups[val] = groups[val] || []; groups[val].push(item); return groups; }, {}); }
这里来稍微解析一下:
Array.prototype.groupBy = function(prop) { ... }
首先,我们在 Array 的原型对象上添加一个 groupBy 方法,这意味我们可以在任何 Array 对象上调用这个方法,类似这样:.groupBy(prop)
。其中 prop
参数使我们想要分组的属性名。当然如果你觉得这样做会污染 Array 的原型对象的话,你可以将其改写成工具函数:
function groupBy (arr,prop){ return arr.reduce(function(groups, item) { var val = item[prop].toUpperCase(); groups[val] = groups[val] || []; groups[val].push(item); return groups; }, {}); }
接下来我们看方法内部实现。
return this.reduce(function(groups, item) { ... }, {});
我们在需要处理的数组上调用 reduce()
方法,并传入两个参数,第一个是 callback ,我们要在数组中的每个元素上运行的函数,第二个是累加器,本质上说这个参数是用于第一次调用 callback 函数的第一个参数。因为我们需要得到是一个对象,为此,我们使用一个空对象 {}
。
reduce()
方法将遍历数组中的每个元素,通过传入累加器和当前元素调用我们的 callback 函数。该 callback 函数的工作是找出将元素放在累加器中的位置,然后返回更新的累加器,具体实现:
var val = item[prop].toUpperCase(); groups[val] = groups[val] || []; groups[val].push(item); return groups;
我们希望根据给定属性的值对所有数组元素进行分组。所以如果该属性是 initial
,那么在开始的例子中,起亚和奇瑞应该在一个分组中,因为他们的首字母都是q
,
- 第 1 行:将获取当前数组元素给定属性的属性值,为了展示需要,使用
toUpperCase()
将其转换为大写字母。 - 第 2 行:是检查我们是否已经为给定属性的属性值设置了一个新的数组,如果累加器中没有该数组,则添加一个空数组。
- 第 3 行:将当前数组元素添加到刚刚添加的数组中。
- 第 4 行:返回更新的累加器,传递给下一次遍历,因此下一个遍历的数组元素可以更新到累加器上。
没几行代码,就可以帮我们完整数据格式转换。
示例
回调最开始的例子,我们来验证一下我们的 groupBy()
方法。
Array.prototype.groupBy = function(prop) { return this.reduce(function(groups, item) { var val = item[prop].toUpperCase(); groups[val] = groups[val] || []; groups[val].push(item); return groups; }, {}); } var cars = [ { "id":"888888888888888888", "name":"帕加尼", "logo":"http://...", "initial":"p" },{ "id":"888888888888888889", "name":"起亚", "logo":"http://...", "initial":"q" }, { "id":"888888888888888890", "name":"奇瑞", "logo":"http://...", "initial":"q" } ]; var cardGurop = cars.groupBy("initial"); // cardGurop 的结果是: // { // "P":[ // { // "id":"888888888888888888", // "name":"帕加尼", // "logo":"http://...", // "initial":"p" // } // ], // "Q":[ // { // "id":"888888888888888889", // "name":"起亚", // "logo":"http://...", // "initial":"q" // },{ // "id":"888888888888888890", // "name":"奇瑞", // "logo":"http://...", // "initial":"q" // } // ] // }
进一步转换数据格式
好了,大功告成?或许这不是我们要的最终结果,实际工作中,以下的数据格式可能更加适用于我们前端模板:
var cardList = [ { "key":"P", "cars":[ { "id":"888888888888888888", "name":"帕加尼", "logo":"http://...", "initial":"p" } ] }, { "key":"Q", "cars":[ { "id":"888888888888888889", "name":"起亚", "logo":"http://...", "initial":"q" }, { "id":"888888888888888890", "name":"奇瑞", "logo":"http://...", "initial":"q" }, ... ] }, ... ];
那么我们可能需要进一步装换一下。思路也很简单,将 groupBy()
方法装换出来的 cardGurop
对象再装换一次:
var keyArr = Object.keys(cardGurop); var cardList = []; keyArr.forEach(key => { cardList.push({ "key" : key, "cars" : cardGurop[key] }) }); // cardList 的结果是: // [ // { // "key":"P", // "cars":[ // { // "id":"888888888888888888", // "name":"帕加尼", // "logo":"http://...", // "initial":"p" // } // ] // }, // { // "key":"Q", // "cars":[ // { // "id":"888888888888888889", // "name":"起亚", // "logo":"http://...", // "initial":"q" // }, // { // "id":"888888888888888890", // "name":"奇瑞", // "logo":"http://...", // "initial":"q" // } // ] // } // ];
总结
实际工作中,掌握一些数据格式装换的技能非常有用,也非常常见,后端同事对于同一份数据可能只会给你一个接口或一种数据格式。除非你碰到了一个大好人,碰到个较真的会跟你谈人生,谁都希望自己代码的逻辑简单。当然我这里只是给了一个最简单的示例,你可能会碰到数据格式装换或者数据操作更加复杂的情况。学习 Javascript 基础和原生对象的方法非常重要。
当然,你也可以使用一些工具类库来解决问题,比如 Lodash 和 Underscore.js 都包含我们上面说的 groupBy
方法,当然还有很多其他有用的方法。不过对于一些有洁癖的工程师来说,一个简单的小功能,引入一个类库,确实有点不值。
最新评论
写的挺好的
有没有兴趣翻译 impatient js? https://exploringjs.com/impatient-js/index.html
Flexbox playground is so great!
感谢总结。
awesome!
这个好像很早就看到类似的文章了
比其他的教程好太多了
柯理化讲的好模糊…没懂