教你用好 Javascript 数组

原文链接:https://juejin.im/post/5d9769b26fb9a04df26c1b89

作为 Javascript 的标准对象之一,数组是非常底层而且实用的数据结构。虽然结构很简单,但是用好却不简单,包括我一开始学习 JS 的时候看到一堆原生方法也是很蒙蔽,怎么能有这么多方法。而且数组的各种方法各有其特点和使用场景,如果你还停留在 for 循环一把梭的阶段,也就是数组元素拼接,遍历等操作都是用 for 循环来完成的阶段,那么这篇文章非常适合你,或者你也可以推给你的坑逼同事︿( ̄︶ ̄)︿。

构造数组

字面量形式

一个原则:能用字面量构造的类型尽量用字面量构造。例如对象,数组,字符串等一票基本类型,[1, 2, 3] 比起 new Array(1, 2, 3) ,可读性和精简程度都好。数组的每个逗号后面都加个空格,想要成为优秀的程序员必须注重细节。

字面量结合扩展运算符

所谓扩展运算符就是三个点那个操作符:...,当我们构造一个新数组需要其它数组中的元素的时候,可以使用扩展运算符。

// 一个无副作用的 push
Array.prototype.purePush = function(...elements) { return [...this, ...elements]; }; console.log([1, 3, 1, 4].purePush(5, 2, 0)); // => [ 1, 3, 1, 4, 5, 2, 0 ]  复制代码

类数组转数组

ES5版

arguments, nodelist 等类数组转换成数组的方式有很多种,我第一次看到下面这种 ES5 类数组转数组的方式也是很懵逼。

function testArrayLikeToArray() {
  var args;

  console.log(arguments); // => { [Iterator]  0: 'a', 1: 'b', 2: 'c', [Symbol(Symbol.iterator)]: [λ: values] } 
  // 可以通过下标和访问,还可以访问 length
  console.log(arguments[0]); // => a
  console.log(arguments.length);
  // 返回 false 说明不是数组
  console.log(Array.isArray(arguments)); // => false

  args = Array.prototype.slice.call(arguments);
  console.log(args); // => [ 'a', 'b', 'c' ]
  console.log(Array.isArray(args)); // => true
}

testArrayLikeToArray('a', 'b', 'c');

主要是这行代码:

args = Array.prototype.slice.call(arguments);

Array.prototype 也可以直接用数组实例如空数组字面量 []来代替,只要能获取到数组原型上的 slice 就可以。上面的代码将 slice 函数的 this 指向 arguments 为什么就可以返回类数组对应的数组呢?

我没有研究过 slice 的具体实现,猜测是下面这样的:

Array.prototype.mySlice = function(start=0, end) {
    const array = this;
    const end = end === undefined ? array.length : end;
    
    const resultArray = [];
    if (array.length === 0) return resultArray;
    for (let index = start; index < end; index++) {
        resultArray.push(array[index]);
    }
    return resultArray;
}
扫描二维码关注公众号,回复: 7432019 查看本文章
 

我想 slice 内部实现可能就是会像我上面的代码一样只需要一个 length 属性,遍历元素返回新数组,所以调用 slice 时将其 this 指向类数组能正常工作。

使用 Array.from

args = Array.from(arguments);

使用扩展运算符
args = [...args];

Array.from 和扩展运算符的作用对象都可以是可迭代对象和类数组,所以类数组也可以正常被转换。建议使用后面两种 ES6 的方式,我们学习新知识就是拿来用的,如果不拿来用那就没意义了,不用我们也不容易记住。

对象转数组

在平时的开发中我们经常需要对对象的 keys,values,entries 操作,对应到 Object 的方法就是 Object.keys, Object.values, Object.entries。这 3 个 API 返回的都是数组而不是像 Map 返回 Iterator,我觉得原因是因为是因为对象一般键值对都是有限的,比较少,直接返回数组并不会占用多少内存,而 Map 不一样,map 一般是服务于大量键值对的,如果直接返回数组那样太浪费内存了,采用迭代器更合适,因为迭代器并不会产生数组,它遍历的是原可迭代对象。这里直接看几个案例就好了:

