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


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

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

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

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

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


  • 用好 filter,map, 和其它 ES6 新增的高阶遍历函数
  • 理解和熟练使用 reduce
  • 用递归代替循环 (可以 break!)
  • 使用高阶函数遍历数组时可能遇到的陷阱
  • 死磕到底, Transduce!
  • for 循环和 for/of 循环的区别
  • 放弃倔强, 实在需要用 for 循环了

Edit: 在我入职上一家公司的第一天, 看到代码库里面一堆的 for 循环, 内心有些崩溃, 于是做了一次技术分享, 展示怎样在代码中避免 for 循环. 这篇文章是那次分享的总结. 至于为什么我提倡避免 for 循环, 参考我写的这篇文章. 本文并不完美, 其中递归的部分其实不应该在生产环境中用的. 重点其实应该是怎样用 reduce 和其它高阶函数, 至于这些高阶函数底层用的是 while 循环还是 for 循环, 都不重要, 我们可以不在乎这些细节. 在实际写代码的时候, 只要被允许, 我都会尽量使用 Ramda. 如果对 Ramda 感兴趣, 可参考我的另一篇文章优雅代码指北 – 巧用 Ramda

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

问题一: 将数组中的虚值去除

  • 原始数据
const a = [1, false, -0, 0n, 0, NaN, undefined, null, '', '', ``];
  • 期待结果
[ 1 ]

虚值

Array.prototype.filter()

解析
const compact = (arr) => arr.filter(Boolean);
console.log(compact(a));

问题二: 将数组中的 VIP 用户余额加 10

  • 原始数据
const users = [
  { username: 'Kelly', isVIP: true, balance: 20 },
  { username: 'Tom', isVIP: false, balance: 19 },
  { username: 'Stephanie', isVIP: true, balance: 30 },
];
  • 期待结果
[
  { username: 'Kelly', isVIP: true, balance: 30 },
  { username: 'Tom', isVIP: false, balance: 19 },
  { username: 'Stephanie', isVIP: true, balance: 40 }
]

Array.prototype.map()

解析
const newUers = (users) =>
  users.map((user) => (user.isVIP ? { ...user, balance: user.balance + 10 } : user));
console.log(newUers(users));

能用原生解决的最好

补充: 经网友提醒, 这个解析存在浅拷贝的问题(已修复). 操作引用型数据确实是一个麻烦的问题. 下面提供两个方案:

  1. 用 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);
  1. 用 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';
  • 期待结果
true

Array.prototype.includes()

Array.prototype.some()

解析
const isVowel = (char) => ['a', 'e', 'o', 'i', 'u'].includes(char);
const containsVowel = (str) => [...str].some(isVowel);
console.log(containsVowel(randomStr));

RegExp.prototype.test()

String.prototype.search()

String.prototype.match()

正则表达式方案
const regex1 = RegExp('a|e|o|i|u', 'gi');
const regex2 = /a|e|o|i|u/gi;

// regex1 与 regex2 等价
console.log(regex1.test(randomStr));
console.log(regex2.test(randomStr));

console.log(!!~randomStr.search(regex1));
console.log(!!~randomStr.search(regex2));

console.log(!!randomStr.match(regex1));
console.log(!!randomStr.match(regex2));

问题四: 判断用户是否全部是成年人

  • 原始数据
const users = [
  { name: 'Jim', age: 23 },
  { name: 'Lily', age: 17 },
  { name: 'Will', age: 25 },
];
  • 期待结果
false

Array.prototype.every()

解析
const isAdult = (user) => user.age >= 18;
console.log(users.every(isAdult));

问题五: 找出上面用户中的第一个未成年人

  • 原始数据
const users = [
  { name: 'Jim', age: 23 },
  { name: 'Lily', age: 17 },
  { name: 'Will', age: 25 },
];
  • 期待结果
{ name: 'Lily', age: 17 }

Array.prototype.find()

解析
const findTeen = (users) => users.find((user) => user.age < 18);
console.log(findTeen(users));

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

解析
const genNumArr = (length, limit) =>
  Array.from({ length }, (_) => Math.floor(Math.random() * limit));
