学透JavaScript:你真的懂 Array 吗?(二)

简介: 学透JavaScript:你真的懂 Array 吗?

实例访问方法


不会改变数组本身的值,会返回新的值。


concat


大厦之成,非一木之材也;大海之润,非一流之归也。——《东周列国志》

作用:合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

语法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

参数:value*N*可选

将数组和/或值连接成新数组。如果省略了valueN参数参数,则concat会返回一个它所调用的已存在的数组的浅拷贝。

返回值:新 Array 实例。


const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result = arr1.concat(arr2, 7, "8", [9]);
console.log(result); // [1, 2, 3, 4, 5, 6, 7, "8", [9]]

concat主要需要注意 2 个点。第 1 点,可以连接所有东西,并不一定是数组,如上面的例子。

第 2 个点是concat的操作是浅拷贝,如下。


const arr1 = [1, 2, 3];
const obj = { k1: "v1" };
const arr2 = [[4]];
const result = arr1.concat(obj, arr2);
console.log(result); // [1, 2, 3,{ k1: "v1" }, [4]]
obj.k2 = "v2";
arr2[0].push(5);
console.log(result); // [1, 2, 3,{ k1: "v1", k2: "v2" }, [4, 5]]


includes


无息乌乎生,无绝乌乎续,无无乌乎有? ——宋应星《谈天·日说三》

作用:判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。

语法:arr.includes(valueToFind[, fromIndex])

参数:

valueToFind:可选,需要查找的元素值。如果不传递,直接返回false

fromIndex:可选,从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。

返回值:boolean 值,表示元素是否存在。


const arr1 = [1, 2, 3];
const result = arr1.includes(1);
console.log(result); // true
const result2 = arr1.includes(22);
console.log(result2); // false

注意:无法判断字面量的引用类型,只能判断基础类型和引用。


const t = [1];
const arr1 = [t, 2, 3];
const result = arr1.includes(t);
console.log(result); // true
const result2 = arr1.includes([1]);
console.log(result2); // false

includes是在 EMAScript6 中诞生的。早在 EMAScript5 中,大家喜欢用indexOf是否等于-1来判断某个元素是否存在于数组中。业界也曾经有过一个作用类似的第三方 API,叫做contains。那么既然是新出来的 API,而又在做和已存在 API 类似的事情,那么一定是有原因的,什么原因呢?

先来看一个常规操作。


const arr = [1, 2, 3];
console.log(arr.indexOf(2) !== -1); // true
console.log(arr.includes(2)); // true

好像没有问题?

那么再来看一个不一般的操作。


const arr = [NaN];
console.log(arr.indexOf(NaN) !== -1); // false
console.log(arr.includes(NaN)); // true

区别出来了!indexOf并不能正常匹配到NaN,因为在 ECMAScript 中,NaN === NaN的结果是false

再来看一个例子。


const arr = [, , , ,];
console.log(arr.indexOf(undefined) !== -1); // false
console.log(arr.includes(undefined)); // true

这两个大概就是includesindexOf最大的区别吧。


join


时人莫小池中水,浅处无妨有卧龙。——窦庠《醉中赠符载》

作用:将一个数组的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。

语法:arr.join([separator])

参数:

separator:可选,指定一个字符串来分隔数组的每个元素。如果需要,将分隔符转换为字符串。如果缺省该值,数组元素用逗号(,)分隔。如果separator是空字符串(""),则所有元素之间都没有任何字符。

返回值:一个所有数组元素连接的字符串。如果 arr.length 为 0,则返回空字符串。


const arr = [1, 2, 3];
const result = arr.join();
console.log(result); // "1,2,3"

join和字符串的split的作用几乎是相反的。

join的使用需要注意一点,它会将每个元素先调用toString再进行拼接。像空数组这种元素,转成字符串就是"",所以拼接起来毫无存在感可言。


let arr = ["h", 9, true, null, [], {}];
let result = arr.join("|");
console.log(result); // "h|9|true|||[object Object]"

当然你可以主动覆盖它的toString方法,这样结果就不一样了。


let arr = ["h", 9, true, null, [], {}];
[].__proto__.toString = function() {
  return "Array";
};
let result = arr.join("|");
console.log(result); // "h|9|true||Array|[object Object]"

而如果数组中存在可能转成String的元素,就会发生异常。比如Symbol


let arr = ["h", 9, true, null, Symbol(1)];
arr.join("|"); // TypeError: Cannot convert a Symbol value to a string


slice


敢于浪费哪怕一个钟头时间的人,说明他还不懂得珍惜生命的全部价值。——达尔文

作用:从数组中截取一段形成新的数组。接收 2 个参数,第 1 个是开始元素的下标,第二个是结束元素的下标(不包含这个元素)。

语法:arr.slice([begin[, end]])

参数:

begin:可选,默认为 0,提取起始处的索引。