Object.keys()

// 判断对象是否为空
const isEmpty = (obj) => obj.keys().length !== 0;

Object.values()

// 字符串转数组
console.log(Object.values('abc')); // => [ 'a', 'b', 'c' ]

遍历对象

有人可能还在用下面这种方式遍历对象:

const me = {
  name: 'ly',
  age: 21,
};

me.__proto__.sex = 'man';

for (const key in me) {
  me.hasOwnProperty(key) && console.log(`${key}: ${me[key]}`);
}

// =>
// name: ly
// age: 21

那就太 low 了,建议使用下面这种方式:

const me = {
  name: 'ly',
  age: 21,
};

me.__proto__.sex = 'man';

// 命令式风格
for (const [key, value] of Object.entries(me)) {
  console.log(`${key}: ${value}`);
}

// 函数式风格
// Object.entries(me).forEach(([key, value]) => console.log(`${key}: ${value}`));

// =>
// name: ly
// age: 21

上面这种方式利用了 Object.entries 让我们可以获取键值对数组,在结合 for of 循环和数组解构让你直接在循环中访问 key 和 value。

Set 和 Map 转数组

set 可以看做 key 和 value 相同的 Map,Map 转换成数组还是利用 map.keys(), map.values(), map.entries(),前面也说过,Map 这三接口返回的是 iterator,而不是数组,而 Array.from 和 扩展运算可以又可以将 iterator 和类数组转换成数组,so,一般 Map 转换成数组就像下面这样:

const LANGUAGE_CODE_MAPPER = Object.freeze(
  new Map([['en', 'English'], ['zh', 'Chinese'], ['ja', 'Japanese'], ['fr', 'French']])
);

// React 渲染 options,当然这里扩展运算符也可以用 Array.from 代替
// 先获取 iterator,再将 iterator 转换成数组
const options = [...LANGUAGE_CODE_MAPPER.entries()].map(([code, language]) => <Option value={{key}}>{{language}}</Option>)

字符串转数组

方法有很多,上面已经讲过可以用 Object.values(),由于字符串也是 iterator,所以也可以使用 Array.from 和扩展运算符来转换。

console.log(Array.from('abcd'));
console.log([...'abcd']);
console.log('abcd'.split(''));
console.log([].slice.call('abcd'));
// 使用 Reflect.apply 改变 slice 函数指向
console.log(Reflect.apply([].slice, 'abcd', []));

// =>
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]

虽然写了很多,但是综合考虑就用 split 好了,兼容器,可读性,简洁性都不差。这里写了这么多种方式只是为了扩展下思路而已,事实上方式有很多,作为一个优秀的程序员,应该学会总结,并将总结的最实践应用于平时的编码之中。

构造元素重复的数组

可以使用 array.fill:

const arr = Array(6).fill(6);
console.log(arr); // => [ 6, 6, 6, 6, 6, 6 ]

构造连续整数的数组

Array.from 也是个高阶函数,可以接受一个函数来处理每个元素。

// length 设置为你需要获取的整数的个数,index + 1 这个 1 可以替换为你要设置的第一个数的大小
const continuousIntegers = Array.from({ length: 10 }, (__, index) => index + 1);
console.log(continuousIntegers); // => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] 

构造连续的字母

console.log(Array.from({ length: 27 }, (__, index) => String.fromCodePoint('a'.codePointAt(0) + index)));
// =>
// ['a'
// ...
// 'z']

new Array(length), Array(length), Array.of(length) 有啥区别?

new Array(6) 结果等于 Array(6),但是 Array.of(6) 是返回把 6 作为第一个元素的数组。

new Array(), Array(), Array.of

Array(n) 和 Array.from({ length: n }) 有啥区别?

