JS经常使用数组方法总结笔记

数组(Array)和对象(Object)几乎是不少程序语言中最经常使用的类型。在ECMAScript中,数组的长度能够动态变化,数组中的数据能够是不一样类型,相比其余语言更加灵活。另外,ECMAScript数组原生支持不少实用的方法,给数据的保存和处理带来很大的方便。javascript

因为数组是引用类型,须要注意方法的可变性,简单理解就是“是否会改变原数组”。这对于函数式编程尤为重要,由于可变方法可能会产生咱们调用它的目的以外的反作用,致使一些不可预知的结果,更容易形成bug且给bug的定位增长了难度。java

这里把数组的经常使用方法总结一下。因为是我的总结,若是有差错的地方还望你们及时指出。编程

tips:数组

  1. 示例代码能够直接打开浏览器console进行运行和实验;
  2. 为阅读方便,方法介绍时会用如sort(?compareFn)表示函数名和参数列表,参数前有?表明是可选参数。
  3. 查看浏览器中实现的全部数组方法,能够直接在console中执行console.dir(Array)console.log("%O", Array), 能看到Array上的静态方法和其prototype上的方法列表;

静态方法

相似于ES6中在class类中定义的static的方法,不会被实例继承,只能经过类自己来访问:浏览器

Array.isArray

这个方法用于检验传入值是不是数组,与instanceof相比,具备“跨iframe”的优势;由于一个浏览器中的多个window是不共享全局对象的,因此经过全局变量直接访问的Array也不必定指向同一个Array构造函数;而当执行value instanceof Array时,这里的Array不必定是建立value时所在全局对象下的Array, 可能会返回错误的信息。app

// 插入一个新iframe
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var a = new window.frames[0].Array(2); // 在iframe中建立一个长度为2的数组

// 两个window的Array是不一样的对象(构造函数)
a instanceof window.frames[0].Array // true
a instanceof window.Array // false

window.frames[0].Array.isArray(a); // true
window.Array.isArray(a) // true

// 由于不一样window下的全局属性是不一样的引用
window.frames[0].Array === window.Array // false
复制代码

Array.from, Array.of

这两个方法均可以用来建立新的数组实例函数式编程

  • Array.from(arrayLike, ?mapfn, ?thisArg)接收3个参数,第一个是类数组或可迭代对象,后两个是可选的回调函数mapFnmap的回调函数)和thisArg(设置this),至关于把可迭代对象转为数组后对其执行map(mapFn, thisArg); 可迭代对象中若是有“空元素”(empty), 会处理为undefined返回;
Array.from(new Array(3)); // [undefined, undefined, undefined]
Array.from("abcdefg", (s, i) => s + i); // ["a0", "b1", "c2", "d3", "e4", "f5", "g6"]
复制代码
  • Array.of(...arg)接收1个或多个参数,会把参数列表按顺序做为新数组的元素,并返回该新数组;
Array.of("a", 1, "b", 2); // ["a", 1, "b", 2]
复制代码

不可变(immutable)方法

不可变方法不会影响原数组,对返回的数组自己进行的操做也和原数组无关;但须要区别的一种状况是,因为元素复制时都是浅复制,新数组中引用类型值的元素与原数组元素引用的是同一个对象,修改会相互影响,同时影响其余引用该对象的全部变量。函数

map, filter, forEach

这三个方法属于ECMAScript定义的迭代方法,能够对数组中每一个元素执行必定操做后返回必定的结果;它们均可以接收两个参数(callbackfn, ?thisArg),第一个是要在每一个元素上执行的回调函数,第二个参数是可选的,即运行该函数的做用域对象(指定this值);须要注意的是若是使用箭头函数做为回调函数,this值是建立时绑定的,不会被第二个参数影响。post

// 第二个参数对this值的影响
[1, 2, 3].map(function (n) {
    return this.name;
}, {name: "Anne"})
// ["Anne", "Anne", "Anne"]

// 箭头函数回调, 建立时绑定了当前所在执行环境的this--> Window做为固定的this的值
[1, 2, 3].map(n => this, {name: "Anne"}) // [Window, Window, Window]
复制代码

