实例访问方法
不会改变数组本身的值,会返回新的值。
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
这两个大概就是includes
和indexOf
最大的区别吧。
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
覆盖了Object
的toString
方法,返回一个字符串,其中包含用逗号分隔的每个数组元素。当一个数组被作为文本值或者进行字符串连接操作时,将会自动调用其 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,Date
、Number
、Object
都存在这个 API。
toLocaleString
和toString
的主要区别就是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
indexOf
和lastIndexOf
是一对,其实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
再多的才智也无法阻挡愚蠢和庸碌的空虚——《瑞克与莫蒂》
flatMap
和flat
是一起出生的。你可以尝试能否从ECMAScript
神奇的API命名规则上猜测一下它的作用。
也许让你猜对了。flatMap
的作用就是flat
和map
这两个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] = x
、arr.length = i
和delete 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
不可以被像map
、filter
一样被链式调用,因为它的返回值是undefined
,而不是个数组。
5.除了抛出异常以外,没有办法中止或跳出 forEach()
循环。如果你需要中止或跳出循环,forEach()
方法不是应当使用的工具。最简单的办法是使用for
,或者every
、some
等元素。
forEach
和for
性能问题
在早期的浏览器中,forEach
的性能一直都不如for
的性能。所以导致大家的一个错误观点,即使是现在人们仍认为forEach
的性能不如for
,其实不然,得益于 V8 引擎的优化。如今在较新版的的浏览器或者 nodejs 里面,forEach
和for
的性能都是不相上下的,也许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
的唯一好处就是灵活性,break
和continue
的随时跳出。**但最大的优点同时也是最大的缺点。**功能强大就会导致出现一些难以阅读的复杂代码。比如下面这段代码。
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
方法每次调用时会返回一个包含value
和done
属性的对象,就这么简单。
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
语法也是依据iterator
的value
和done
进行循环。
稍微了解数据结构的同学会发现,这玩意不就是一个单向链表吗?还别说,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
。
every
和some
很像,其实every
和早期的几个迭代方法都很像,更恰当的说法是,早期的几个迭代方法都很像。every
和forEach
的区别有两点,第一点,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。
some
和every
很像,区别在于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]
filter
和map
是在 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 */
作用就是把数组转换成了一个存储了数组全部索引的迭代器。
keys
与Object.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
:生成新数组元素的函数,使用三个参数:
currentValue
:callback
数组中正在处理的当前元素。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
和map
、filter
等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
的逻辑,而是直接返回该元素。
那么照着这个思路,改造方法大体上是根据传入的参数foods
的length
来给定返回值,如果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,那么从上面的代码中,你应该看到了熟悉的东西。reducer
和initialState
。
是的,它们都是使用的同一种思想和原理。
你可以这么想,Redux中的reduce
并不是同步自动完成的,而是异步手动激活的。reducer
的第一个参数currentState
就是当前的基础状态。每次发动不同的action
时,会触发一次reduce
的调用。通过reducer
的第二个参数currentValue
和reducer
的逻辑来改变currentState
,currentValue
对应的是Redux中Action
传递过来的参数type
和payload
。
你可能有点听不明白,没关系,你早晚会明白的。
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 }
从上面的例子中可以看到,for
比reduce
多了一个变量来存储上一次计算的结果值。
reduce
其实也存在这么一个值,只不过得益于函数式编程的好处,它不会被你直接看到。可能现在你并不能感受到reduce
比for
有什么太大的优势。但是当你面对一份数百行的代码文件时,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
也可以实现forEach
、filter
等功能。
reduceRight
不要回避苦恼和困难,挺起身来向它挑战,进而克服它。——池田大作
作用:接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。
语法:arr.reduceRight(callback(accumulator, currentValue[, index[, array]])[, initialValue])
参数:
callback
:一个回调函数,用来操作数组中的每个元素,可接受四个参数:
accumulator
:上一次调用回调的返回值,或提供的initialValue。
currentValue
:当前被处理的元素。index
:可选,数组中当前被处理的元素的索引。array
:可选,调用reduceRight()
的数组。
initialValue
:可选,值用作回调的第一次调用的累加器。如果未提供初始值,则将使用并跳过数组中的最后一个元素。在没有初始值的空数组上调用reduce或reduceRight就会创建一个TypeError。
返回值:执行之后的返回值。
reduceRight
和reduce
是一对双胞胎,不同之处是从后面朝前迭代。可以类比indexOf
和lastIndexOf
。
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" */