console.log(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((acc, val) => acc.concat(Array.isArray(val) ? flatten(val) : val), []);
console.log(flatten(nestedArr));
// to enable deep level flatten use recursion with reduce and concat
const flatDeep = (arr, d = 1) => {
  return d > 0
    ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
    : arr.slice();
};

console.log(flatDeep(nestedArr, Infinity));
// [1, 2, 3, 4, 5, 6]

reduce + concat + isArray + recursivity

console.log(nestedArr.flat(2));

Array.prototype.flat()


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

  • 原始数据
const objLikeArr = [
  ['name', 'Jim'],
  ['age', 18],
  ['single', true],
];
  • 期待结果
{ name: 'Jim', age: 18, single: true }
解析
const fromPairs = (pairs) => pairs.reduce((res, pair) => ((res[pair[0]] = pair[1]), res), {});
console.log(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' },
];
  • 期待结果
[
  { name: 'Adam', age: 30, sex: 'male' },
  { name: 'Anthony', age: 23, sex: 'male' }
]
[
  { name: 'Helen', age: 27, sex: 'female' },
  { name: 'Amy', age: 25, sex: 'female' }
]
解析
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);

console.log(maleUser);
console.log(femaleUser);

问题十三: 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');
console.log(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;
console.log(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]));
  }
}

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

五, 死磕到底, Transduce!

我们先用 reduce 来定义 filtermap, 至于为什么这样做等下再解释.

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), []);

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

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

现在 filtermap 的函数 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 引擎有一个原型链快速推测机制, 修改原型链会破坏这个机制, 造成性能问题.


答疑

认为是 “强行函数式” 的诸位, 实际上你们是被 for 毒害了. 这才是数据转换本该有的样子.

arr.map((v) => v.id);

这段代码的意思是 “生成一个对应原数组中每个对象 id 属性的新数组”
这是声明式的代码.

var n = [];
for (let i = 0; i < arr.length; i++) {
  n.push(arr[i].id);
}

这段代码的意思:”新建一个数组 n , 和整数 i , 当 i 小于 arr.length 的时候, 把原数组第 n 个元素的 id 属性 添加到 n 中”.
这是命令式的代码.

两者可读性的差异是高下立判的.

说可读性不好的, 只不过是对 map/foreach/reduce 感到陌生罢了. 但这些并不是什么多么复杂的东西, 只是对数组通用操作的抽象而已. 即便 “底层都是 for”,”也没有提升性能”, 但这也是很有必要的. 将过程抽象成函数, 将过程式的代码变成函数之间的组合, 对于程序而言会有着更好的可读性 / 可维护性 / 易于测试并行等等.

计算机越是底层的操作, 越是命令式的. 而人类的思维却不适合繁杂的操作步骤. 程序员的工作重点便是将过程逐步进行各种形式的抽象, 我们所用的程序语言 / 范式 / 设计模式 / 各种业务函数, 无不如此. 很多人不理解这一点, 认为写代码无非是照着计算机的方式一步一步来罢了, 于是这些人成了 “面条代码” 的制造者.


Using map/reduce to reformat objects in an array

  • 原始数据

The following code takes an array of objects and creates a new array containing the newly reformatted objects.

const kvArray = [
  { key: 1, value: 10 },
  { key: 2, value: 20 },
  { key: 3, value: 30 },
];
  • 期待结果
[
  { key: 1, value: 10 },
  { key: 2, value: 20 },
  { key: 3, value: 30 },
];
解决方案一
const productsById = (arr, key, value) =>
  arr.reduce((rObj, v) => {
    let Obj = {};
    Obj[v[key]] = v[value];
    rObj.push(Obj);
    return rObj;
  }, []);
console.log(productsById(kvArray, 'key', 'value'));
const productsById = (arr, key, value) =>
  arr.reduce((rObj, v) => ((rObj[v[key]] = v[value]), rObj), {});
console.log(productsById(kvArray, 'key', 'value'));
//{ '1': 10, '2': 20, '3': 30 }
解决方案二
const productsById = (arr, key, value) =>
  arr.map((v) => {
    let rObj = {};
    rObj[v[key]] = v[value];
    return rObj;
  });
console.log(productsById(kvArray, 'key', 'value'));
const productsById = (arr, key, value) =>
  arr.map((v) => {
    let rObj = {};
    return (rObj[v[key]] = v[value]), rObj;
  });