传入的回调函数会接收到三个参数(item, index, array),即当前元素、当前索引和源数组;通常使用最多的是前两个。ui

  • map对每一个元素执行该回调后,将回调函数返回值组成新的数组返回,用于对元素成员转换或取值;
[1,2,3,4,5].map((n, i) => n + i); // [1, 3, 5, 7, 9]
复制代码
  • filter是将执行回调函数后返回的是truthy值的元素保留组成新数组返回,一般用于数组的过滤;
[1, 2, 3, 4, 5].filter((n, i) => n % 2 === 0); // [2, 4]
复制代码
  • forEach则只是对每一个元素执行回调函数的操做,没有返回值。
let a = [1, 2, 3, 4, 5];
let b = [];
a.forEach((n, i) => b[i] = n + i) // (返回)undefined
a // (未改变)[1, 2, 3, 4, 5]
b // [1, 3, 5, 7, 9]
复制代码

some, every

这两个方法也属于迭代方法,一样接收一个回调函数参数和一个可选的做用域对象参数,回调函数会接收(item, index, array)做为参数并须要返回布尔类型值,做为每一个元素是否符合条件的判断依据;

与上面方法不一样的是它们返回的是布尔值;从字面能够看出,some表明的是“只要有符合条件的元素就返回true”而every则是“全部元素都符合条件才返回true”.因此:

[1, 2, 3, 4, 5].some((n) => n % 2 === 0) // true
[1, 2, 3, 4, 5].every((n) => n % 2 === 0) // false
复制代码

它们不必定会遍历全部的元素,当some遇到符合条件的元素或every遇到不符合条件的元素它们就会中止遍历直接返回结果,由于后面的遍历再也不必要;

// 输出true以前 执行了3次
[1, 2, 3, 4, 5].some(n => {
    console.count("some");
    return n === 3;
});

// 输出false以前 执行了1次
[1, 2, 3, 4, 5].every(n => {
    console.count("every");
    return n === 3;
});

复制代码

reduce, reduceRight

我以为reduce函数值得是数组方法中被运用最多的方法之一(还有mapfilter)。初学JavaScript时我对它认知较浅,只有在遇到相似书中所举的“数组求和”问题时才会想到它。但如今认识到它其实比我想象的能作更多事(本质仍是同样的),我写在另外一篇总结里(扩展一下使用reduce的思路)。

reducereduceRight是ES5中增长的数组归并方法。reduce会从第一项到最后一项遍历数组全部元素,构建一个最终返回的值(取决于回调函数);reduceRightreduce同样,只是遍历方向相反,从最后一项开始到第一项进行归并操做。

它们接收两个参数(callbackfn, ?initialValue),第一个是在每一项上调用的回调函数,第二个是可选参数,用于设置初始值;例如书中的例子:

[1, 2, 3, 4, 5].reduce((prev, cur) => prev + cur); // (数组元素的和)15
复制代码

在每一项上调用的回调函数能够接收到四个参数,即(accumulator, currentValue, currentIndex, sourceArray);

  • accumulator: 可理解为累积器,每次执行回调函数后的返回值,传入下一项中做为此参数;在reduce初始值(第二个参数)没有设定时,执行时会默认把数组中第一个元素做为这个参数直接在第二个元素上执行;若是传入了初始值,则先在第一个元素上执行,初始值做为回调的该参数。
  • currentValue: 当前元素
  • currentIndex: 当前索引
  • sourceArray: 源数组

能够验证,没有设定初始值时,执行回调函数的次数比元素个数少一个;而设定初始值时执行次数与元素个数相同。由于有初始值时遍历从第一个元素开始。

[1, 2, 3, 4, 5].reduce((sum, cur) => {
    console.count("reduce-no-initail");
    return sum + cur;
});
// 输出结果 15 前,"reduce-no-initail"打印了4次

[1, 2, 3, 4, 5].reduce((sum, cur) => {
    console.count("reduce-initail");
    return sum + cur;
}, 0);
// 输出结果 15 前,"reduce-initail"打印了5次
复制代码

concat, slice