end:可选,默认为数组length。提取终止处的索引。(包含begin,不包含end

返回值:一个含有被提取元素的新数组。


const arr1 = [1, 2, 3];
const result = arr1.slice(1, 2); // 从下标1的位置,截取到下标2
console.log(result); // [2]

特殊用法:

slice()可以浅拷贝数组。


const result = arr1.slice();


toSource


书到用时方恨少、事非经过不知难。——陆游

toSource不属于标准 API,仅属于Firefox浏览器独有,不建议使用,可以直接 pass 掉,看下面的toString

作用:返回一个字符串,代表该数组的源代码。

语法:array.toSource()


const array = [1, 2, 3];
array.toSource(); // "[1, 2, 3]"


toString


鲸落海底,哺暗界众生十五年。——加里·斯奈德

作用:返回一个字符串,表示指定的数组及其元素。

语法:arr.toString()

返回值:一个表示指定的数组及其元素的字符串。

toString覆盖了ObjecttoString方法,返回一个字符串,其中包含用逗号分隔的每个数组元素。当一个数组被作为文本值或者进行字符串连接操作时,将会自动调用其 toString 方法。

join 不传递参数时作用相同,如果要手动将数组转成字符串时,建议使用join,因为更加灵活。


const arr1 = [1, 2, 3];
const result = arr1.toString();
console.log(result); // "1,2,3"


toLocaleString


大鹏之动,非一羽之轻也;骐骥之速,非一足之力也。——《潜夫论·释难》

语法:arr.toLocaleString([locales[,options]]);

参数:

locales:可选,带有 BCP 47 语言标记的字符串或字符串数组。

options:可选,一个可配置属性的对象,对于数字 Number.prototype.toLocaleString(),对于日期Date.prototype.toLocaleString()

返回值:表示数组元素的字符串。

既然存在了toString,那么toLocaleString是为了解决什么问题呢?

除了Array具有这个 API,DateNumberObject都存在这个 API。

toLocaleStringtoString的主要区别就是toLocaleString的参数了,它可以将元素转化成哪个国家的人类语言。比如一百万这个数字。西班牙人的表示方式为1.000.000,英国人的表示方式为1,000,000。而日期也是如此,比如中国大陆用 年年年年/月月/日日 上午 or 下午 12 小时制时分秒,而国外大多数是 日日/月月/年年年年 24 小时制时分秒。


const arr1 = [1000000, new Date()];
const resultENGB = arr1.toLocaleString("en-GB");
const resultESES = arr1.toLocaleString("es-ES");
const resultAREG = arr1.toLocaleString("ar-EG");
const resultZHCN = arr1.toLocaleString("zh-CN");
console.log(resultENGB); // "1,000,000,29/12/2019, 23:40:39"
console.log(resultESES); // "1.000.000,29/12/2019 23:40:39"
console.log(resultAREG); // ١٬٠٠٠٬٠٠٠,٢٩‏/١٢‏/٢٠١٩ ١١:٤١:٣١ م
console.log(resultZHCN); // "1,000,000,2019/12/29 下午11:40:39"

从上面的例子中,可以总结出toLocaleString存在的根本目的是为了保证多个国家的用户浏览器来是符合各自习惯的。因为中国人完全看不懂阿拉伯的数字和日期,阿拉伯人同样也不容易看懂中国人的日期一样。

第 2 个参数非常强大,应用场景一般是展示货币,它可以自定义转换后的样式。


const arr1 = [1000000, new Date()];
const resultGBP = arr1.toLocaleString("en-GB", {
  style: "currency",
  currency: "GBP",
});
const resultCNY = arr1.toLocaleString("zh-CN", {
  style: "currency",
  currency: "CNY",
});
console.log(resultGBP); // "£1,000,000.00,29/12/2019, 23:51:18"
console.log(resultCNY); // "¥1,000,000.00,2019/12/29 下午11:51:18"

可以看到,设置了钞票代码后,就可以将数字转换成钞票的样式。需要注意一点,人民币的代码是CNY,而不是RMB。好吧,开个玩笑。

其实在调用 Array 的toLocaleString时,会自动调用每个元素的toLocaleString。但是前面说了,只有数组、日期、数字和对象存在这个 API,那么其它的类型没有这个 API 咋办呢?调用toString呗。


indexOf


有些鸟是注定不会被关在牢笼里的,它们的每一片羽毛都闪耀着自由的光辉。——《肖申克的救赎》

作用:返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

语法:arr.indexOf(searchElement[, fromIndex])

参数:

searchElement:要查找的元素。

fromIndex:可选,开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回-1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即-1 表示从最后一个元素开始查找,-2 表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,并不改变其查找顺序,查找顺序仍然是从前向后查询数组。如果抵消后的索引值仍小于 0,则整个数组都将会被查询。其默认值为 0。

在 ECMAScript5 时期,indexOf还一直在做一件和 includes 类似的事,前面也提到了,就是通过 indexOf 得到结果是否为-1 来判断数组中是否存在某个元素。后来为了职责单一,创造出了includes。所以indexOf目前的用途最主要的还是获取数组内第一个与参数匹配的某个下标。


const arr1 = [1, 2, 3];
const result = arr1.indexOf(3);
console.log(result); // 2
const result2 = arr1.indexOf(21);
console.log(result2); // -1

indexOf匹配元素时,只能正常匹配基本类型的元素。像引用类型,就必须要使用引用来匹配。


const arr1 = [];
const arr2 = [0, [], arr1];
console.log(arr2.indexOf([])); // -1
console.log(arr2.indexOf(arr1)); // 2

indexOflastIndexOf是一对,其实indexOf的名字应该叫作firstIndexOf


lastIndexOf


方向是比速度更重要的追求。——白岩松

作用:返回指定元素在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

参数:

searchElement:被查找的元素。

fromIndex:可选,从此位置开始逆向查找。默认为数组的长度减 1(arr.length - 1),即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。

返回值:数组中该元素最后一次出现的索引,如未找到返回-1。

和 indexOf 作用几乎相同,唯一的区别是从后面进行匹配。


const arr1 = [1, 2, 3, 2];
const result = arr1.lastIndexOf(2);
console.log(result); // 3
const result2 = arr1.indexOf(2);
console.log(result2); // 1


flat


即使我们生活在阴沟里,我们也要仰望星空。——电影《少年的你》影评

flat是2019年新加入的API,由于“把多维数组摊平”这个需求一直存在,但需求程度不是很高,所以并没有被官方特别重视,直到现在才出现了这个API。

在早期的JavaScript中,事实上,在2019年这个API没有开放之前,我们还在使用自己制作的摊平API来实现这个功能,具体可以看后面的内容,有具体的代码实现。

flat的作用很简单,就是把一个数组摊平而已。摊平到什么程度,由你而定。

比如这样一个数组:



         

摊开一层。


let result = arr.flat(1);
console.log(result);// [1, 2, 3, 4, [5, 6, [7, 8, 9]]]

摊开两层。


let result = arr.flat(2);
console.log(result);// [1, 2, 3, 4, 5, 6, [7, 8, 9]]

如果你不知道这是一个几维数组,而又想将它们全部摊开,那么就传入Infinity即可。


let result = arr.flat(Infinity);
console.log(result);// [1, 2, 3, 4, 5, 6, 7, 8, 9]

如果直接调用flat()而不传递任何参数,你认为效果应该是怎样的呢?是全部摊开吗?

那你就猜错了。

如果不传递任何参数,那么效果和传递1是一样的。


flatMap


再多的才智也无法阻挡愚蠢和庸碌的空虚——《瑞克与莫蒂》

flatMapflat是一起出生的。你可以尝试能否从ECMAScript神奇的API命名规则上猜测一下它的作用。

也许让你猜对了。flatMap的作用就是flatmap这两个API的结合体。

如果你想让一个数组中每个元素复制自身并且创造一个为自身2倍的值加入到下一个下标中。

你需要做两步,先使用map得到这些值,但它们的返回值变成了一个二维数组。

第二步自然就是把这个返回的数组摊平成一维数组了。


let arr = [1, 2, 3];
const result = arr.map((item) => [item, item * 2]).flat();
console.log(result);// [1, 2, 2, 4, 3, 6]

flatMap的作用是什么呢?就是把这个链式调用的API,合并成一个API。


let arr = [1, 2, 3];
const result = arr.flatMap((item) => [item, item * 2]);
console.log(result);// [1, 2, 2, 4, 3, 6]

看,多么无聊的API。真佩服ECMA那帮语言学的设计天才们。

这只是一句无足轻重的吐槽,请不要在意。

著名的作家Kevin Kelly在一次演讲中说过一段话:

关于技术,在最开始时,没有人知道新的发明最适合用于做什么,例如艾迪生的留声机,他原本不知道这能用来干什么。留声机慢慢被应用于两个场景:一是录下临终遗言;二是录下教堂里的讲话,包括唱歌。后来留声机主要用于录制音乐等。

但我们生活中不乏很多人有这种思想:世界不需要没有用的创新。

觉得地说,世界上不存在没有用的创新,所有创新都有它的用途。那些认为无用的创新无用的人,大概是没办法等待漫长的发掘创新用途的过程。其实:存在即合理,合理即存在。


实例迭代方法


对原始数组进行遍历。在遍历过程中,数组元素的操作不会受到影响。


forEach


千般荒凉,以此为梦;万般蹀躞,以此为归。——余秋雨

作用:对数组的每个元素执行一次提供的函数。

语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);