console.log(productsById(kvArray, 'key', 'value'));

合并具有 1 个相同属性名称的对象数组

将对象数组或某一属性值相同的对象的另一属性值进行累加

  • 原始数据
const data = [
  { pid: 'water', num: 1000 },
  { pid: 'oil', num: 2000 },
  { pid: 'water', num: 100 },
  { pid: 'oil', num: 200 },
  { pid: 'water', num: 10 },
  { pid: 'oil', num: 20 },
];
  • 期待结果
[
  { pid: 'water', num: 1110 },
  { pid: 'oil', num: 2220 },
];
解决方案一
const count = (data) => {
  const resultObj = data.reduce((result, item) => {
    result[item.pid] = (result[item.pid] || 0) + (Number(item.num) || 0);
    return result;
  }, {});
  const result = Object.getOwnPropertyNames(resultObj).map((pid) => {
    return { pid, num: resultObj[pid] };
  });
  return result;
};

console.log(count(data));
解决方案二
const count1 = (arr) => {
  let res = [];
  let tmp = {};
  arr.map((v) => {
    if (!tmp.hasOwnProperty(v.pid)) {
      tmp[v.pid] = res.length;
      return res.push(Object.assign({}, v));
    }
    res[tmp[v.pid]].num += v.num;
  });
  return res;
};
console.log(count1(data));

const count2 = (arr) => {
  let res = [];
  let tmp = {};
  arr.map((v) => {
    if (!tmp.hasOwnProperty(v.pid)) {
      tmp[v.pid] = res.length;
      res.push(Object.assign({}, v));
    } else res[tmp[v.pid]].num += v.num;
  });
  return res;
};
console.log(count2(data));

const count3 = (data) => {
  let res = [];
  let tmp = {};
  data.forEach((v) => {
    if (!tmp.hasOwnProperty(v.pid)) {
      tmp[v.pid] = res.length;
      return res.push(Object.assign({}, v));
    }
    res[tmp[v.pid]].num += v.num;
  });

  return res;
};
console.log(count3(data));

const count4 = (data) => {
  let res = [];
  let tmp = {};

  data.forEach((v) => {
    if (!tmp.hasOwnProperty(v.pid)) {
      tmp[v.pid] = res.length;
      return res.push(Object.assign({}, v));
    } else res[tmp[v.pid]].num += v.num;
  });

  return res;
};
console.log(count4(data));

逻辑或 (||) 两个竖线符号 ( || ) 表示”或”运算,参与运算的任意一个参数为 true, 返回的结果就为 true, 否则返回 false. 或”运算串联多个值,将返回第一个真值,如果所有的值都是假值,就返回该链的最后一个值。

逗号表达式 对它的每个操作数求值(从左到右), 并返回最后一个操作数的值。

解决方案三
const count = (data) =>
  Object.values(
    data.reduce(
      (acc, { pid, num }) => (((acc[pid] = acc[pid] || { pid, num: 0 })['num'] += num), acc),
      {},
    ),
  );

console.log(count(data));

如何将具有相同属性的对象合并到单个数组中?

  • 原始数据
const data = [
  { id: '1', name: 'a' },
  { id: '1', name: 'b' },
  { id: '1', name: 'c' },
  { id: '2', name: 'a' },
  { id: '2', name: 'b' },
  { id: '3', name: 'a' },
];
  • 期待结果
[
  { id: '1', name: ['a', 'b', 'c'] },
  { id: '2', name: ['a', 'b'] },
  { id: '3', name: ['a'] },
];
解决方案一
const anotherArray = data.reduce((acc, { id, name }) => {
  const existing = acc.find((a) => a.id == id);
  if (existing) existing['name'].push(name);
  else acc.push({ id, name: [name] });
  return acc;
}, []);

console.log(anotherArray);
解决方案二
const anotherArray = (data) =>
  Object.values(
    data.reduce(
      (acc, { id, name }) => ((acc[id] = acc[id] || { id, name: [] })['name'].push(name), acc),
      {},
    ),
  );

console.log(anotherArray(data));

如何在两个对象数组中合并具有相同键的属性?

  • 原始数据