这两个方法不传入参数时都会简单浅复制已知数组并返回这个副本,全部也经常使用于复制数组或类数组/可迭代对象(经过Array.prototype.concat.call(someObj)[].concat.call(someObj)的方式).

  • concat(...items)用于对数组副本的拼接和合并,接收0或多个参数,不传入参数时会返回将原数组浅复制后的新数组;传入1个或多个参数时,会在浅复制一份原数组的基础上,将每一个参数(若是参数是数组则按序取出其中的元素,不然直接取该参数)做为元素按顺序拼接在其后;至关于直接将参数合并后执行了一次flat()再与原函数合并。
[[0], 1].concat(2, [3, 4], [[5, 6], 7]); // [[0], 1, 2, 3, 4, [5, 6], 7]
复制代码
  • slice(?start, ?end)用于返回数组的某一部分的副本,接收2个可选参数,表明起始索引和结束索引(左闭右开), 不传参数的状况与concat类似,返回将原数组浅复制的新数组;传入一个参数则默认从该参数位置到数组末尾; 传入的负值参数会取绝对值后从后向前数,例如-1会被解释为倒数第一个元素的位置(其余数组方法对表明索引的负数参数的处理都与此相同)。
[1,2,3,4].slice(2) // [3, 4]
[1,2,3,4].slice(2, -1) // [3]
复制代码

find, findIndex, indexOf, lastIndexOf, includes

这几个方法的类似之处都是执行对数组的查找操做;不一样之处在于:

  • find(predicate, ?thisArg)findIndex(predicate, ?thisArg)接收一个回调函数做为查找标准,该函数接收每一个迭代元素的(item, index, array)参数,一旦执行后返回值为truthy则视为找到该元素,find将会返回该元素(或其引用)而findIndex返回该元素的索引,并中止查找;它们还能够接收第二个可选参数用于绑定this所指的对象;
[
    {name: "a", val: 1},
    {name: "b", val: 2},
    {name: "c", val: 1}
].find(item => item.val === 1)
// {name: "a", val: 1}

function getVal2(o) {
    return o.val === this.val;
}

[
    {name: "a", val: 1},
    {name: "b", val: 2},
].find(getVal2, {name: "d", val: 2})
// {name: "b", val: 2}

复制代码
  • indexOf(searchElement, ?fromIndex)lastIndexOf(searchElement, ?fromIndex)接收的第一个参数是一个要查找的元素,并在迭代数组元素时使用===来判断是不是查找的元素,若是是则返回该元素的索引,若是最后都没有找到则返回-1; lastIndexOfindexOf同样,只是是从数组末尾开始向前查找;它们能够接收第二个参数,用于设定从哪一个位置开始查找;
[NaN, +0, -0].indexOf(NaN) // -1
[NaN, -0].indexOf(0) // 1
复制代码
  • includes(searchElement, ?fromIndex)的参数与indexOf类似,也是一个要查找的值,和一个可选的“起始位置”;但不一样的是这个方法在对比元素时使用sameValueZero的判断方式,即NaNNaN视为相等,其余与===判断相同。
[NaN, -0].includes(NaN) // true
[NaN, -0].includes(0) // true

复制代码

关于===和'sameValueZero'相等性判断

  • ===不进行类型转换,直接对比值,若是是引用类型值则对比其是否指向同一个对象;NaN不等于自身,0+0-0互相相等;

  • 'sameValueZero'判断时除了对NaN处理为其与自身相等,其余均与===同样;

  • 另外ES6中新增的Object.is(),则在sameValueZero的基础上,增长了对0的符号的限制,用它来判断时0等于+0, 但它们不等于-0

flat, flatMap

flat(?depth)用于铺平数组,能够接收一个参数设定要铺平的深度(层数), 默认为1,若是传入的深度值比数组自己的深度大,则与传入数组的最大深度效果相同, 数组被彻底铺平成为一维数组。

let a = [1, [2, [3, [4, 5], 6], 7], [8], 9];
a.flat() // [1, 2, [3, [4, 5], 6], 7, 8, 9]
a.flat(2) // [1, 2, 3, [4, 5], 6, 7, 8, 9]
a.flat(10) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
a.flat(Infinity) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码

flatMap(callback, ?thisArg)则是map方法与flat的方法的结合体,接收一个回调函数和一个可选的this参数,做为第一步map的参数对每一个元素执行并返回新的值,而后对构建的新数组进行展平1层。它与咱们本身调用map方法后再调用flat方法效果相同,但效率可能更高一点点。