控制台测试一下,一目了然,Array.from({ length: 4 }) 返回的是包含4个值为 undefined 的元素的数组,而 Array(4) 返回的是包含4个空元素的数组。

说到这你可能不明白空元素和undefined 有什么区别,空元素是无法被 forEach, map 等函数遍历到到的,所以我在构造可遍历的数组时是使用 Array.from(length: n) 而不是 Array(n),例如上面的构造连续整数和字母的示例,你换成 Array(length) 就不行。

array

数组方法

数组方法有很多,要想把知识点记得牢,除了多用多思考,学会分类也是很重要的。

分类

按照用途来分

  1. push/pop 操作数组尾部,其实也可以说是栈相关的方法
  2. shift/unshift 操作数组首部,他俩组合其实可以说将数组当成一个反过来的栈。
  3. splice 唯一可以删除插入元素的方法,当你要删除元素或者在哪个位置插入一个元素,找它准没错
  4. slice 切片操作,或者说取原数组的连续子集,虽然不如 python 那样方便还可以传一个 step 参数,但是一般够用
  5. indexOf/lastIndexOf /includes/find/findIndex 查找元素
  6. join 拼接成字符串的,少数和字符串操作相关的方法之一
  7. isArray 判断是否是个数组,面试经常问的一道非常非常非常基础的问题
  8. of/from 构造或者转换数组的
  9. reverse 翻转数组的,调用之后会改变原数组的函数不多,他也是一个
  10. sort 其实可以和 reverse 放一类,reverse 叫倒序,sort 排序
  11. keys/values/entries 和 Map 类似,返回的都是 Iterator
  12. flat 数组扁平化
  13. concat 拼接数组,因为效果是将数组参数打平后拼接,所以可以用来实现扁平化
  14. forEach/filter/map/reduce/reduceRight/some/every 用于集合变换的,后面单独聊聊

按照参数类型来分

  1. forEach/filter/map 等接受函数作为参数的高阶函数,这些方法大多数参数都是(value, index, self),即当前遍历的原素的值,索引,数组本身。
  2. 其它

这么多方法该如何选择?

这部分内容是写给初学者的,读者选择性阅读。

上面列了这么多,按照用途分类我都列了 14 类,要是像 JSON 那样就 stringify 和 parse 两个常用的方法那该多爽。我刚开始学习 js 数组的时候也很郁闷,怎么这么多的方法。其实没关系,方法多是好事,方法意味着方便,合适的场景选择合适的方法可以大大减少我们的代码量,可读性也会提高不少。

回到标题,这么多方法该如何选择呢?首先,我觉得作为一个初学者,你好歹把这些方法挨个看一边,知道它们是干嘛用的,它们的用法是怎么样的。我每次学习一门新语言的时候,都会花一到两周的时间去学习基础语法,基础必须扎实,你绝大数情况写的代码都是基础语法,其实我也觉得 js 语法很简单,我当初做笔记就有点烦,觉得写那些以前 java, python 就搞过的 map,slice 没啥意思,写个锤子,但是我还是认真的每个 API 都自己敲一遍,有些坑你自己不敲一遍你永远不会知道,你不敲一下 typeof null 你还真想不到这笔返回 'object'。

过一遍之后当然是忘得差不多了,没关系,有个印象就行,好像那个 reverse 函数是用来翻转数组的,但是到底是返回新数组还是直接改变原数组,这个忘了,有关系吗?没关系的其实,记住翻转就够了,至于会不会返回新数组,临时测试一下就好了。

然后就是多敲代码,多用。前面也说了,学习新语法你觉得有用的就一定要用上,比如你以前不知道可以用 forEach 这个高阶函数来遍历数组,你学了之后,觉得这个不错哦,那你就用上呗,下次敲代码,碰到一个场景你要遍历一个数组,就用 forEach,就不要去用 for 循环了。当然了可能有些情况下 forEach 并不适用比如你要提前 break,诶,你看,你这不就发现 forEach 的一个坑,不能提前退出,面试也经常会问 forEach 遍历和 for 循环有啥区别啊,这不就是一个区别喽,for 循环可以使用 break 提前退出。重要的是说三遍:

学了新知识,一定要用,

学了新知识,一定要用,

学了新知识,一定要用

我每次在一篇文章中 get 到新技能之后,比如之前看人家用正则配合 map 处理多条件分支,我在项目中碰到多分支的情况就会考虑用上。如果学了用不上咋办,忘了它,都用不上学它干嘛,比如学 Java web 现在还学 jsp,学 js 你去了解 with 语句有啥用啊?包括之前在知乎看到有人发了个贴说面试官问它原型链,其实我觉得问原型链除非人家简历写着熟悉或精通原生 js,你问人家原型链,那无可厚非,但是就因为人家原型链没搞明白就否认人家,认为人家技术不行,我觉得这是不应该的,你自己平时写业务代码需要用上原型链的机会多不多没B数吗?

其实上面说的还不是最关键的,最关键的是学会思考,学会总结,思考多了,总结多了,自然就能如鱼得水,从容应对。你在用那些方法的时候有没有像我在分类中描述的那样总结出那些方法各自的用途?如果你要在删除某个索引位置的元素或者在某个索引位置插入元素那肯定是用 splice 了,有没有总结过 forEach,map,filter,reduce, some, every 其实都可以用来遍历?

所以总的来说学习一个新知识比较好的方式是先脚踏实地的把基础弄懂,然后就是多用,用的过程中多思考,多中结。

数组实现栈和队列

虽然 Javascript 这门语言没有提供标准的数据结构类,但是其实到处都有数据结构的影子,对象其实本身就是 Map,对象还可以看做 Tree,队列和栈其实都可以用数组来实现,它们的区别只是行为上不一样而已。push,pop 可以用来实现栈:

const stack = [];

stack.push('a');
stack.push('b');

// 符合栈后进先出的特点
console.log(stack.pop()); // => b
console.log(stack.pop()); // => a

实现队列其实也很简单,使用 push 和 shift 即可:

const queue = [];

queue.push('a');
queue.push('b');

// 符合队列先进先出的特点
console.log(queue.shift()); // => a
console.log(queue.shift()); // => b

splice

const testArray = ['c', 'd', 'd'];
// 删除操作,第一个参数表示删除的起始下标, 第二个参数表示删除的个数
testArray.splice(1, 1);

// 第三个参数及其往后表示插入的元素,可以结合结构赋值使用
const ab = ['a', 'b'];
testArray.splice(0, 0, ...ab);
console.log(testArray); // => [ 'a', 'b', 'c', 'd' ] 

其实 Javascript 这门语言本身可以说早期是设计的非常糟糕了,你没有单独的 remove 和 insert 方法就算了,唯一一个可以用来插入删除的 API 还设计的这么反直觉。正常人的思维肯定是希望返回一个新数组,而不是修改原数组,假设设计者是基于性能考虑才不返回一个修改后的新数组,那么为何又要返回一个保存了删除了元素的数组。我如果需要删除的那几个元素我直接 slice 不好吗?返回一个数组很容易让人误解这是返回修改后新数组。

reverse

const arr = [1, 2, 3];
console.log(arr.reverse()); // => [ 3, 2, 1 ]
console.log(arr); // => [ 3, 2, 1 ]
// 居然返回原数组!!!
console.log(arr.reverse() === arr); // => true

reverse 和 splice 一样都被设计成有副作用的不纯函数。问题是为嘛要返回一个新数组,容易让人误解返回的是新数组,我觉得压根就不用返回新数组,你返回 undefined 那就等于告诉使用者这里是修改原数组,多好啊。

slice

有些人可能不知道 slice 其实是可以接受负数的:

const arr = ['a', 'b', 'c', 'd'];
console.log(arr.slice(-3)); // => [ 'b', 'c', 'd' ]  截取从最后一个数组下标开始
console.log(arr.slice(-3, -1)); // => [ 'b', 'c' ]

查找元素

查找元素的方法有很多,indexOf/lastIndexOf/includes/find/findIndex。经常看到有人写类似下面这样的代码:

const arr = ['a', 'b', 'c', 'd'];

const isExists = arr.indexOf('k') != -1;
if (isExists) {
  console.log('数组中存在 k');
}

如果你要判断数组中是否包含某个元素,明显 ES7 引入的 includes 函数更合适,可读性好多了。

const handleSelect = newValue => {
  if (this.state.includes(newValue)) {
    message.error(`您已经选择过${newValue},不能重复选择!`);
  }
};

lastIndexOf 在一些需要反向查找的情况下还是很有用的。其实 includes 叫查找元素还不太准确,find 才是真正的查找元素,find 返回的是元素本身,所以当你需要在数组中查找元素并且返回这个元素的时候你可以考虑使用 find。有 indexOf 不够吗为什么还有个 findIndex 呢?确实不够,有些场景下,使用函数式的高阶函数可以大大提高接口的灵活性。

const students = [{ Chinese: 120, English: 138 }, { Chinese: 140, English: 110 }, { Chinese: 108, English: 120 }];
const chineseHighest = students.find(stu => stu.Chinese === 140);
console.log(chineseHighest); // => { Chinese: 140, English: 110 } 

f

上面这个查找语文分数为 140 的学生用 indexOf 是不可能做的,使用高阶函数,让使用者传递一个函数去完成接口中的关键步骤这样的方式来扩展接口灵活性在 Javascript 到处可见,像 findIndex,sort 都是这样,所以我之前在面美团的时候被问到说函数式有什么用啊,这不就是吗?提高接口的灵活性。

至于啥时候用 findIndex, 那肯定是当你需要被查找元素的下标而不是元素本身喽。

join

数组的 join 方法其实挺常用的,来举几个案例:

获取重复的字符串

console.log(`${Array(35).fill('*').join('')} 我是美丽的分割线 ${Array(35).fill('*').join('')}`);
// => *********************************** 我是美丽的分割线 *********************************** 

将数组中的内容拼接成字符串

let url = 'http://xxx.com/xxx';

const params = {
  name: 'lyreal666',
  age: 21,
};

const query = [];
Object.keys(params).forEach(key => {
  query.push(`${key}=${params[key]}`);
});
url += `?${query.join('&')}`;

console.log(url);

我的同事写过类似上面的代码,其实有很多可以吐槽的地方。上面的代码目的是为了得到一个 queryString,其实最开始的数据时个对象,为什么最后就能将对象的内容拼接成字符串呢?这其实你要这样想:不要把对象只是当成一个对象,对象是可以转换成数组的,所以对象也可以其实也可以充分利用数组的那些接口,包括这里的 join。

其实我平时转换 queryString 我是使用标准对象 URLSearchParams

new URLSearchParams({ name: 'ly', age: 21 }).toString(); // => "name=ly&age=21"

一行代码解决,不使用标准对象也可以优雅的实现,其实可以 join 都不用,后面会提到。

sort

sort 这里补充一个坑:js 数组的 sort 默认是按照元素的字符串码值排序。当需要对数字数字排序需要自定义比较函数。

const arr = [111, 11, 8, 2, 7, 4, 3];
console.log(arr.sort()); // => [ 11, 2, 3, 4, 7, 8 ]
console.log(arr.sort((x, y) => x - y)); // => [ 2, 3, 4, 7, 8, 11 ]
flat

flat 是 ES2019 的语法,用于数组的扁平化,可以使用 reduce 和 concat 偏平到最里层的 flat。思路是这样的:

你要得到一个最终的结果 result 数组,你必须遍历每一个元素将其合并到结果数组,又考虑到遍历到的元素可能本身也是个数组,那就将其先扁平化再合并,也就是使用递归,扁平化数组元素后返回的是一个数组。哪个 API 可以将一个数组并将其合并到 result 数组呢?自然是 concat。那遍历收集每个数组元素得到一个和初始值类型相同的情况选择那个接口呢,当然是 reduce,一步到位就是下面的代码:

const flat = array => array.reduce((pre, current) => pre.concat(Array.isArray(current) ? flat(current) : current), []);

const testArray = [1, [2, [3, 4], 5, [6, 7], 8], 9];
console.log(flat(testArray)); // => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

forEach/filter/map/reduce/reduceRight/some/every

reduceRight 和 reduce 只是遍历的方向不同,所以后面不会单独讲 reduceRight。

forEach 和 for 循环有什么区别?

  1. forEach 提前退出循环,而 for 循环可以,直接 break 就可以。
  2. 当 forEach 接受的函数是 async 没法 await 的,也就是说每次执行的 async 函数之间是并发的,而 for 循环可以使用 await 让每次执行的 async 函数之间是同步的
  3. forEach 是函数式风格而 for 循环是命令式风格

关于上面第二点,我举个例子:

const sleep = seconds =>
  new Promise(resolve => {
    setTimeout(() => resolve(), seconds * 1000);
  });

const crawl = async url => {
  console.log(`start crawl url: ${url}...`);
  await sleep(3);
  console.log(`crawl url: ${url} end...`);
};

const urls = ['http://a.com', 'http://b.com', 'http://c.com', 'http://d.com', 'http://e.com'];
// forEach
console.time('Test forEach');
urls.forEach(crawl);
console.timeEnd('Test forEach');

// for loop
(async function testForLoop() {
  console.time('Test for loop');
  for (const url of urls) await crawl(url);
  console.timeEnd('Test for loop');
})();


// =>
// start crawl url: http://a.com...
// start crawl url: http://b.com...
// start crawl url: http://c.com...
// start crawl url: http://d.com...
// start crawl url: http://e.com...
// Test forEach: 2.837ms
// start crawl url: http://a.com...
// crawl url: http://a.com end...
// crawl url: http://b.com end...
// crawl url: http://c.com end...
// crawl url: http://d.com end...
// crawl url: http://e.com end...
// crawl url: http://a.com end...
// start crawl url: http://b.com...
// crawl url: http://b.com end...
// start crawl url: http://c.com...
// crawl url: http://c.com end...
// start crawl url: http://d.com...
// crawl url: http://d.com end...
// start crawl url: http://e.com...
// crawl url: http://e.com end...
// Test for loop: 15012.782ms

上面列举的那些高阶函数之间有什么区别?

都可以用来遍历,区别在于返回值和用途不同。

  1. forEach 无返回值,常用于存粹的遍历,大多数情况可以替代 for 循环使用
  2. filter 返回原数组的子集,用于对原数组的进行过滤筛选
  3. map 返回的和原数组长度相同的映射数组,用于映射到另外一个数组
  4. reduce/reduceRight 返回一个和初始值类型相同的值,中文叫归并操作,用于遍历数组每个元素收集需要的数据返回一个值
  5. some/every 返回值都是 boolean 类型。只有 some 和 every 可以提前退出遍历,用于判断数组中有元素满足或者所有元素是否满足某条件。

filter/map 返回的都是新数组,并不会修改原数组,除非你在遍历过程中修改了原数组的元素,非常不建议在遍历操作中修改原数组,除非你清楚的知道修改的后果是什么。

什么时候用 forEach 什么时候用 Map?

当然是看你需不需要返回值啦,forEach 就是纯粹的遍历,Map 是用来映射原数组的。

reduce 有什么用?

来看几个案例:

还是上面那个获取 queryString 的,我用 reduce 来改写一下,思考过程是这样的:我最终要得到一个字符串,这个字符串是遍历所有的元素得到的,显然不能用 map,map 是返回数组, reduce 可以啊,reduce 是扫描一遍每一个元素合成一个值啊。