const people = [
    { id: '001', name: 'David', age: 29 },
    { id: '002', name: 'Lucia', age: 41 },
    { id: '003', name: 'Steve', age: 18 },
  ],
  address = [
    { id: '001', city: 'Barcelona' },
    { id: '002', city: 'Paris' },
    {},
    { id: '003', city: 'Tokyo' },
    { id: '004', city: 'Barcelona' },
  ],
  • 期待结果
[
  { id: '001', name: 'David', age: 29, city: 'Barcelona' },
  { id: '002', name: 'Lucia', age: 41, city: 'Paris' },
  { id: '003', name: 'Steve', age: 18, city: 'Tokyo' },
];
解决方案
const result = (people, address) => {
  const map = new Map(address.map((o) => [o.id, o]));
  const result = people.map((o) => Object.assign({}, o, map.get(o.id)));
  return result;
};

console.log(result(people, address));

Grouping objects by a property

  • 原始数据
const people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 },
];
  • 期待结果
{
  '20': [ { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ],
  '21': [ { name: 'Alice', age: 21 } ]
}
解决方案一
const groupBy = (objectArray, property) => {
  return objectArray.reduce((acc, obj) => {
    let key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
};

console.log(groupBy(people, 'age'));
解决方案二
const groupBy = (arr = [], key) =>
  key ? arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {}) : {};

console.log(groupBy(people, 'age'));

Counting instances of values in an object

  • 原始数据
const names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
  • 期待结果
{ 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
解决方案
const countedNames = (arr) =>
  arr.reduce(
    (allNames, name) => (name in allNames ? allNames[name]++ : (allNames[name] = 1), allNames),
    {},
  );
console.log(countedNames(names));

Convert an array to an object

We can use .reduce() to convert an array to a POJO. This can be handy if you need to do lookups of some sort. For example, imagine if we had a list of people:

我们可以使用 .reduce() 将数组转换为 POJO.如果您需要进行某种形式的查找,这将很方便.例如,假设我们有一个人员列表:

  • 原始数据
const peopleArr = [
  {
    username: 'glestrade',
    displayname: 'Inspector Lestrade',
    email: 'glestrade@met.police.uk',
    authHash: 'bdbf9920f42242defd9a7f76451f4f1d',
    lastSeen: '2019-05-13T11:07:22+00:00',
  },
  {
    username: 'mholmes',
    displayname: 'Mycroft Holmes',
    email: 'mholmes@gov.uk',
    authHash: 'b4d04ad5c4c6483cfea030ff4e7c70bc',
    lastSeen: '2019-05-10T11:21:36+00:00',
  },
  {
    username: 'iadler',
    displayname: 'Irene Adler',
    email: null,
    authHash: '319d55944f13760af0a07bf24bd1de28',
    lastSeen: '2019-05-17T11:12:12+00:00',
  },
];
  • 期待结果
{
  glestrade: {
    username: 'glestrade',
    displayname: 'Inspector Lestrade',
    email: 'glestrade@met.police.uk',
    authHash: 'bdbf9920f42242defd9a7f76451f4f1d',
    lastSeen: '2019-05-13T11:07:22+00:00'
  },
  mholmes: {
    username: 'mholmes',
    displayname: 'Mycroft Holmes',
    email: 'mholmes@gov.uk',
    authHash: 'b4d04ad5c4c6483cfea030ff4e7c70bc',
    lastSeen: '2019-05-10T11:21:36+00:00'
  },
  iadler: {
    username: 'iadler',
    displayname: 'Irene Adler',
    email: null,
    authHash: '319d55944f13760af0a07bf24bd1de28',
    lastSeen: '2019-05-17T11:12:12+00:00'
  }
}

In some circumstances, it might be convenient to look up user details by their username. To make that easier, we can convert our array to an object. It might look something like this:

在某些情况下,通过用户名查找用户详细信息可能会很方便.为了简化操作,我们可以将数组转换为对象.它可能看起来像这样:

解决方案
const keyByUsernameReducer = (acc, person) => ({ ...acc, [person.username]: person });
const peopleObj = peopleArr.reduce(keyByUsernameReducer, {});
console.log(peopleObj);

文章作者: leihuang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 leihuang !
  目录