前言
在很多时候,我很喜欢使用 Array.prototype.reduce,它的功能很强大,适用范围非常广,但是很多新手对它却难以理解,即使是具备了非常多例子和使用场景的 MDN 里的 reduce 文档,看完后也让人无从下手。
我在刚认识 reduce
的时候也是如此,看了 MDN 的文档后依旧一头雾水,只感受到 reduce
很强大,但在实战中却难以使用它,在各种技术社群问 reduce
还有什么骚操作,但是好像知道的人并不是很多,亦或者大佬们都没时间理我这种连 reduce
都不知道怎么使用的技术小白。后来我才发现,对于 reduce
这个方法,人们更需要的是去理解它,而不在于它如何使用或者它有哪些操作,在理解了之后自然就会知道什么时候可以使用它。
性能说明
很多人很纠结 for
、forEach
、map
这几个方法和 reduce
到底哪个效率更高,事实上这对于前端来说不需要考虑太多,如果你觉得你的前端项目很卡,问题大多不会出现在代码之中,页面重排重绘才是页面卡顿的幕后黑手。
你的页面不会因为你用哪个循环方法而产生巨大的性能改变,除非你的数据真的达到了那种千万亿万的等级以至于一个循环终于需要好几秒而且必须要前端处理,对此我们除了可以在一些特定场景中使用一些高效的算法比如排序或者是二分查询等等,如果硬要全部循环一遍我们也有 webWorker
、 webAsemmbly
这些更优的选择来处理这种情况,而并不需要我们在选用循环方法上面去考虑,这不仅会破坏我们整体代码的语义结构,也会使你在写代码时如履薄冰,实在是没有必要。(当然如果是追求 leetcode 速度,那就用 for
吧)
理解 reduce
在讲我的理解之前,还是要贴一下 reduce
的一般使用方式。
const arr = Array.from({length: 10}, (item, index) => ~~(Math.random()*100) * index)
const result = arr.reduce((acc, cur, index, source) => {
// your code
return acc
}, defaultValue)
reduce 应该是个数据生成器
其实 reduce
很多人很难上手它是因为它本身就很不像一个循环方法,像数组原型常见的 map
,forEach
,some
,every
等等循环方法,使用的循环方法传参都是一样的,而 reduce
则像个另类,因为它能返回任何类型的数据。
有使用过前端框架 React
的朋友可能知道,在 React
中有个 useReducer
的 hooks
,在 Redux
中也有个 reducer
,他们的名称里都带有 reduce
,虽然在使用上有所差别,但他们也都有一个共同点,就是将上次处理的结果,返回给下次使用。
我觉得 reduce
确实跟循环的意义有所不同,数组循环是对每一项数组项进行处理,而 reduce
更像是一个步骤器,每一次循环是一个步骤,每个步骤结束后把这个步骤处理好的数据丢给下个步骤使用,直到循环结束抛出最后结果。
这也是为什么在我们使用 reduce
时,每次方法结束都必须要返回值,若不返回值的话,下个步骤就无法拿到 acc
的值(undefined
),就像在工厂流水线中,在某个流程有人把整个产品直接扔掉了,那做下一步工作的人就拿不到这个产品了。
所以在我看来,reduce
并不是一个好的循环方法,但它是一个很强大的数据生成器,它可以无拘无束地生成各种类型的数据包括数组、字符串、普通对象、 Set 类型等等。
从 polyfill 去理解
if (!Array.prototype.reduce) {
Object.defineProperty(Array.prototype, 'reduce', {
value: function(callback) {
var o = Object(this);
var len = o.length >>> 0;
var k = 0;
var value = arguments[1];
while (k < len) {
if (k in o) value = callback(value, o[k], k, o);
k++;
}
return value;
}
});
}
可以看到,整个 reduce
的流程简洁明了,在 while
中,value
在被不断地重写新值,返回最后一次被重写的 value
值,这个 value
就是我们每次拿到的 acc
,也就是每次方法处理后的值,最后的一次 acc
就是 reduce
返回的结果。
不应该用 reduce 去替代常规的循环方法
这一点其实很多人都懂,语义化问题嘛,但我总觉得大多数不用完完全全是因为不会使用 reduce
而不去使用,而不是真正的因为语义问题。
语义问题有时候可以说是一门代码哲学,像 html 标签也可以有语义问题,但是很多人还是 div
+class
一把梭,因为语义问题并不是一种代码问题,它是为了代码能更好的被每个写代码的人理解。
从极限视角来看,如果你完全不在乎代码语义的话,计算机为什么还要去编译代码呢?你直接用计算机懂的语言写不用编译不用解释高性能无敌它不香吗?这里说一句题外话,这也是 webAssembly
的思想所在。
所以说,代码的语义对于整个代码项目是很有帮助的,它让我们可以通过合理的语义了解这段代码在执行什么,从而提高代码的可维护性和易读性。
reduce 使用场景
你只要不将 reduce
当作循环方法,而是将它当成一个数据生成器,只是它的原材料来自于某个数组当中的元素,这样的话很多场景都很好理解。
这里主要介绍一下几种常用的场景。
生成 Object
这个场景大多数是数据归类,例如把数据 [1,2,3,'a','b','c',[1,2,3],['a','b','c'],{a:1},{b:2},{c:3}]
按数据类型归类或者生成数据字典。
①数据归类
const data = [1,2,3,'a','b','c',[1,2,3],['a','b','c'],{a:1},{b:2},{c:3}]
function classify (data) {
return Object.prototype.toString.call(data).slice(8, -1)
}
const result = data.reduce((acc,cur) => {
const className = classify(cur)
acc[className] ? acc[className].push(cur) : acc[className] = [cur]
return acc
}, {})
console.log(result)
②生成数据字典
const data = [
{ name: 'Alice', id: 1 },
{ name: 'Max', id: 2 },
{ name: 'Jane', id: 3 },
{ name: 'roxz', id: 4 },
{ name: 'zonby', id: 5 },
{ name: 'zark', id: 6 },
];
const dataMap = data.reduce((acc, cur) => (acc[cur.id] = cur, acc), {})
console.log(dataMap)
生成 String
这种场景比较不多见,因为很少有要根据数组元素对应生成不同的字符串并拼接,很多时候可以用数组方法跟 join
方法实现。但我们还是可以举点特别的例子。
const data = ['a','b','d','h',123,'x']
function dispatchString (string) {
switch(data) {
case 'a':
return 'abc'
case 'd':
return 'def'
case 'x':
return 'xyz'
default:
return '|'
}
}
const result = data.reduce((acc,cur) => acc += dispatchString(cur), '')
console.log(result)
生成 Array
这个使用场景倒是非常常见,很多场景下它可以节约循环的次数,比如对于某个数组需要先对数据进行处理然后再进行过滤,一般来说就是先 map
再 filter
,但是可以用 reduce
一次循环解决。但是对应的也有语义问题,最好是简短一些或者封装成方法。
const data = [1,2,3,4,5,6,7,8,9]
// 数组正常处理
const mapFilterResult = data.map(item => ~~(Math.random()*100) * item).filter(item => item > 50)
// reduce 处理
const reduceResult = data.reduce((acc, cur) => {
const data = ~~(Math.random()*100) * cur
if (data <= 50) return acc
acc.push(data)
return acc
}, [])
console.log(mapFilterResult)
console.log(reduceResult)
其他
除此之外,还可以用来把二维数组转换成一维数组,但是其实 Array 对象类型是有原型方法 Array.prototype.flat
的,所以并不需要特意用 reduce
来实现。
reduce 的限制
虽然 reduce
手脚通天,可以无拘无束的生成各种数据,但它也有本身的限制,和大多数循环方法一样,reduce
的循环也是按基本法来的,无法在循环中去决定谁先被处理谁后被处理,只能按照所提供的数组里的顺序来执行,但与常规循环方法不同的是,它可以审视之前被处理过的值,每次循环都可以拿到此次循环前已经处理过的数据。
总结
在理解了 reduce
之后再使用它,才能让我们不会误会它的语义或者行为,同时能够写出更好更准确的代码(如果你想成为那个“公司不可替代的人才”,倒是不用考虑这点)。
这其实跟现在的前端环境一样,像开发人员使用前端框架,如果不了解框架本身,我们写项目代码就跟黑盒测试一样,出错了只能要么百度谷歌,要么叨扰群里的大神,但是他们毕竟不能设身处地的为你着想,从而会让你浪费掉许许多多的时间来处理错误。相反,如果你通读框架源码,你就会知道你的项目在什么时候在执行什么,整个过程如何,从而快速定位错误,解决问题。
除此之外,知晓源码还能让你对项目的性能理解透彻,写出性能更佳,更为安全稳固的项目代码。
说明
文章是作者单一理解,也有一些主观的想法,例子也全都是纯手写的,如有错漏,可以在下方评论指出,我会积极面对错误并将其改正,感谢大家!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!