前言
补充一:看来很多人没看完文章就评论了。我在文章末尾说了,是不写 for 循环,不是不用 for 循环。简单陈述不写 for 循环的理由:for 循环易读性差,而且鼓励写指令式代码和执行副作用。更多参考
补充二:回应大家的一些反对意见。本来准备专门写文章回应的,但是没时间,就简短回复,直接扔链接了。
1、for 循环性能最好。回应:微观层面的代码性能优化,不是你应该关注的。我在文章中演示了,对百万级数据的操作,reduce 只比 for 循环慢 8 ms,可忽略不计。如果你要操作更大的数据,要考虑下换语言了。
2、不用 for 循环不能 break。回应:用递归。我在里有解释怎样解决递归爆栈。
3、框架都用 for 循环!回应:框架考虑的场景和你不一样。React 和 Vue 还用 class 来创建对象呢。你该跟着学吗?事实上你应该用工厂函数。
一,用好 filter,map,和其它 ES6 新增的高阶遍历函数
问题一:
将数组中的空值去除
答案:
问题二:
将数组中的 VIP 用户余额加 10
答案:
问题三:
判断字符串中是否含有元音字母
答案:
containsVowel(randomStr);
问题四:
判断用户是否全部是成年人
答案:
问题五:
找出上面用户中的未成年人
答案:
findTeen(users);
问题六:
将数组中重复项清除
答案:
uniq(dupArr);
问题七:
答案:
genNumArr(10,100);
二,理解和熟练使用 reduce
问题八:
不借助原生高阶函数,定义 reduce
答案:
问题九:
将多层数组转换成一层数组
答案:
问题十:
将下面数组转成对象,key/value 对应里层数组的两个值
答案:
fromPairs(objLikeArr);
问题十一:
取出对象中的深层属性
答案:
pluckDeep("a.b.c")(deepAttr);
问题十二:
将用户中的男性和女性分别放到不同的数组里:
答案:
const isMale = person => person.sex === "male";
const [maleUser,femaleUser] = partition(users,isMale);
问题十三:
reduce 的计算过程,在范畴论里面叫 catamorphism,即一种连接的变形。和它相反的变形叫 anamorphism。现在我们定义一个和 reduce 计算过程相反的函数 unfold(注:reduce 在 Haskell 里面叫 fold,对应 unfold)
根据这个 unfold 函数,定义一个 Python 里面的 range 函数。
答案:
三,用递归代替循环
问题十四:
将两个数组每个元素一一对应相加
答案:
const add = x => y => x + y;
zipWith(add)(num1)(num2);
问题十五:
将 Stark 家族成员提取出来。注意,目标数据在数组前面,使用 filter 方法遍历整个数组是浪费。
答案:
const isStark = name => name.toLowerCase().includes("stark");
takeWhile(isStark)(houses);
四,使用高阶函数遍历数组时可能遇到的陷阱
问题十六:
从长度为 100 万的随机整数组成的数组中取出偶数,再把所有数字乘以 3
能运行的答案:
bigArr.filter(isOdd).map(triple);
注意,上面的解决方案将数组遍历了两次,无疑是浪费。如果写 for 循环,只用遍历一次:
在我的电脑上测试,先 filter 再 map 的方法耗时 105.024 ms,而采用 for 循环的方法耗时仅 25.598 ms!那是否说明遇到此类情况必须用 for 循环解决呢? No!
五,死磕到底,Transduce!
我们先用 reduce 来定义 filter 和 map,至于为什么这样做等下再解释。
const map = (f,arr) => arr.reduce((acc,val) => (acc.push(f(val)),[]);
重新定义的 filter 和 map 有共有的逻辑。我们把这部分共有的逻辑叫做 reducer。有了共有的逻辑后,我们可以进一步地抽象,把 reducer 抽离出来,然后传入 filter 和 map:
const map = f => reducer => (acc,value) => reducer(acc,f(value));
现在 filter 和 map 的函数 signature 一样,我们就可以进行函数组合(function composition)了。
bigNum.reduce(map(triple)(filter(isOdd)(pushReducer)),[]);
但是这样嵌套写法易读性太差,很容易出错。我们可以写一个工具函数来辅助函数组合:
然后我们就可以优雅地组合函数了:
经过测试(用 console.time()/console.timeEnd()),上面的写法耗时 33.898 ms,仅比 for 循环慢 8 ms。为了代码的易维护性和易读性,这点性能上的微小牺牲,我认为是可以接受的。
这种写法叫 transduce。有很多工具库提供了 transducer 函数。比如 nofollow" target="_blank" href="https://github.com/cognitect-labs/transducers-js">transducers-js。除了用 transducer 来遍历数组,还能用它来遍历对象和其它数据集。功能相当强大。
六,for 循环和 for ... of 循环的区别
for ... of 循环是在 ES6 引入 Iterator 后,为了遍历 Iterable 数据类型才产生的。EcmaScript 的 Iterable 数据类型有数组,字符串,Set 和 Map。for ... of 循环属于重型的操作(具体细节我也没了解过),如果用 AirBNB 的 ESLint 规则,在代码中使用 for ... of 来遍历数组是会被禁止的。
那么,for ... of 循环应该在哪些场景使用呢?目前我发现的合理使用场景是遍历自定义的 Iterable。来看这个题目:
问题十七:
将 Stark 家族成员名字遍历,每次遍历暂停一秒,然后将当前遍历的名字打印来,遍历完后回到第一个元素再重新开始,无限循环。
答案:
const wait = ms =>
new Promise(resolve => {
setTimeout(() => {
resolve();
},ms);
});
(async () => {
for (const name of infiniteNameList) {
await wait(1000);
console.log(name);
}
})();
七,放弃倔强,实在需要用 for 循环了
前面讲到的问题基本覆盖了大部分需要使用 for 循环的场景。那是否我们可以保证永远不用 for 循环呢?其实不是。我讲了这么多,其实是在鼓励大家不要写 for 循环,而不是不用 for 循环。我们常用的数组原型链上的 map,filter 等高阶函数,底层其实是用 for 循环实现的。在需要写一些底层代码的时候,还是需要写 for 循环的。来看这个例子:
注意,这个例子只是为了好玩。生产环境中不要直接修改 JS 内置数据类型的原型链。原因是 V8 引擎有一个原型链快速推测机制,修改原型链会破坏这个机制,造成性能问题。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对编程之家的支持。
原文链接:https://www.f2er.com/js/31315.html