参数:

callback:为数组中每个元素执行的函数,该函数接收三个参数:

  • currentValue:数组中正在处理的当前元素。
  • index:可选,数组中正在处理的当前元素的索引。
  • array 可选,forEach() 方法正在操作的数组。

thisArg:可选,可选参数。当执行回调函数 callback 时,用作 this 的值。

返回值:undefined

虽然说forEach作为日常开发最为频繁的一个 API,但仍然有非常多的细节不被大家所熟知。导致很多同学在使用forEach时出现意料之外的现象发生,让人感到困惑。

下面千老师来分析一下forEach到底有哪些需要注意的细节。

forEach在第一次执行时就会确定执行范围。在forEach执行期间,人为改变数组元素会影响forEach的执行。在上面我们学到了,修改数组的方法一共有 9 种。

分别是添加类(push、unshfit、splice)、删除类(pop、shift、splice)、填充类(copyWithin、fill)、改变顺序类(reverse、sort)。除数组自身的方法以外,arr[i] = xarr.length = idelete array[i]这几种方式也会改变数组。

先看一个简单的例子。


const array = [0, 1, 2, 3, 4, 5];
array.forEach((currentValue, index, array) => {
  if (currentValue % 2 === 0) {
    array.push(1);
  }
  console.log(currentValue);
});
console.log(array);
/*
0
1
2
3
4
5
[0, 1, 2, 3, 4, 5, 1, 1, 1]
*/

