如何在js代码中消灭for循环实例详解


Posted in Javascript onJuly 29, 2018

前言

这篇文章基于我在公司内部分享会整理而成。欢迎探讨补充。

补充一:看来很多人没看完文章就评论了。我在文章末尾说了,是不写 for 循环,不是不用 for 循环。简单陈述不写 for 循环的理由:for 循环易读性差,而且鼓励写指令式代码和执行副作用。更多参考这篇文章

补充二:回应大家的一些反对意见。本来准备专门写文章回应的,但是没时间,就简短回复,直接扔链接了。

1、for 循环性能最好。回应:微观层面的代码性能优化,不是你应该关注的。我在文章中演示了,对百万级数据的操作,reduce 只比 for 循环慢 8 ms,可忽略不计。如果你要操作更大的数据,要考虑下换语言了。

  • Fast code is NOT important
  • The Sad Tragedy of Micro-Optimization Theater
  • Ditching the Micro-Optimization Fetish

2、不用 for 循环不能 break。回应:用递归。我在这篇文章里有解释怎样解决递归爆栈。

3、框架都用 for 循环!回应:框架考虑的场景和你不一样。React 和 Vue 还用 class 来创建对象呢。你该跟着学吗?事实上你应该用工厂函数。Class vs Factory function: exploring the way forward

一,用好 filter,map,和其它 ES6 新增的高阶遍历函数

问题一:

将数组中的空值去除

const arrContainsEmptyVal = [3, 4, 5, 2, 3, undefined, null, 0, ""];

答案:

const compact = arr => arr.filter(Boolean);

问题二:

将数组中的 VIP 用户余额加 10

const VIPUsers = [
 { 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)
);

问题三:

判断字符串中是否含有元音字母

const randomStr = "hdjrwqpi";

答案:

const isVowel = char => ["a", "e", "o", "i", "u"].includes(char);
const containsVowel = str => [...str].some(isVowel);

 
containsVowel(randomStr);

问题四:

判断用户是否全部是成年人

const users = [
 { name: "Jim", age: 23 },
 { name: "Lily", age: 17 },
 { name: "Will", age: 25 }
];

答案:

users.every(user => user.age >= 18);

问题五:

找出上面用户中的未成年人

答案:

const findTeen = users => users.find(user => user.age < 18);

findTeen(users);

问题六:

将数组中重复项清除

const dupArr = [1, 2, 3, 3, 3, 3, 6, 7];

答案:

const uniq = arr => [...new Set(arr)];

uniq(dupArr);

问题七:

生成由随机整数组成的数组,数组长度和元素大小可自定义

答案:

const genNumArr = (length, limit) =>
 Array.from({ length }, _ => Math.floor(Math.random() * limit));

genNumArr(10, 100);

二,理解和熟练使用 reduce

问题八:

不借助原生高阶函数,定义 reduce

答案:

const reduce = (f, acc, arr) => {
 if (arr.length === 0) return acc;
 const [head, ...tail] = arr;
 return reduce(f, f(head, acc), tail);
};

问题九:

将多层数组转换成一层数组

const nestedArr = [1, 2, [3, 4, [5, 6]]];

答案:

const flatten = arr =>
 arr.reduce(
 (flat, next) => flat.concat(Array.isArray(next) ? flatten(next) : next),
 []
 );

问题十:

将下面数组转成对象,key/value 对应里层数组的两个值

const objLikeArr = [["name", "Jim"], ["age", 18], ["single", true]];

答案:

const fromPairs = pairs =>
 pairs.reduce((res, pair) => ((res[pair[0]] = pair[1]), res), {});

fromPairs(objLikeArr);

问题十一:

取出对象中的深层属性

const deepAttr = { a: { b: { c: 15 } } };

答案:

const pluckDeep = path => obj =>
 path.split(".").reduce((val, attr) => val[attr], obj);

pluckDeep("a.b.c")(deepAttr);

问题十二:

将用户中的男性和女性分别放到不同的数组里:

const users = [
 { name: "Adam", age: 30, sex: "male" },
 { name: "Helen", age: 27, sex: "female" },
 { name: "Amy", age: 25, sex: "female" },
 { name: "Anthony", age: 23, sex: "male" },
];

答案:

const partition = (arr, isValid) =>
 arr.reduce(
 ([pass, fail], elem) =>
 isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]],
 [[], []],
 );
 
const isMale = person => person.sex === "male";

const [maleUser, femaleUser] = partition(users, isMale);

问题十三:

reduce 的计算过程,在范畴论里面叫 catamorphism,即一种连接的变形。和它相反的变形叫 anamorphism。现在我们定义一个和 reduce 计算过程相反的函数 unfold(注:reduce 在 Haskell 里面叫 fold,对应 unfold)