比较常见的状况如从对象数组中取出某些值为数组的属性值,而后但愿变成一个一维数组方便执行其余操做,就能够用这个方法;

let b = [
    {memberIds: [1,2,3]},
    {memberIds: [4,5,6]},
    {memberIds: [7,8,9,10]}
];
// 获取b中全部memberId组成一个一维数组
b.flatMap(obj => obj.memberIds) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 和下面的方法结果同样
b.map(obj => obj.memberIds).flat() // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
复制代码

可变(mutable)方法

这里可变方法是指会改变数组自己的方法。并非说可变方法不能用,它们有时候可能很是有用。若是开发人员本身清楚使用它们的目的和结果并只在须要的时候使用,有利于提升代码的可维护性和健壮性。

fill, copyWithin, splice

这三个方法的类似性不是很高,但使用目的有必定的类似,即将数组中某些元素改变为咱们但愿的值,甚至插入/删除一些值。

  • fill(value, ?start, ?end)是对已有的数组填充,它的做用范围仅限于当前数组长度以内,不能改变数组长度。它接收三个参数,第一个是须要填充的值,第二个和第三个是可选的位置参数,设定填充的起始索引(含)和结束索引(不含),若是不传则分别默认为0和数组的length。对负数位置参数的处理与上面提到的相同;若是超出了数组自己,则转为与它们最近的有效值(0 或 array.length)。执行结束返回改变后的数组。例如:
[1,2,3,4].fill(5) // [5,5,5,5]
[1,2,3,4].fill(5, 2, 4) // [1,2,5,5]
[1,2,3,4].fill(5, -2, 4) // [1,2,5,5]
[1,2,3,4].fill(5, -3, 4) // [1,5,5,5]
[1,2,3,4].fill(5, -5, 4) // [5,5,5,5]
复制代码
  • copyWithin(target, start, ?end)在数组内部复制一部分值到另外一部分,也不能改变数组长度。它接收第一个参数是要放置复制元素的目标位置索引,第二个参数是要复制的部分的起始索引(含),结束索引(不含)即是可选的第三个参数,若是没有传入则默认为函数长度,即从起始一直复制到末尾。复制的部分将从目标位置开始填充,覆盖对应位置原有的元素。执行结束后返回改变后的数组。
[1,2,3,4].copyWithin(2, 0, 2) // [1,2,1,2]
[1,2,3,4].copyWithin(2, 0, 4) // [1,2,1,2]
[1,2,3,4].copyWithin(2, 4, 4) // [1,2,3,4] (没有复制到元素,也不会对原来的数组有影响)
复制代码
  • splice(start, ?deleteCount, ...items)几乎能够算这些方法中最强大的方法了。它能够对数组任意位置执行插入、删除、替换操做,也能够改变数组长度。就像对数组作手术,而具体会作什么样的手术(执行什么操做)则彻底由参数决定。它能够接收1个或多个参数,第一个设置起始位置,第二个为可选的“删除数量”,设置从起始位置(含)开始删除多少个元素,若是没有传入则默认将起始位置及其以后的元素所有删除。若是设置为0则不删除元素,并将其后的参数列表按顺序都从起始位置开始插入数组中。最后返回的是被删除的元素组成的数组。
let a = [1,2,3,4,5,6];
a.splice(4) // [5, 6] 删除了5,6

a // [1,2,3,4] 数组a自己长度改变

a.splice(2, 0, 7, 8, 9, 0); // [] 没有删除元素
a // [1,2,7,8,9,0,3,4] // 7,8,9,0被做为插入元素从索引2开始插入,数组原来的元素被放到插入元素列表的后面
复制代码

push, shift, pop, unshift

这几个方法都用于对数组的头部或尾部进行插入和删除。push(末尾推入)和pop(末尾删除)操做在数组末尾,像使用栈同样使用数组,遵循“后进先出”的规则;shift(头部删除)和unshift(头部推入)做用于数组头部,结合使用shiftpushunshiftpop能够从正向和反向模拟队列行为,像使用队列同样使用数组,遵循“先进先出”的规则。

  • push(...items)方法能够接收任意多个参数,把它们按序依次添加到数组末尾,返回改变后的数组的长度;
  • pop()方法不接收参数,每次执行都会从数组中删除掉最后一项,并返回这个元素;
  • unshift(...items)方法与push的方向相反,把接收到的任意多个参数放在数组头部,返回改变后的数组长度;
  • shift()方法也不接收参数,每次执行都会从数组头部删除掉第一项并返回这个元素;