可以看到,在forEach过程中,向数组新添加的数据,是不会被遍历到的。

但是如果在forEach的过程中修改数据,forEach则会读取遍历到它的那一刻的值。比如调整一下上面的那个例子。


const array = [0, 1, 2, 3, 4, 5];
array.forEach((currentValue, index, array) => {
  if (currentValue % 2 === 0) {
    array[index + 1]++;
  }
  console.log(currentValue);
});
console.log(array);
/*
0
2
3
3
4
6
[0, 2, 3, 3, 4, 6, NaN]
*/


注意事项


因为 API 太多,不再一一举例。这里简单归纳总结一下forEach的规律:

1.forEach执行开始时,就会确定执行次数。无论数组长度如何变化,都不会超过这个执行次数。但可能会低于这个次数。

2.forEach执行过程中,长度被改变。增长时没作用,减少时,到达数组最大长度后,就会结束(跳过)遍历。

3.forEach执行过程中,元素被改变。会读取遍历到该元素那一刻的值。

4.forEach不可以被像mapfilter一样被链式调用,因为它的返回值是undefined,而不是个数组。

5.除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具。最简单的办法是使用for,或者everysome等元素。

forEachfor

性能问题

在早期的浏览器中,forEach的性能一直都不如for的性能。所以导致大家的一个错误观点,即使是现在人们仍认为forEach的性能不如for,其实不然,得益于 V8 引擎的优化。如今在较新版的的浏览器或者 nodejs 里面,forEachfor的性能都是不相上下的,也许for会占据一点性能优势,但这个差距微乎其微。

为此千老师还特意做了一个实验,在 Chrome79 版本下, 长度为 100 万的数组的性能对比,我运行了 5 次:


let array = Array.from({ length: 1000000 }, (v, i) => {
  return i;
});
// for
console.time("log");
for (let i = 0; i < array.length; i++) {}
console.timeEnd("log");
// log: 17.89697265625ms
// log: 12.362060546875ms
// log: 18.535888671875ms
// log: 13.59326171875ms
// log: 13.08984375ms
// forEach
console.time("log");
array.forEach(function(val, i) {});
console.timeEnd("log");
// log: 16.1630859375ms
// log: 19.702392578125ms
// log: 18.179931640625ms
// log: 19.887939453125ms
// log: 20.77197265625ms

可以看到性能差距非常微小,甚至有时forEach的性能会胜过for

复杂性

for是典型的命令式编程产物。而forEach和其它的迭代方法一样,都属于函数式编程。for的唯一好处就是灵活性,breakcontinue的随时跳出。**但最大的优点同时也是最大的缺点。**功能强大就会导致出现一些难以阅读的复杂代码。比如下面这段代码。


for (var i = 0, len = grid.length, j = len - 1, p1, p2, sum; i < len; j = i++) {
  p1 = grid[i];
  p2 = grid[j];
  sum += p1 + p2;
}

forEach就不会出现这种情况,因为它屏蔽了for的配置条件。

最后千老师给出的建议就是,98%的情况下都应该优先使用forEach或者其它迭代方法,剩下 2%的情况应该是你在乎那一点点性能的情况,这时就需要你自己权衡了。


entries


内外相应,言行相称。——韩非

作用:返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对。

语法:arr.entries()

返回值:一个迭代器(interator)对象。

迭代器 interator

文章到这里,第一次出现interator这个名词。千老师相信很多同学直到这个概念,但更多的同学可能不知道。这里有必要讲明白Iterator是什么。以便更好地理解数组。

其实要讲Iterator可以再写一篇文章的,但千老师尽量控制。简明扼要的把这个概念讲明白就行了。

它是Iterable对象上的[Symbol.iterator]属性。准确地讲,Array 也属于iterable

