如何在js代码中消灭for循环实例详解
前言
这篇文章基于我在公司内部分享会整理而成。欢迎探讨补充。
补充一:看来很多人没看完文章就评论了。我在文章末尾说了,是不写for循环,不是不用for循环。简单陈述不写for循环的理由:for循环易读性差,而且鼓励写指令式代码和执行副作用。更多参考这篇文章
补充二:回应大家的一些反对意见。本来准备专门写文章回应的,但是没时间,就简短回复,直接扔链接了。
1、for循环性能最好。回应:微观层面的代码性能优化,不是你应该关注的。我在文章中演示了,对百万级数据的操作,reduce只比for循环慢8ms,可忽略不计。如果你要操作更大的数据,要考虑下换语言了。
- FastcodeisNOTimportant
- TheSadTragedyofMicro-OptimizationTheater
- DitchingtheMicro-OptimizationFetish
2、不用for循环不能break。回应:用递归。我在这篇文章里有解释怎样解决递归爆栈。
3、框架都用for循环!回应:框架考虑的场景和你不一样。React和Vue还用class来创建对象呢。你该跟着学吗?事实上你应该用工厂函数。ClassvsFactoryfunction:exploringthewayforward
一,用好filter,map,和其它ES6新增的高阶遍历函数
问题一:
将数组中的空值去除
constarrContainsEmptyVal=[3,4,5,2,3,undefined,null,0,""];
答案:
constcompact=arr=>arr.filter(Boolean);
问题二:
将数组中的VIP用户余额加10
constVIPUsers=[ {username:"Kelly",isVIP:true,balance:20}, {username:"Tom",isVIP:false,balance:19}, {username:"Stephanie",isVIP:true,balance:30} ];
答案:
VIPUsers.map( user=>(user.isVIP?{...user,balance:user.balance+10}:user) );
问题三:
判断字符串中是否含有元音字母
constrandomStr="hdjrwqpi";
答案:
constisVowel=char=>["a","e","o","i","u"].includes(char); constcontainsVowel=str=>[...str].some(isVowel); containsVowel(randomStr);
问题四:
判断用户是否全部是成年人
constusers=[ {name:"Jim",age:23}, {name:"Lily",age:17}, {name:"Will",age:25} ];
答案:
users.every(user=>user.age>=18);
问题五:
找出上面用户中的未成年人
答案:
constfindTeen=users=>users.find(user=>user.age<18); findTeen(users);
问题六:
将数组中重复项清除
constdupArr=[1,2,3,3,3,3,6,7];
答案:
constuniq=arr=>[...newSet(arr)]; uniq(dupArr);
问题七:
生成由随机整数组成的数组,数组长度和元素大小可自定义
答案:
constgenNumArr=(length,limit)=> Array.from({length},_=>Math.floor(Math.random()*limit)); genNumArr(10,100);
二,理解和熟练使用reduce
问题八:
不借助原生高阶函数,定义reduce
答案:
constreduce=(f,acc,arr)=>{ if(arr.length===0)returnacc; const[head,...tail]=arr; returnreduce(f,f(head,acc),tail); };
问题九:
将多层数组转换成一层数组
constnestedArr=[1,2,[3,4,[5,6]]];
答案:
constflatten=arr=> arr.reduce( (flat,next)=>flat.concat(Array.isArray(next)?flatten(next):next), [] );
问题十:
将下面数组转成对象,key/value对应里层数组的两个值
constobjLikeArr=[["name","Jim"],["age",18],["single",true]];
答案:
constfromPairs=pairs=> pairs.reduce((res,pair)=>((res[pair[0]]=pair[1]),res),{}); fromPairs(objLikeArr);
问题十一:
取出对象中的深层属性
constdeepAttr={a:{b:{c:15}}};
答案:
constpluckDeep=path=>obj=> path.split(".").reduce((val,attr)=>val[attr],obj); pluckDeep("a.b.c")(deepAttr);
问题十二:
将用户中的男性和女性分别放到不同的数组里:
constusers=[ {name:"Adam",age:30,sex:"male"}, {name:"Helen",age:27,sex:"female"}, {name:"Amy",age:25,sex:"female"}, {name:"Anthony",age:23,sex:"male"}, ];
答案:
constpartition=(arr,isValid)=> arr.reduce( ([pass,fail],elem)=> isValid(elem)?[[...pass,elem],fail]:[pass,[...fail,elem]], [[],[]], ); constisMale=person=>person.sex==="male"; const[maleUser,femaleUser]=partition(users,isMale);
问题十三:
reduce的计算过程,在范畴论里面叫catamorphism,即一种连接的变形。和它相反的变形叫anamorphism。现在我们定义一个和reduce计算过程相反的函数unfold(注:reduce在Haskell里面叫fold,对应unfold)
constunfold=(f,seed)=>{ constgo=(f,seed,acc)=>{ constres=f(seed); returnres?go(f,res[1],acc.concat(res[0])):acc; }; returngo(f,seed,[]); };
根据这个unfold函数,定义一个Python里面的range函数。
答案:
constrange=(min,max,step=1)=> unfold(x=>x三,用递归代替循环
问题十四:
将两个数组每个元素一一对应相加
constnum1=[3,4,5,6,7]; constnum2=[43,23,5,67,87];答案:
constzipWith=f=>xs=>ys=>{ if(xs.length===0||ys.length===0)return[]; const[xHead,...xTail]=xs; const[yHead,...yTail]=ys; return[f(xHead)(yHead),...zipWith(f)(xTail)(yTail)]; }; constadd=x=>y=>x+y; zipWith(add)(num1)(num2);问题十五:
将Stark家族成员提取出来。注意,目标数据在数组前面,使用filter方法遍历整个数组是浪费。
consthouses=[ "EddardStark", "CatelynStark", "RickardStark", "BrandonStark", "RobStark", "SansaStark", "AryaStark", "BranStark", "RickonStark", "LyannaStark", "TywinLannister", "CerseiLannister", "JaimeLannister", "TyrionLannister", "JoffreyBaratheon" ];答案:
consttakeWhile=f=>([head,...tail])=> f(head)?[head,...takeWhile(f)(tail)]:[]; constisStark=name=>name.toLowerCase().includes("stark"); takeWhile(isStark)(houses);四,使用高阶函数遍历数组时可能遇到的陷阱
问题十六:
从长度为100万的随机整数组成的数组中取出偶数,再把所有数字乘以3
//用我们刚刚定义的辅助函数来生成符合要求的数组 constbigArr=genNumArr(1e6,100);能运行的答案:
constisOdd=num=>num%2===0; consttriple=num=>num*3; bigArr.filter(isOdd).map(triple);注意,上面的解决方案将数组遍历了两次,无疑是浪费。如果写for循环,只用遍历一次:
constresults=[]; for(leti=0;i在我的电脑上测试,先filter再map的方法耗时105.024ms,而采用for循环的方法耗时仅25.598ms!那是否说明遇到此类情况必须用for循环解决呢?No!
五,死磕到底,Transduce!
我们先用reduce来定义filter和map,至于为什么这样做等下再解释。
constfilter=(f,arr)=> arr.reduce((acc,val)=>(f(val)&&acc.push(val),acc),[]); constmap=(f,arr)=>arr.reduce((acc,val)=>(acc.push(f(val)),acc),[]);重新定义的filter和map有共有的逻辑。我们把这部分共有的逻辑叫做reducer。有了共有的逻辑后,我们可以进一步地抽象,把reducer抽离出来,然后传入filter和map:
constfilter=f=>reducer=>(acc,value)=>{ if(f(value))returnreducer(acc,value); returnacc; }; constmap=f=>reducer=>(acc,value)=>reducer(acc,f(value));现在filter和map的函数signature一样,我们就可以进行函数组合(functioncomposition)了。
constpushReducer=(acc,value)=>(acc.push(value),acc); bigNum.reduce(map(triple)(filter(isOdd)(pushReducer)),[]);但是这样嵌套写法易读性太差,很容易出错。我们可以写一个工具函数来辅助函数组合:
constpipe=(...fns)=>(...args)=>fns.reduce((fx,fy)=>fy(fx),...args);然后我们就可以优雅地组合函数了:
bigNum.reduce( pipe( filter(isOdd), map(triple) )(pushReducer), [] );经过测试(用console.time()/console.timeEnd()),上面的写法耗时33.898ms,仅比for循环慢8ms。为了代码的易维护性和易读性,这点性能上的微小牺牲,我认为是可以接受的。
这种写法叫transduce。有很多工具库提供了transducer函数。比如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家族成员名字遍历,每次遍历暂停一秒,然后将当前遍历的名字打印来,遍历完后回到第一个元素再重新开始,无限循环。
conststarks=[ "EddardStark", "CatelynStark", "RickardStark", "BrandonStark", "RobStark", "SansaStark", "AryaStark", "BranStark", "RickonStark", "LyannaStark" ];答案:
function*repeatedArr(arr){ leti=0; while(true){ yieldarr[i++%arr.length]; } } constinfiniteNameList=repeatedArr(starks); constwait=ms=> newPromise(resolve=>{ setTimeout(()=>{ resolve(); },ms); }); (async()=>{ for(constnameofinfiniteNameList){ awaitwait(1000); console.log(name); } })();七,放弃倔强,实在需要用for循环了
前面讲到的问题基本覆盖了大部分需要使用for循环的场景。那是否我们可以保证永远不用for循环呢?其实不是。我讲了这么多,其实是在鼓励大家不要写for循环,而不是不用for循环。我们常用的数组原型链上的map,filter等高阶函数,底层其实是用for循环实现的。在需要写一些底层代码的时候,还是需要写for循环的。来看这个例子:
Number.prototype[Symbol.iterator]=function*(){ for(leti=0;i<=this;i++){ yieldi; } }; [...6];//[0,1,2,3,4,5,6]注意,这个例子只是为了好玩。生产环境中不要直接修改JS内置数据类型的原型链。原因是V8引擎有一个原型链快速推测机制,修改原型链会破坏这个机制,造成性能问题。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。