这几个方法也能够应用在类数组对象上,或者有length属性和数值字符串属性的对象,它们会根据length属性肯定数组的末尾位置并访问对应位置,而与对象实际存在的元素个数或其余属性无关。

var a = [];
a.push(1,2,3) // 3 (添加元素后的数组长度)
a // [1,2,3]
a.pop() // 3 (删除的尾部元素)
a // [1,2]
a.unshift(5,6,7,8); // 6 (添加元素后的数组长度)
a // [5,6,7,8,1,2]
a.shift() // 5 (删除的头部元素)
a // [6,7,8,1,2]
复制代码

reverse, sort

这两个是数组的重排序方法;

  • reverse()直接按元素的位置进行反序操做,并返回改变后的数组;
[4,14,3,23].reverse() // [23,3,14,4]
[4,14,3,23].sort() // [14,23,3,4]
复制代码
  • sort(?compareFn)方法则是默认根据对比元素的字符串表示的前后顺序升序排列--即便每一个元素都是数值,也会先把它们转换为字符串,而后按照字符串的对比规则(对比它们的UTF-16字符编码值)肯定排序关系。若是数组中有undefined,则它们不参与排序并被放置在最后。

通常状况下咱们更多须要的是对一组数值或拥有数值类型属性的对象进行排序,直接调用sort是没法知足的,须要本身传入一个“比较函数”,接收两个值(a, b)做为参数并返回一个数值,若是返回负数则a排在b以前,若是返回正数则相反,若是返回是0, 则通常将这两个值保持原来的前后顺序一块儿与其余值按序排列。ECMAScript中没有保证对比时返回0的两个值必定保持前后顺序,因此并不是全部的浏览器都能保证作到这一点(引用)。

[4,14,3,23].sort() // [14,23,3,4] 按数值转换为字符串后的字符编码排序而非数值自己
[4,14,3,23].sort((a, b) => a - b) // [3,4,14,23] 按数值的大小进行升序排列

let a = ["x", "u", "m", "a"];
[undefined, ...a, "undefined"].sort() // ["a", "m", "u", "undefined", "x", undefined]

复制代码

可变方法的不可变替代

使用不可变方法复制数组

例如slice, concat, map, filter等,根据不一样须要选择代替;

// concat 代替 push / unshift
let a = [1,2,3];
a.push(4);
a // [1,2,3,4];

let b = a.concat(5);
b // [1,2,3,4,5]
a // [1,2,3,4]

// slice 代替 pop / shift
b.pop();
b // [1,2,3,4]
let c = b.slice(0, -1);
c // [1,2,3]
b // [1,2,3,4]
复制代码

使用扩展操做符复制数组

扩展运算符能够方便地对数组进行复制或部分复制,不会改变原数组;

let a = [1,2,3,4];
let b = [..a, 5];
b // [1,2,3,4,5]
a // [1,2,3,4]
复制代码

但须要注意的是,不管是不可变方法仍是扩展运算符,数组的复制都是浅复制,对于引用类型的元素复制的是其引用,而非整个对象。

注意:不可变方法隐蔽下的可变操做

还有一点值得注意,虽然不可变方法自己不会改变原数组,可是由于数组自己是引用类型的值,若是在回调函数中引用数组自己并对其元素进行改变操做或从新赋值,仍是会“隐蔽地”修改原数组。这种作法应该尽可能避免,由于在后续维护时可能会给别人带来没必要要的困扰(不知在哪里莫名其妙值就被改变了)。例如:

let c = [9, 8, 7, 6, 5];
c.map((n, i) => c[i] = i);
// 此时c已变成[0, 1, 2, 3, 4]
复制代码

当回调函数很是长的时候这种问题更难定位,其余引用c值的变量颇有可能也同时被影响。因此每一个函数最好都目的明确只作一件事,把确实须要改变原引用值的操做放在一个专门的函数中操做,而不是散布在任何看起来不会发生这种改变的地方。

参考

相关文章
相关标签/搜索