在 ECMAScript6 之前,JavaScript 中的对象没有办法区分哪些对象能迭代,哪些对象不能迭代。我们总会说数组可以被迭代,但我们没办法说一些类数组对象也能迭代。虽然它们能够被我们通过一些手段迭代,比如Array.prototype.forEach.call()。这些都是对象,都是靠我们的习惯认定一个对象能不能被迭代,并没有规范来约束。这样子很奇怪。所以 ECMAScript6 推出了一个迭代协议,来解决这个问题。

所有具有[Symbol.iterator]属性的对象,都属于可迭代对象(Iterable)。通过调用可迭代对象上的[Symbol.iterator]方法就可以得到一个迭代器(iterator)。通过调用迭代器身上的next方法就可以实现迭代。


let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
while (!_done) {
  const { value, done } = iterator.next();
  if (!done) console.log(value);
  _done = done;
}
/**
"h"
"e"
"l"
"l"
"o"
" "
"W"
"o"
"r"
"l"
"d"
"!"
**/

为什么这个属性的名字这么奇怪呢?长得并不是像length这种属性一样。**要知道 JavaScript 中所有看起来奇怪的设计都是有原因的。**因为在 ECMAScript2015 之前是没有迭代器这个概念的。这属于新加入的概念,为了保证兼容性,不能随便在对象原型上添加iterator这么个属性,不然就会导致之前的 JavaScript 代码产生意想不到的问题。刚好 ECMAScript2016 引入了Symbol概念。使用Symbol可以很好的解决属性冲突问题。

我们可以利用Symbol.iterator属性来创建可迭代对象。


class Rand {
  [Symbol.iterator]() {
    let count = 0;
    return {
      next: () => ({
        value: count++,
        done: count > 5,
      }),
    };
  }
}
var rand = new Rand();
var iterator = rand[Symbol.iterator]();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 1, done: false}
// ..
iterator.next(); // {value: 5, done: false}
iterator.next(); // {value: undefined, done: true}

上面的代码虽然看上去花里胡哨,其实语法很简单。Symbol.iterator方法返回一个带有next方法的对象。而next方法每次调用时会返回一个包含valuedone属性的对象,就这么简单。

value表示当前迭代的值,done表示迭代是否结束。

注意:可迭代对象(iterable)和迭代器对象(iterator)不是一回事。唯一的联系就是可迭代对象上会包含一个Symbol.iterator的属性,它指向一个迭代器对象。

ECMAScript6 还增加了一种新语法,中文叫展开操作符(Spread syntax)。可迭代对象可以利用这种操作符将一个可迭代对象展开。


let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
console.log(...str);
/*
"h"
"e"
"l"
"l"
"o"
" "
"W"
"o"
"r"
"l"
"d"
"!"
*/

ECMAScript6 中新添加的for of语法也是依据iteratorvaluedone进行循环。

稍微了解数据结构的同学会发现,这玩意不就是一个单向链表吗?还别说,iterator就是个单向链表。

明白了迭代器的概念,我们回到entries上面继续研究。


var arr = [1, 2, 3];
var iterator = arr.entries();
iterator.next();
/*
{value: Array(2), done: false}
value: (2) [0, 1]
done: false
*/
iterator.next();
/*
{value: Array(2), done: false}
value: (2) [1, 2]
done: false
__proto__: Object
*/

entries会将数组转化成迭代器。这个迭代器中每个迭代出的值都是一个数组,[下标, 值]的形式。这样有什么用呢?其实千老师一时半会也想不到有什么用,但是当你应该用到它的时候,自然就知道它的应用场景是什么了。(如果你还能记住有这么个 API 的话。)


every


天下之事常成于困约,而败于奢靡。——陆游

作用:测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

语法:arr.every(callback[, thisArg])

参数:

callback:用来测试每个元素的函数,它可以接收三个参数:

  • element:用于测试的当前值。
  • index:可选,用于测试的当前值的索引。
  • array:可选,调用 every 的当前数组。

thisArg:执行 callback 时使用的 this 值。

返回值:如果回调函数的每一次返回都为 truthy 值,返回 true ,否则返回 false

everysome很像,其实every和早期的几个迭代方法都很像,更恰当的说法是,早期的几个迭代方法都很像。everyforEach的区别有两点,第一点,every的执行会在不满足条件时停止遍历。第二点,every有一个返回值。


const arr = [0, 1, 2, 30, 4, 5];
const result = arr.every(function(item, index) {
  console.log(index);
  /*
  0
    1
    2
    3
  */
  return item < 10;
});
console.log(result); // false


some


业精于勤,荒于嬉;行成于思,毁于随。——韩愈

作用:测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。

语法:arr.some(callback(element[, index[, array]])[, thisArg])

参数:

callback:用来测试每个元素的函数,接受三个参数:

  • element
    数组中正在处理的元素。
  • index 可选
    数组中正在处理的元素的索引值。
  • array可选
    some()被调用的数组。

thisArg:可选,执行 callback 时使用的 this 值。

