详解如何在JS代码中消灭for循环


Posted in Javascript onDecember 11, 2019

Edit: 在我入职第三家公司的第一天,看到代码库里面一堆的 for 循环,内心有些崩溃,于是做了一次技术分享,展示怎样在代码中避免 for 循环。这篇文章是那次分享的总结。本文并不完美,其中递归的部分其实不应该在目前的生产环境中使用。但是我依然坚持认为 JS 引擎应该支持尾调优化,写尾递归和写循环性能没差别。

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

问题一:

将数组中的 falsy 值去除

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

答案:

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

问题二:

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

const users = [
 { 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:

import R from "ramda";

const add10IfVIP = R.ifElse(
  R.propEq("isVIP", true),
  R.evolve({ balance: R.add(10) }),
  R.identity
);

const updateUsers = R.map(add10IfVIP);
updateUsers(users);

用 Immer

如果你习惯写 mutable 的代码,可以试下 Immer,用 mutable 的风格写 immutable 的代码。

import produce from "immer";

const updatedUsers = produce(users, nextState => {
  nextState.forEach(user => {
  if (user.isVIP) {
    user.balance += 10;
    }
  });
});

问题三:

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

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);

三,用递归代替迭代(可以break!)

Edit: 如何解决递归爆栈,可以参考我的另一篇文章《不懂递归?读完这篇保证你懂》

问题十四:

将两个数组每个元素一一对应相加。注意,第二个数组比第一个多出两个,不要把第二个数组遍历完。

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

答案:

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);

问题十六:

找出数组中的奇数,然后取出前4个:

const numList = [1, 3, 11, 4, 2, 5, 6, 7];

答案:

const takeFirst = (limit, f, arr) => {
 if (limit === 0 || arr.length === 0) return [];
 const [head, ...tail] = arr;
 return f(head)
  ? [head, ...takeFirst(limit - 1, f, tail)]
  : takeFirst(limit, f, tail);
};

const isOdd = n => n % 2 === 1;

takeFirst(4, isOdd, numList);

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

问题十七:

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

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

能运行的答案:

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

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

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

const results = [];
for (let i = 0; i < bigArr.length; i++) {
 if (isEven(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(isEven)(pushReducer)), []);

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

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

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

bigNum.reduce(
 pipe(
  filter(isEven),
  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 相关文章推荐
JavaScript中对象property的删除方法介绍
Dec 30 Javascript
javascript使用appendChild追加节点实例
Jan 12 Javascript
jQuery实现文本框输入同步的方法
Jun 20 Javascript
jquery实现简单的表单验证
Nov 17 Javascript
JS与jQuery遍历Table所有单元格内容的方法
Dec 07 Javascript
第四章之BootStrap表单与图片
Apr 25 Javascript
详解在AngularJS的controller外部直接获取$scope
Jun 02 Javascript
node.js+captchapng+jsonwebtoken实现登录验证示例
Aug 17 Javascript
仿ElementUI实现一个Form表单的实现代码
Apr 23 Javascript
iview的table组件自带的过滤器实现
Jul 12 Javascript
js实现随机div颜色位置 类似满天星效果
Oct 24 Javascript
ES6字符串的扩展实例
Dec 21 Javascript
Angular封装表单控件及思想总结
Dec 11 #Javascript
小程序接口的promise化的实现方法
Dec 11 #Javascript
js中Function引用类型常见有用的方法和属性详解
Dec 11 #Javascript
jQuery实现验证用户登录
Dec 10 #jQuery
这样回答继承可能面试官更满意
Dec 10 #Javascript
jquery实现弹窗(系统提示框)效果
Dec 10 #jQuery
微信小程序 this.triggerEvent()的具体使用
Dec 10 #Javascript
You might like
php析构函数的简单使用说明
2015/08/24 PHP
PHP的mysqli_ssl_set()函数讲解
2019/01/23 PHP
php常用日期时间函数实例小结
2019/07/04 PHP
Laravel5.1 框架表单验证操作实例详解
2020/01/07 PHP
使javascript也能包含文件
2006/10/26 Javascript
JavaScript对内存分配及管理机制详细解析
2013/11/11 Javascript
设置jsf的选择框h:selectOneMenu为不可编辑状态的方法
2014/01/07 Javascript
多选列表框动态添加,移动,删除,全选等操作的简单实例
2014/01/13 Javascript
jquery 淡入淡出效果的简单实现
2014/02/07 Javascript
javascript生成随机大小写字母的方法
2014/02/20 Javascript
js在数组中删除重复的元素自保留一个(两种实现思路)
2014/08/22 Javascript
JavaScript中扩展Array contains方法实例
2020/08/23 Javascript
jQuery EasyUI实现右键菜单变灰不可用效果
2015/09/24 Javascript
常用的Javascript设计模式小结
2015/12/09 Javascript
AngularJS 实现JavaScript 动画效果详解
2016/09/08 Javascript
使用JS代码实现点击按钮下载文件
2016/11/12 Javascript
基于jQuery对象和DOM对象和字符串之间的转化实例
2017/08/08 jQuery
微信小程序实现倒计时60s获取验证码
2020/04/17 Javascript
bootstrap模态框嵌套、tabindex属性、去除阴影的示例代码
2017/10/17 Javascript
Node做中转服务器转发接口
2017/10/18 Javascript
vue实现同一个页面可以有多个router-view的方法
2018/09/20 Javascript
jQuery 选择器用法基础入门示例
2020/01/04 jQuery
Vue 401配合Vuex防止多次弹框的案例
2020/11/11 Javascript
[38:51]2014 DOTA2国际邀请赛中国区预选赛 Orenda VS LGD-CDEC
2014/05/22 DOTA
[01:36:17]DOTA2-DPC中国联赛 正赛 Ehome vs iG BO3 第一场 1月31日
2021/03/11 DOTA
Python编写检测数据库SA用户的方法
2014/07/11 Python
Python基于回溯法子集树模板实现8皇后问题
2017/09/01 Python
Python队列、进程间通信、线程案例
2019/10/25 Python
通过实例学习Python Excel操作
2020/01/06 Python
Python如何将装饰器定义为类
2020/07/30 Python
英国在线自行车商店:Evans Cycles
2016/09/26 全球购物
Carolina Lemke Berlin澳大利亚官网:时尚太阳镜品牌
2019/09/17 全球购物
介绍一下SOA和SOA的基本特征
2016/02/24 面试题
教师实习自我鉴定
2013/12/14 职场文书
开学典礼决心书
2014/03/11 职场文书
2015年高校就业工作总结
2015/05/04 职场文书