let url = 'http://xxx.com/xxx';

const params = {
  name: 'lyreal666',
  age: 21,
};

// 使用 map 来实现
// url += '?' + Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');

// 使用 reduce
url += Object.entries(params).reduce(
  (pre, [key, value]) => `${pre}${pre === '?' ? '' : '&'}${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
  '?'
);
console.log(url); // => http://xxx.com/xxx?name=lyreal666&age=21

连乘或连加

const add = (...operands) => operands.reduce((pre, current) => pre + current, 0);
console.log(add(2, 3, 3)); // => 8

const pow = (x, y = 2) => Array.from({ length: y }).reduce(pre => pre * x, 1);
console.log(pow(3)); // => 9
console.log(pow(2, 3)); // => 8

再来看看 redux 中的 compose

const compose = (...funcs) => {
  if (funcs.length === 0) {
    return args => args;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

const a = arg => {
  console.log(`call a, arg: ${arg}`);
};

const b = arg => {
  console.log(`call b, arg: ${arg}`);
  return 'a';
};
const c = arg => {
  console.log(`call c, arg: ${arg}`);
  return 'b';
};

const composedFunc = compose(
  a
  b,
  c
);

// 即执行效果为 arrgs => a(b(c(args)))
composedFunc('c');

// => 
// call c, arg: c
// call b, arg: b
// call a, arg: a

所以 reduce 使用的时候一般就是当你需要通过遍历一个数组计算出一个值得时候。

some/every 该怎么用?

这俩和短路或,短路与很像。当你需要判断数组中的是否有一个或多个元素满足条件时考虑使用 some,只要一个元素满足就会退出遍历。当你需要判断数组中的元素是都都满足某个条件时使用 every,只要有一个元素不满足就会退出。

some 和 every其实是可以相互转换的,你想啊,数组中有一个或多个瞒住条件 condition 是不是数组中所有元素都满足 !condition 的结果取反。

看一个例子:

// 小明的分数
const grades = [80, 59, 80];

// 完蛋,要被老爸打屁屁
const isOver = grades.some((grade) => grade < 60);
// 等同于
// const isOver = !grades.every((grade) => grade >= 60);

链式调用

在使用这几个高阶函数的时候我们经常是链式调用的,例如:

// level 为 0 表示管理员
const users = [
  { name: 'ly', level: 2 },
  { name: 'Bob', level: 1 },
  { name: 'Lily', level: 0 },
  { name: 'Tom', level: 3 },
];

const customerNames = users
  .filter(user => user.level !== 0)
  .map(customer => customer.name)
  .join(', ');

console.log(customerNames); // => ly, Bob, Tom

比起 for 循环不但节省代码,逻辑还更清晰,而且因为处理过程是分步的,出错的时候我们只需要关注出错的那步调用,如果是 for 循环的话代码往往是糅杂在一起的,分析起来涉及面比较广。看看 for 循环版本的:

// level 为 0 表示管理员
const users = [
  { name: 'ly', level: 2 },
  { name: 'Bob', level: 1 },
  { name: 'Lily', level: 0 },
  { name: 'Tom', level: 3 },
];

let str = '';
for (const user of users) {
  if (user.level !== 0) {
    str += `${str === '' ? '' : ', '}${user.name}`;
  }
}
console.log(str); // => ly, Bob, Tom

这篇文章算是我对 Javascript 数组的一些总结和经验之谈,可能后续还会补充一些内容。希望能给读者一些启发,尤其是其中我提到的一些学习方法,希望对一些初学者能够起到一些指导作用。

如果文章内容有什么错误或者不当之处,欢迎在评论区指出。感谢您的阅读,如果文章对您有所帮助或者启发,不妨点个赞,关注一下呗。

本文为原创内容,首发于个人博客,转载请注明出处。

猜你喜欢

转载自www.cnblogs.com/tommymarc/p/11627933.html