返回值:数组中有至少一个元素通过回调函数的测试就会返回true;所有元素都没有通过回调函数的测试返回值才会为 false。

someevery很像,区别在于some会在碰到第一个符合条件的元素时停止遍历。所以这里也没什么好说的。把every的例子拿到这里就可以看到区别。


const arr = [0, 1, 2, 30, 4, 5];
const result = arr.some(function(item, index) {
  console.log(index);
  // 0
  // 1
  return item < 10;
});
console.log(result); // true


filter


物以类聚,人以群分。——《易经》

作用:创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

语法:var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

参数:callback:用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:

  • element:数组中当前正在处理的元素。
  • index:可选,正在处理的元素在数组中的索引。
  • array:可选,调用了 filter 的数组本身。

thisArg:可选,执行 callback 时,用于 this 的值。

返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

可以用一句话理解filter取走我们想要的东西。


const arr = [0, 1, 2, 3, 4, 5];
const result = arr.filter(function(item, index) {
  return item % 2 === 0;
});
console.log(result); // [0, 2, 4]

filtermap是在 ES6 中最早加入的api。在没有filter时,forEach同样可以实现filter功能。


const arr = [0, 1, 2, 3, 4, 5];
let result = [];
arr.forEach(function(item, index) {
  if (item % 2 === 0) {
    result.push(item);
  }
});
console.log(result); // true


find


勇气通往天堂,怯懦通往地狱。——塞内加

作用:返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

语法:arr.find(callback[, thisArg])

参数:

callback:在数组每一项上执行的函数,接收 3 个参数:

  • element:当前遍历到的元素。
  • index:可选,当前遍历到的索引。
  • array:可选:数组本身。

thisArg:可选,执行回调时用作this 的对象。

返回值:数组中第一个满足所提供测试函数的元素的值,否则返回 undefined


const arr = [0, 1, 20, 30, 40];
const result = arr.find((item) => item > 10);
console.log(result);// 20


findIndex


得之,我幸;不得,我命,如此而已。——徐志摩

作用:返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

语法:arr.findIndex(callback[, thisArg])

参数:

callback:针对数组中的每个元素, 都会执行该回调函数, 执行时会自动传入下面三个参数:

  • element:当前元素。
  • index:当前元素的索引。
  • array:调用findIndex的数组。

thisArg:可选。执行callback时作为this对象的值。

返回值:数组中通过提供测试函数的第一个元素的索引。否则返回-1。

findIndex返回的结果就是find返回元素的索引。


const arr = [0, 1, 20, 30, 40];
const result = arr.findIndex((item) => item > 10);
console.log(result);// 2


keys


知人者智,自知者明。胜人者有力,自胜者强。——老子

作用:返回一个包含数组中每个索引键的Array Iterator对象。

语法:arr.keys()

返回值:一个新的Array迭代器对象。


let arr = ['a', 'b', 'c'];
const result = arr.keys();
for(let key of result){
  console.log(key);  
}
/*
0
1
2
*/

作用就是把数组转换成了一个存储了数组全部索引的迭代器。

keysObject.keys不同的是,keys不会忽略empty元素。


let arr = ['a', ,'b', , 'c', ,];
const result = arr.keys();
for(let key of result){
  console.log(key);  
}
/*
0
1
2
3
4
5
*/


map


有则改之,无则加勉。——《论语》

作用:创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

语法:


var new_array = arr.map(function callback(currentValue[, index[, array]]) {
 // Return element for new_array 
}[, thisArg])

参数:

callback:生成新数组元素的函数,使用三个参数:

  • currentValuecallback数组中正在处理的当前元素。
  • index:可选,callback 数组中正在处理的当前元素的索引。
  • array:可选,map方法调用的数组。

thisArg:可选,执行 callback 函数时值被用作this

返回值:回调函数的结果组成了新数组的每一个元素。

map是ECMAScript2015中最为最古老的一批API,它的主要作用就是映射一个数组。它的功能使用forEach同样能够实现。

比如将一个数组所有元素翻倍。


