详解如何在JS代码中消灭for循环
Edit:在我入职第三家公司的第一天,看到代码库里面一堆的for循环,内心有些崩溃,于是做了一次技术分享,展示怎样在代码中避免for循环。这篇文章是那次分享的总结。本文并不完美,其中递归的部分其实不应该在目前的生产环境中使用。但是我依然坚持认为JS引擎应该支持尾调优化,写尾递归和写循环性能没差别。
一,用好filter,map,和其它ES6新增的高阶遍历函数
问题一:
将数组中的falsy值去除
constarrContainsEmptyVal=[3,4,5,2,3,undefined,null,0,""];
答案:
constcompact=arr=>arr.filter(Boolean);
问题二:
将数组中的VIP用户余额加10
constusers=[ {username:"Kelly",isVIP:true,balance:20}, {username:"Tom",isVIP:false,balance:19}, {username:"Stephanie",isVIP:true,balance:30} ];
答案:
users.map( user=>(user.isVIP?{...user,balance:user.balance+10}:user) );
补充:经网友提醒,这个答案存在浅拷贝的问题。操作引用型数据确实是一个麻烦的问题。下面提供两个方案:
用Ramda:
importRfrom"ramda"; constadd10IfVIP=R.ifElse( R.propEq("isVIP",true), R.evolve({balance:R.add(10)}), R.identity ); constupdateUsers=R.map(add10IfVIP); updateUsers(users);
用Immer
如果你习惯写mutable的代码,可以试下Immer,用mutable的风格写immutable的代码。
importproducefrom"immer"; constupdatedUsers=produce(users,nextState=>{ nextState.forEach(user=>{ if(user.isVIP){ user.balance+=10; } }); });
问题三:
判断字符串中是否含有元音字母
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三,用递归代替迭代(可以break!)
Edit:如何解决递归爆栈,可以参考我的另一篇文章《不懂递归?读完这篇保证你懂》
问题十四:
将两个数组每个元素一一对应相加。注意,第二个数组比第一个多出两个,不要把第二个数组遍历完。
constnum1=[3,4,5,6,7]; constnum2=[43,23,5,67,87,3,6];答案:
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);问题十六:
找出数组中的奇数,然后取出前4个:
constnumList=[1,3,11,4,2,5,6,7];答案:
consttakeFirst=(limit,f,arr)=>{ if(limit===0||arr.length===0)return[]; const[head,...tail]=arr; returnf(head) ?[head,...takeFirst(limit-1,f,tail)] :takeFirst(limit,f,tail); }; constisOdd=n=>n%2===1; takeFirst(4,isOdd,numList);四,使用高阶函数遍历数组时可能遇到的陷阱
问题十七:
从长度为100万的随机整数组成的数组中取出偶数,再把所有数字乘以3
//用我们刚刚定义的辅助函数来生成符合要求的数组 constbigArr=genNumArr(1e6,100);能运行的答案:
constisEven=num=>num%2===0; consttriple=num=>num*3; bigArr.filter(isEven).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(isEven)(pushReducer)),[]);但是这样嵌套写法易读性太差,很容易出错。我们可以写一个工具函数来辅助函数组合:
constpipe=(...fns)=>(...args)=>fns.reduce((fx,fy)=>fy(fx),...args);然后我们就可以优雅地组合函数了:
bigNum.reduce( pipe( filter(isEven), 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引擎有一个原型链快速推测机制,修改原型链会破坏这个机制,造成性能问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。