const unfold = (f, seed) => {
 const go = (f, seed, acc) => {
 const res = f(seed);
 return res ? go(f, res[1], acc.concat(res[0])) : acc;
 };
 return go(f, seed, []);
};

根据这个 unfold 函数,定义一个 Python 里面的 range 函数。

答案:

const range = (min, max, step = 1) =>
 unfold(x => x < max && [x, x + step], min);

三,用递归代替循环

问题十四:

将两个数组每个元素一一对应相加

const num1 = [3, 4, 5, 6, 7];
const num2 = [43, 23, 5, 67, 87];

答案:

const zipWith = 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)];
};

const add = x => y => x + y;

zipWith(add)(num1)(num2);

问题十五:

将 Stark 家族成员提取出来。注意,目标数据在数组前面,使用 filter 方法遍历整个数组是浪费。

const houses = [
 "Eddard Stark",
 "Catelyn Stark",
 "Rickard Stark",
 "Brandon Stark",
 "Rob Stark",
 "Sansa Stark",
 "Arya Stark",
 "Bran Stark",
 "Rickon Stark",
 "Lyanna Stark",
 "Tywin Lannister",
 "Cersei Lannister",
 "Jaime Lannister",
 "Tyrion Lannister",
 "Joffrey Baratheon"
];

答案:

const takeWhile = f => ([head, ...tail]) =>
 f(head) ? [head, ...takeWhile(f)(tail)] : [];

const isStark = name => name.toLowerCase().includes("stark");

takeWhile(isStark)(houses);

四,使用高阶函数遍历数组时可能遇到的陷阱

问题十六:

从长度为 100 万的随机整数组成的数组中取出偶数,再把所有数字乘以 3

// 用我们刚刚定义的辅助函数来生成符合要求的数组
const bigArr = genNumArr(1e6, 100);

能运行的答案:

const isOdd = num => num % 2 === 0;
const triple = num => num * 3;

bigArr.filter(isOdd).map(triple);

注意,上面的解决方案将数组遍历了两次,无疑是浪费。如果写 for 循环,只用遍历一次:

const results = [];
for (let i = 0; i < bigArr.length; i++) {
 if (isOdd(bigArr[i])) {
 results.push(triple(bigArr[i]));
 }
}

在我的电脑上测试,先 filter 再 map 的方法耗时 105.024 ms,而采用 for 循环的方法耗时仅 25.598 ms!那是否说明遇到此类情况必须用 for 循环解决呢? No!

五,死磕到底,Transduce!

我们先用 reduce 来定义 filter 和 map,至于为什么这样做等下再解释。

const filter = (f, arr) =>
 arr.reduce((acc, val) => (f(val) && acc.push(val), acc), []);

const map = (f, arr) => arr.reduce((acc, val) => (acc.push(f(val)), acc), []);

重新定义的 filter 和 map 有共有的逻辑。我们把这部分共有的逻辑叫做 reducer。有了共有的逻辑后,我们可以进一步地抽象,把 reducer 抽离出来,然后传入 filter 和 map:

const filter = f => reducer => (acc, value) => {
 if (f(value)) return reducer(acc, value);
 return acc;
};

const map = f => reducer => (acc, value) => reducer(acc, f(value));

现在 filter 和 map 的函数 signature 一样,我们就可以进行函数组合(function composition)了。

const pushReducer = (acc, value) => (acc.push(value), acc);

bigNum.reduce(map(triple)(filter(isOdd)(pushReducer)), []);

但是这样嵌套写法易读性太差,很容易出错。我们可以写一个工具函数来辅助函数组合:

const pipe = (...fns) => (...args) => fns.reduce((fx, fy) => fy(fx), ...args);

然后我们就可以优雅地组合函数了:

bigNum.reduce(
 pipe(
 filter(isOdd),
 map(triple)
 )(pushReducer),
 []
);

经过测试(用 console.time()/console.timeEnd()),上面的写法耗时 33.898 ms,仅比 for 循环慢 8 ms。为了代码的易维护性和易读性,这点性能上的微小牺牲,我认为是可以接受的。

这种写法叫 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 家族成员名字遍历,每次遍历暂停一秒,然后将当前遍历的名字打印来,遍历完后回到第一个元素再重新开始,无限循环。

const starks = [
 "Eddard Stark",
 "Catelyn Stark",
 "Rickard Stark",
 "Brandon Stark",
 "Rob Stark",
 "Sansa Stark",
 "Arya Stark",
 "Bran Stark",
 "Rickon Stark",
 "Lyanna Stark"
];

答案:

function* repeatedArr(arr) {
 let i = 0;
 while (true) {
 yield arr[i++ % arr.length];
 }
}

const infiniteNameList = repeatedArr(starks);

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 循环的。来看这个例子:

Number.prototype[Symbol.iterator] = function*() {
 for (let i = 0; i <= this; i++) {
 yield i;
 }
};

[...6]; // [0, 1, 2, 3, 4, 5, 6]

注意,这个例子只是为了好玩。生产环境中不要直接修改 JS 内置数据类型的原型链。原因是 V8 引擎有一个原型链快速推测机制,修改原型链会破坏这个机制,造成性能问题。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
在Z-Blog中运行代码[html][/html](纯JS版)
Mar 25 Javascript
让getElementsByName适应IE和firefox的方法
Sep 24 Javascript
提高网站信任度的技巧
Oct 17 Javascript
JS获取后台Cookies值的小例子
Mar 04 Javascript
javascript给span标签赋值的方法
Nov 26 Javascript
jQuery拖拽排序插件制作拖拽排序效果(附源码下载)
Feb 23 Javascript
微信开发 JS-SDK 6.0.2 经常遇到问题总结
Dec 08 Javascript
微信小程序 转发功能的实现
Aug 04 Javascript
Node.js  REPL (交互式解释器)实例详解
Aug 06 Javascript
vue2.0 兄弟组件(平级)通讯的实现代码
Jan 15 Javascript
对VUE中的对象添加属性
Sep 18 Javascript
简单易扩展可控性强的Jquery转盘抽奖程序
Mar 16 jQuery
Vue-cli3项目配置Vue.config.js实战记录
Jul 29 #Javascript
vue权限路由实现的方法示例总结
Jul 29 #Javascript
JS高级技巧(简洁版)
Jul 29 #Javascript
js运算符的一些特殊用法
Jul 29 #Javascript
不得不知的ES6小技巧
Jul 28 #Javascript
JS 中可以提升幸福度的小技巧(可以识别更多另类写法)
Jul 28 #Javascript
Angular6笔记之封装http的示例代码
Jul 27 #Javascript
You might like
DC动画很好看?新作烂得令人发指,名叫《红色之子》
2020/04/09 欧美动漫
PHP 5.3.1 安装包 VC9 VC6不同版本的区别是什么
2010/07/04 PHP
Eclipse中php插件安装及Xdebug配置的使用详解
2013/04/25 PHP
PHP把小数转成整数3种方法
2014/06/30 PHP
[原创]CI(CodeIgniter)简单统计访问人数实现方法
2016/01/19 PHP
Laravel 5.4因特殊字段太长导致migrations报错的解决
2017/10/22 PHP
TP5框架model常见操作示例小结【增删改查、聚合、时间戳、软删除等】
2020/04/05 PHP
PHP 超级全局变量相关总结
2020/06/30 PHP
关于jquery.validate1.9.0前台验证的使用介绍
2013/04/26 Javascript
jquery实现滑动图片自己测试的例子
2013/11/05 Javascript
基于jquery扩展漂亮的下拉框可以二次修改
2013/11/19 Javascript
IE下双击checkbox反应延迟问题的解决方法
2014/03/27 Javascript
jquery实现搜索框常见效果的方法
2015/01/22 Javascript
JavaScript针对网页节点的增删改查用法实例
2015/02/02 Javascript
jQuery模拟360浏览器切屏效果幻灯片(附demo源码下载)
2016/01/29 Javascript
JS 清除字符串数组中,重复元素的实现方法
2016/05/24 Javascript
深入理解angularjs过滤器
2016/05/25 Javascript
JS实现点击链接切换显示隐藏内容的方法
2017/10/19 Javascript
微信小程序实现动态获取元素宽高的方法分析
2018/12/10 Javascript
layui使用数据表格实现购物车功能
2019/07/26 Javascript
微信小程序wxml列表渲染原理解析
2019/11/27 Javascript
vue3.0 上手体验
2020/09/21 Javascript
Python 编码Basic Auth使用方法简单实例
2017/05/25 Python
python实现学生管理系统
2018/01/11 Python
python制作图片缩略图
2019/04/30 Python
提升Python效率之使用循环机制代替递归函数
2019/07/23 Python
python selenium爬取斗鱼所有直播房间信息过程详解
2019/08/09 Python
Django数据结果集序列化并展示实现过程
2020/04/22 Python
基于Pyinstaller打包Python程序并压缩文件大小
2020/05/28 Python
python中的时区问题
2021/01/14 Python
澳大利亚领先的在线美容商店:Facial Co
2017/10/22 全球购物
美术专业学生个人自我评价
2013/09/19 职场文书
市级绿色学校申报材料
2014/08/25 职场文书
违反交通法规检讨书
2014/09/10 职场文书
银行授权委托书样本
2014/10/13 职场文书
十一月早安语录:把心放轻,人生就是一朵自在的云
2019/11/04 职场文书