let arr = [1, 2, 3, 4, 5, 6, 7];
const result = arr.map(function(item) {
    return item * 2;
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]

forEach同样能够实现,但比map稍微麻烦一点。


let arr = [1, 2, 3, 4, 5, 6, 7];
const result = [];
arr.forEach(function(item) {
    result.push(item * 2);
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]

既然两者都可以实现,虽然map更简洁。那么应该在什么情况下使用map呢?

1.是否需要返回一个新的数组。

2.是否需要从回掉函数中得到返回值。

满足任一条件都可以使用map,否则使用forEach或者for...of

最常见的用法是从对象数组中提取某些值。


let arr = [
  { name: "dog", age: 11 },
  { name: "cat", age: 4 },
  { name: "小明", age: 15 },
];
const result = arr.map(function(item) {return item.age});
console.log(result); // [11, 4, 15]

当你不知道是否应该使用map时,也不用纠结,因为map最大的意义就是可以简化一些代码。


reduce


人的一生是短的,但如果卑劣地过这一生,就太长了。——莎士比亚

作用:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

参数:

callback:执行数组中每个值 (如果没有提供 initialValue则第一个值除外)的函数,包含四个参数:

  • accumulator:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
  • currentValue:数组中正在处理的元素。
  • index:可选,数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
  • array:可选,调用reduce()的数组。

initialValue:可选,作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

返回值:函数累计处理的结果。


let arr = [1, 2, 3, 4, 5, 6];
const result = arr.reduce(function(acc, val) {
    return acc + val;
});
console.log(result);// 21

mapfilter等API相比,reduce好像有点与众不同。不同在哪里呢?

reduce的字面意思是“减少”,实际上,称呼它为组合更合适一些。

举个形象点的例子,健身计算蛋白质、脂肪、碳水化合物。

这是一个食物的营养表。


const nutritionFacts = {
  "🥚": {
    carbohydrate: 3,// 碳水化合物
    protein: 13,// 蛋白质
    fat: 9,// 脂肪
    calories: 144// 热量
  },
  "🍏": {
    carbohydrate: 12,
    protein: 0,
    fat: 0,
    calories: 52
  },
  "🍌": {
    carbohydrate: 21,
    protein: 1,
    fat: 0,
    calories: 91
  },
  "🍞": {
    carbohydrate: 58,
    protein: 8,
    fat: 5,
    calories: 312
  },
  "🥦": {
    carbohydrate: 3,
    protein: 4,
    fat: 1,
    calories: 33
  },
  "🥩": {
    carbohydrate: 2,
    protein: 20,
    fat: 4,
    calories: 125
  }
};

下面我们可以通过reduce来封装一个计算热量的函数。


const calculation = (foods) => {
  return foods.reduce((nutrition, food) => {
    return {
      carbohydrate:
        nutritionFacts[nutrition].carbohydrate +
        nutritionFacts[food].carbohydrate,
      protein: nutritionFacts[nutrition].protein + nutritionFacts[food].protein,
      fat: nutritionFacts[nutrition].fat + nutritionFacts[food].fat,
      calories:
        nutritionFacts[nutrition].calories + nutritionFacts[food].calories
    };
  });
};
const result = calculation(["🥩", "🥦"]);
console.log(result);
/*
{
  calories: 158,
  carbohydrate: 5,
  fat: 5,
  protein: 24
}
*/

你可以多尝试一下,然后就能从上面的代码中发现一个BUG。如果没有发现,请继续尝试。

这个BUG就是:如果你只吃了一块牛肉,那么它会把牛肉原封不动地返回给你。(这不符合事实,正确答案应该是💩)


const result = calculation(["🥩"]);
console.log(result);// "🥩"

那么该怎样修复呢?

首先要确定原因,是什么原因导致出现了这个现象?你可能发现了,如果reduce的调用者的length为1时,它不会去调用callback的逻辑,而是直接返回该元素。

那么照着这个思路,改造方法大体上是根据传入的参数foodslength来给定返回值,如果length是1的话,直接返回它对应的营养成分。可以写出以下代码:


if (foods.length === 1) {
  const { carbohydrate, protein, fat, calories } = nutritionFacts[foods[0]];
    return {
      carbohydrate,
      protein,
      fat,
      calories
    };
} else if (foods.length < 1) {
    // 原来的逻辑
}

但这样明显感觉到很笨拙,有没有更加睿智的做法呢?当然是有的,别忘了reduce还存在第二个参数。如果不传递第二个参数,第一个参数就要被作为初始状态。这种情况下,第一个值就要被跳过。不过一旦有了第二个参数,第一个值就不会被跳过。所以我们可以用第二个参数更加优雅的解决这个问题。


nutritionFacts["💧"] = {carbohydrate: 0, protein: 0, fat: 0, calories: 0};// 添加 💧 默认所有营养都为0
const calculation = (foods) => {
  return foods.reduce((nutrition, food) => {
    return {
      carbohydrate:
        nutritionFacts[nutrition].carbohydrate +
        nutritionFacts[food].carbohydrate,
      protein: nutritionFacts[nutrition].protein + nutritionFacts[food].protein,
      fat: nutritionFacts[nutrition].fat + nutritionFacts[food].fat,
      calories:
        nutritionFacts[nutrition].calories + nutritionFacts[food].calories
    };
  }, "💧");
};

聪明的同学发现了一个等式。


["🥩", "🥦"].reduce(reducer, initialState);
[initialState, "🥩", "🥦"].reduce(reducer);

上面这两种用法,效果是相等的。

如果你用过Redux或者Rxjs,那么从上面的代码中,你应该看到了熟悉的东西。reducerinitialState

是的,它们都是使用的同一种思想和原理。

你可以这么想,Redux中的reduce并不是同步自动完成的,而是异步手动激活的。reducer的第一个参数currentState就是当前的基础状态。每次发动不同的action时,会触发一次reduce的调用。通过reducer的第二个参数currentValuereducer的逻辑来改变currentStatecurrentValue对应的是Redux中Action传递过来的参数typepayload

你可能有点听不明白,没关系,你早晚会明白的。

for一样可以实现reduce。比如计算碳水化合物。


let foods = ["🥩", "🥦"];
let result = {};
for(let i = 0; i < foods.length; i++) {
    result.carbohydrate = (result.carbohydrate || 0) + nutritionFacts[foods[i]].carbohydrate;
}
console.log(result);// { carbohydrate: 5 }

从上面的例子中可以看到,forreduce多了一个变量来存储上一次计算的结果值。

reduce其实也存在这么一个值,只不过得益于函数式编程的好处,它不会被你直接看到。可能现在你并不能感受到reducefor有什么太大的优势。但是当你面对一份数百行的代码文件时,reduce自动替你维护这个变量的优势又很容易体现出来了。

还需要注意一个点,reduce从字面意思看是减少,实际上它并不是减少。因为它的回调函数中的第一个参数可以是任何值,比如数组或者对象。既然是数组或者是对象,那么就是一个可以无限扩展的数据结构。所以,千万不要被reduce的字面意思骗过去了。


let arr = [0, 1, 2, 3, 4, 5];
const result = arr.reduce((accumulator, currentValue) => {
  accumulator.push(currentValue * 2);
  return accumulator;
}, []);
console.log(result);// [0, 2, 4, 6, 8, 10]

看,reduce可以实现类似于map的功能。同样的,reduce也可以实现forEachfilter等功能。


reduceRight


不要回避苦恼和困难,挺起身来向它挑战,进而克服它。——池田大作

作用:接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。

语法:arr.reduceRight(callback(accumulator, currentValue[, index[, array]])[, initialValue])

参数:

callback:一个回调函数,用来操作数组中的每个元素,可接受四个参数:

  • accumulator:上一次调用回调的返回值,或提供的 initialValue。
  • currentValue:当前被处理的元素。
  • index:可选,数组中当前被处理的元素的索引。
  • array:可选,调用 reduceRight() 的数组。

initialValue:可选,值用作回调的第一次调用的累加器。如果未提供初始值,则将使用并跳过数组中的最后一个元素。在没有初始值的空数组上调用reduce或reduceRight就会创建一个TypeError。

返回值:执行之后的返回值。

reduceRightreduce是一对双胞胎,不同之处是从后面朝前迭代。可以类比indexOflastIndexOf


values


我和谁都不争,和谁争我都不屑。——兰德

作用:返回一个新的 Array Iterator 对象,该对象包含数组每个索引的值。

语法:arr.values()

返回值:一个新的 Array 迭代对象。


let arr = ['a', 'b', 'c'];
const iterator = arr.values();
for(let item of iterator){
  console.log(item);
}
/*
"a"
"b"
"c"
*/


Symbol.iterator


一切特立独行的人格都意味着强大——加缪

作用:@@iterator 属性和 Array.prototype.values() 属性的初始值是同一个函数对象。

语法:arr[Symbol.iterator]()

返回值:与values相同。

一般不建议用这个方法,直接用values就好了。


let arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
for(let item of iterator){
  console.log(item);
}
/*
"a"
"b"
"c"
*/



相关文章
|
1月前
|
存储 JavaScript 前端开发
JavaScript Array
【10月更文挑战第06天】
36 15
|
7天前
|
JavaScript 前端开发 开发者
|
1月前
|
存储 JavaScript 前端开发
JavaScript Array(数组) 对象
JavaScript Array(数组) 对象
25 3
|
3月前
|
JavaScript 算法 前端开发
JS算法必备之Array常用操作方法
这篇文章详细介绍了JavaScript中数组的创建、检测、转换、排序、操作方法以及迭代方法等,提供了数组操作的全面指南。
JS算法必备之Array常用操作方法
|
2月前
|
JavaScript 前端开发
JavaScript Array map() 方法
JavaScript Array map() 方法
|
1月前
|
数据采集 JavaScript 前端开发
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
|
2月前
|
存储 JavaScript 前端开发
JS篇(Array、Object)
JS篇(Array、Object)
17 1
|
3月前
|
JavaScript 前端开发 开发者
|
4月前
|
JavaScript API 索引
JS【详解】Set 集合 (含 Set 集合和 Array 数组的区别,Set 的 API,Set 与 Array 的性能对比,Set 的应用场景)
JS【详解】Set 集合 (含 Set 集合和 Array 数组的区别,Set 的 API,Set 与 Array 的性能对比,Set 的应用场景)
63 0
|
6月前
|
JavaScript 前端开发 索引
在JavaScript中,可以使用数组字面量或Array构造函数来创建一个数组对象
【4月更文挑战第16天】在JavaScript中,可以使用数组字面量或Array构造函数来创建一个数组对象
66 4
下一篇
无影云桌面