深入 JavaScript 数组

Clloz · · 58次浏览 ·

前言

本文整理一些我觉得有用或者没用的(说不定哪天就有用了 :stuck_out_tongue:JavaScript 数组技巧。本文前半部分是对 ArrayAPI 进行比较深入的解读,后半部分是对一些具体场景的解决方案。本文较长,请耐心阅读。

创建数组

JavaScript 的数组创建大致三种方式:字面量,new Array(el1, el2, el3...)new Array(length)new 可以省略。

new Array() 有两种初始化可能,即 new Array(el1, el2, el3...)new Array(length),出现第二种情况当且仅当只有一个参数,并且该参数是 1 – (2^{32} – 1) 之间的整数,其他情况都会把传入的参数当做生成的数组的元素。

实际上, JavaScript 并没有常规的数组,所有的数组其实就是个对象,只不过会自动管理一些”数字”属性和 length 属性罢了。说的更直接一点, JavaScript 中的数组根本没有索引,因为索引应该是数字,而 JavaScript 中数组的索引其实是字符串。arr[1]其实就是 arr["1"],给arr["1000"] = 1arr.length 也会自动变为 1001。这些表现的根本原因就是,JavaScript 中的对象就是字符串到任意值的键值对。注意键只能是字符串。不过目前 ES6 中已经有了类似于 Java 等语言的 Map 类型,键可以是任意类型的值。

稀疏数组和密集数组

Java 和C语言中,数组是一片连续的存储空间,有着固定的长度。即数组元素之间是紧密相连的,不存在空隙,这就是密集数组。在 JavaScript 中是支持稀疏数组的,比如我们用 new Array(length) 创建的就是一个稀疏数组。

我们熟知的 ArrayAPI 都会对这些空位有自己的处理:

  • flat(),flatMap() 会移除空位。
  • forEach(), filter(), reduce(), every() 和some()都会跳过空位。
  • map() 会跳过空位,但会保留这个值,即保留这个空位。
  • join()toString() 会将空位视为 undefined,而 undefinednull 会被处理成空字符串。
  • Array.from 将数组的空位转为 undefined
  • 将空位转换为 undefined
  • copyWithin 将空位一起拷贝
  • fill() 将空位视为正常的数组位置
  • for…of 循环会遍历空位
  • entries()keys()includes()values()find()findIndex() 将空位处理成 undefined

如果你遍历数组希望跳过数组中的未赋值空位,可以使用 in 操作符,

let a = []
a[1] = 'clloz'
console.log(a) //[ <1 empty item>, 'clloz' ]
console.log(0 in a) //false
console.log(1 in a) //true

创建密集数组的方法:

//利用 apply 将数组作为多个参数传入
Array.apply(null, Array(3)) //[undefined, undefined, undefined]

//生成一个元素值等于下标的数组
Array.apply(null, Array(3)).map(Function.prototype.call.bind(Number)); // [0, 1, 2]
//上面这行代码等同于下面的代码
[undefined,undefined,undefined].map((value, index) => Number.call(value, index)) //index

Array.from({length:3}); //Array.from() 可以作用于拥有一个 length 属性和若干索引属性的任意对象

Array.apply(null,{length:3}) //这种用法突出了JavaScript中的数组其实就是个对象,只要有 `length` 属性,并且都是数字索引属性即使数组

new Array(...Array(3)) //扩展运算符和 apply 类似的效果

Array.apply 实际上是利用了 apply 将数组作为多个参数传递的特性来生成密集数组,Array.call(null, Array(3)) 实际上等于 Array(arr[0], arr[1], arr[2])(设 Array(3) 生成的数组是 arr)。

Array API 深入

我在 JavaScript常用内置对象API 一文整理了一些 JavaScript 内置对象的 API,主要是当做工具表,在使用一些不熟悉的 API 的时候有快速查询的地方(不常使用的知识很快就忘了 :sleeping: ,不过学习也就是不断遗忘和重复的过程)。本文主要是说 Array 的技巧,必然要对 API 深入一些,这一小节我们就对 Array 相关的内容进行梳理。

因为数组的 API 很多,有些很类似,特别是有没有返回值,以及是否会改变原数组,很容易混淆,在这一小节的最前面进行一个整理。

不改变原数组

  • Array.from()
  • Array.isArray()
  • Array.prototype.concat()
  • Array.prototype.every()
  • Array.prototype.filter()
  • Array.prototype.find()
  • Array.prototype.findIndex()
  • Array.prototype.flat()
  • Array.prototype.flatMap()
  • Array.prototype.forEach() forEach本身不改变原数组,但是 callback 可能会改变
  • Array.prototype.includes()
  • Array.prototype.map() map,但是 callback 可能会改变
  • Array.prototype.reduce()
  • Array.prototype.reduceRight()
  • Array.prototype.slice()

改变原数组

  • Array.prototype.copyWithin()
  • Array.prototype.fill()
  • Array.prototype.pop()
  • Array.prototype.push()
  • Array.prototype.shift()
  • Array.prototype.sort()
  • Array.prototype.splice()
  • Array.prototype.unshift()

返回数组

  • Array.from()
  • Array.prototype.concat()
  • Array.prototype.copyWithin()
  • Array.prototype.fill()
  • Array.prototype.filter()
  • Array.prototype.flat()
  • Array.prototype.flatMap()
  • Array.prototype.map()
  • Array.prototype.slice()
  • Array.prototype.sort()
  • Array.prototype.splice()

不返回数组

  • Array.isArray() 返回 Boolean
  • Array.prototype.every() 返回 Boolean
  • Array.prototype.find() 返回第一个满足测试函数的元素值或 undefined
  • Array.prototype.findIndex() 返回第一个满足测试函数的元素索引或 -1
  • Array.prototype.forEach() 返回 undefined
  • Array.prototype.includes() 返回 Boolean
  • Array.prototype.pop() 返回元素的值
  • Array.prototype.pop() 返回数组的新长度
  • Array.prototype.reduce() 返回遍历完成后累积的值
  • Array.prototype.reduceRight()返回遍历完成后累积的值
  • Array.prototype.shift() 返回删除的元素
  • Array.prototype.unshift() 返回新数组的长度

Array.length

JavaScript 中的最大安全整数是 2^{32} – 1,所以 Array.length 的范围应该是在 0 - 最大安全整数 之间。

var namelistA = new Array(4294967296); // 2的32次方 = 4294967296
var namelistC = new Array(-100) // 负号

console.log(namelistA.length); // RangeError: 无效数组长度
console.log(namelistC.length); // RangeError: 无效数组长度

var namelistB = [];
namelistB.length = Math.pow(2,32)-1; //set array length less than 2 to the 32nd power
console.log(namelistB.length);  // 4294967295

改变 length 的大小会改变数组。当我们设置 length 小于数组长度的时候。超过的部分会被截断。当我们设置 length 大于数组长度的时候,实际的元素数目会增加,新增的元素的值为 undefined(实际上和 Array(length) 一样,新增的位置此时并没有包含任何实际的元素,不能理所当然地认为它包含 arrayLength 个值为 undefined 的元素,但是如果输出)。

let arr = [1, 2, 3]

arr.length = 5;

console.log(arr, arr[3], arr[4]) //[ 1, 2, 3, <2 empty items> ] undefined undefined

let b = arr.map(e => e)
console.log(b) //[ 1, 2, 3, <2 empty items> ]

arr.forEach(e => console.log(e)) //forEach 会跳过空位
//1
//2
//3

Array.length 的属性特性为

  • writable:true 属性值可写。
  • enumerable:false 属性不可以通过迭代器 forfor...in 进行迭代。
  • configurable:false 不可删除或更改属性特性。

Array.from

Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。稀疏数组的空位会被转为 undefinedArray.from.length === 1。当我们有需要将对象转为数组的时候(比如需要使用数组方法而不想使用 applycall),Array.from 是一个不错的选择。

Array.from() 的第一个参数可以类数组对象(拥有一个 length 属性和若干索引属性的任意对象,包括字符串)可迭代对象(可以获取对象中的元素,如 MapSet 等)包括字符串。我的理解是可以用迭代器 for ... of 进行迭代的对象都可以。

Array.from() 可以接受第二,第三个参数,可以用这两个参数在新生成的数组上执行一次 mapArray.from(obj, mapFn, thisArg) 就相当于 Array.from(obj).map(mapFn, thisArg), 除非创建的不是可用的中间数组。

Array.from('clloz') //["c", "l", "l", "o", "z"]

//从Set生成数组
const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set); // [ "foo", "bar", "baz" ]

//从Map生成数组
const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map); // [[1, 2], [2, 4], [4, 8]]

Array.from(map.keys()); //[1, 2, 4]
Array.from(map.values()); //[2, 4, 8]

//从类数组对象生成数组
function f() {
  return Array.from(arguments);
}
f(1, 2, 3); //[ 1, 2, 3 ]

//使用map生成元素值为下标的数组
Array.from({length: 5}, (v, i) => i); // [0, 1, 2, 3, 4]

如果不考虑 Array.frommapArray.from 做的事情类似如下函数

Array.from = function (arrayLike) {
    let k = 0;
        len = arrayLike.length;
    let result = new Array(len);
    while (k < len) {
        result[k] = arrayLike[k];
        k++
    }
    return result
}
let c = Array.from('clloz')
console.log(c) // [ 'c', 'l', 'l', 'o', 'z' ]

我这个只是大致演示一下 Array.from 做的事情,完整的 polyfill 参考 Array.from – MDN

Array.isArray()

Array.isArray() 有几个容易忽略的点:
1. Array.isArray(Array.prototype) 返回 trueArray.prototype 本身也是一个数组,length0
2. Array.isArray() 能检测 iframes 中的 Arrayinstanceof 则不能。
3. Array.isArray()polyfill

if (!Array.isArray) {
    Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
    };
}

Array.of()

Array.of() 就是没有 Array(length) 形式的 Array。也就是说他只支持 Array.of(el1, el2, ...) 这种形式。它是 ES6 中的新方法。

//polyfill
if (!Array.of) {
  Array.of = function() {
    return Array.prototype.slice.call(arguments);
  };
}

Array.prototype.concat()

Array.prototype.concat() 需要注意的一点就是它是浅拷贝,当我们的数组中有对象或者嵌套数组的时候,是无法进行深拷贝的。细节看下面的代码:

let a = [[1], 2]
let b = [[3], 4, {name: 'clloz'}]

let c = a.concat(b)
console.log(c) //[ [ 1 ], 2, [ 3 ], 4, { name: 'clloz' } ]
b[2].name = 'clloz1992'
console.log(c)
a[0].push(100) //[ [ 1 ], 2, [ 3 ], 4, { name: 'clloz1992' } ]
console.log(c) //[ [ 1, 100 ], 2, [ 3 ], 4, { name: 'clloz1992' } ]

concat 除了接受数组作为参数,也可以接受其他值,如果值是引用类型则将引用添加到新的数组中,如果是值类型则将值添加到新数组中(StringNumber 或者 Boolean,非包装类型)。

我自己实现的一个简单的 concatpolyfill

Array.prototype._concat = function () {
    if (Object.prototype.toString.call(this) !== '[object Array]') throw Error('this is not a Array!')
    let result = Array.prototype.slice.call(this);
    let args = Array.prototype.slice.call(arguments);

    for(let i = 0; i < args.length; i++) {
        if (Array.isArray(args[i])) {
            for (let j = 0; j < args[i].length; j++) {
                result.push(args[i][j])
            }
        }
    }
    return result;
}
let a = [1,2,3,4]
let b = [5,6,7]
let c = [8,9,10]
let d = a._concat(b, c)
console.log(d) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Array.prototype.every()

Array.prototype.every()ES5 添加的数组方法,下面列出 every 的几个注意点:

  • every 和数学中的”所有”类似,当所有的元素都符合条件才会返回 true。正因如此,若传入一个空数组,无论如何都会返回 true
  • 没有传入 this 值,callback 中的 this 在非严格模式下是全局对象,严格模式下为 undefined
  • 只要任何一次 callback 的执行返回 false,将直接返回 false(后面的元素讲不会再执行回调函数)。否则会执行到最后一个元素,返回 true
  • 不改变原数组。
  • every 遍历的元素范围在第一次调用 callback 之前就已确定了。在调用 every 之后添加到数组中的元素不会被 callback 访问到。如果数组中存在的元素被更改,则他们传入 callback 的值是 every 访问到他们那一刻的值。那些被删除的元素或从来未被赋值的元素将不会被访问到(所以对于稀疏数组中的空位 every 是不会处理的)。

polyfill 实现参考 Array.prototype.every() – MDN

Array.prototype.fill()

Array.prototype.fill()ES6 添加的数组方法,接受三个参数 value,start 和 endstartend 的默认值分别是 0,this.length,如果传入的值是个负数, 则开始索引会被自动计算成为 length+start/end,即从后往前数第 start/end 个,-1 就是倒数第一个。几个注意点:

  • 会填充稀疏数组的空位 Array(3).fill(4) 返回 [4,4,4]
  • 若填充的值是对象,则填充的是对象的引用。let a = Array(3).fill({}); console.log(a[0] === a[1]) 将返回 true
  • 原数组会改变,返回改变后的数组。

Array.prototype.filter()

Array.prototype.filter()ES5 添加的数组方法。filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或等价于 true 的值的元素创建一个新数组。注意点如下:

  • filter 的一个参数 callback 接受三个参数,元素值元素索引数组本身;第二个参数是 callback 执行时的 this,若没有传入则在非严格模式下为全局对象,严格模式下为 undefined
  • callback 只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。稀疏数组中的空位不会被处理。
  • filter 遍历的元素范围在第一次调用 callback 之前就已经确定了。在调用 filter 之后被添加到数组中的元素不会被 filter 遍历到(比如在遍历过程中)。如果已经存在的元素被改变了,则他们传入 callback 的值是 filter 遍历到它们那一刻的值。被删除或从来未被赋值的元素不会被遍历到。

Array.prototype.find()

Array.prototype.find()ES6 添加的数组方法,参数和注意点和 filter 类似,不同的是不会跳过稀疏数组的空位,找到立即返回元素值(只找第一个),否则返回 undefined

Array.prototype.findIndex()

find 几乎相同,不过返回值是元素索引,若没有找到则返回 -1

Array.prototype.flat()

ES2019 新增的数组扁平化 API,支持设定深度,不改变原数组,返回扁平化后的新数组。该方法会移除稀疏数组中的空位

Array.prototype.flatMap()

这个方法相当于先对数组执行 map,然后对 map 之后返回的数组执行 flat(1)

var arr1 = [1, 2, 3, 4];
JSON.stringify(arr1.map(x => [[x*2]])); // "[[[2]],[[4]],[[6]],[[8]]]"
JSON.stringify(arr1.map(x => [[x*2]]).flat(1)) //"[[2],[4],[6],[8]]"
JSON.stringify(arr1.map(x => [[x*2]]).flat(1)) === JSON.stringify(arr1.flatMap(x => [[x*2]])) //true

Array.prototype.forEach()

forEach() 方法按升序为数组中含有效值的每一项执行一次 callback 函数,稀疏数组上的空位被跳过。forEach 接受两个参数,第一个是 callback 回调函数,接受三个参数:当前元素值,当前元素索引,数组对象本身(后两个可选);第二个是回调函数执行时的 this。该方法注意点总结:

  • 稀疏数组的空位会被跳过。
  • this 默认在严格模式下是 undefined,非严格模式下是全局对象。
  • forEach() 遍历的范围在第一次调用 callback 前就会确定。调用 forEach 后添加到数组中的项不会被 callback 访问到。如果已经存在的值被改变,则传递给 callback 的值是 forEach() 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()),之后的元素将被跳过。
  • forEach() 为每个数组元素执行一次 callback 函数,返回值始终是 undefined,所以不可被链式调用,一般会放在链式调用的最后。
  • forEach() 本身不会改变原数组,但是在回调函数中由于我们可以传入原数组和当前元素索引,所以回调函数中可以修改原数组。
  • 除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具。
//回调函数改变原数组
let arr = [1,2,3]
arr.forEach((v, i, thisArr) => {
    thisArr[i] *= 2;
})
console.log(arr) //[ 2, 4, 6 ]

//迭代过程中修改数组,有元素会被跳过,其他遍历方法类似
var words = ['one', 'two', 'three', 'four'];
words.forEach(function(word) {
  console.log(word);
  if (word === 'two') {
    words.shift();
  }
});
// one
// two
// four

Array.prototype.includes()

Array.prototype.includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。该方法接受两个参数,一个是要查找的值,另一个可选参数是从哪个索引开始查找。

Array.prototype.includes() 的注意点:

  • 比较字符和字符串区分大小写。
  • 稀疏数组的空位被当做 undefined 处理(当你想找的是 undefined 的时候会返回 true
  • 如果第二个参数大于等于数组长度,会直接返回 false,并且不会进行查找。
  • 如果第二个参数 fromIndex 为负值,计算出的索引 array.length + fromIndex 将作为开始搜索的位置。如果计算出的索引仍然小于 0,则整个数组都会被搜索。
  • 不仅可以用于数组,也可以用于类数组对象,需利用 callapply

Array.prototype.indexOf()

indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1。接受两个参数,一个是要查找的值,另一个可选参数是从哪个索引开始查找。该方法和 Array.prototype.includes() 类似,只不过一个返回的是布尔值,一个返回的是索引。匹配是否和参数相等采用的是 === 严格相等。

Array.prototype.join()

join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。接受一个可选参数作为连接数组元素时的分隔符。返回所有数组元素连接的字符串如果 arr.length === 0,则返回空字符串。该方法的注意点如下:

  • 如果一个元素为 undefinednull,它会被转换为空字符串。
  • 如果元素是 ObjectFunction,则会调用对应的 toString() 方法转为字符串。
  • Number(), String(), Boolean 包装的对象也都会被当做基本类型处理。
  • 利用 callapply 可以用在其他可迭代对象上,比如 arguments,字符串。
let a = {length: 10, name: 'clloz'}
let str = Array.prototype.join.call(a, ',')
console.log(str) //,,,,,,,,,
function m() {}
let arr = [1,2,3,4, {name: 'clloz'}, [2,[3,4,5]], m , Number(10), true, String('clloz'), Boolean(1)]
console.log(arr.join(','))
//1,2,3,4,[object Object],2,3,4,5,function m() {},10,true,clloz,true

Array.prototype.keys()

ES6 添加的数组方法.keys() 方法返回一个包含数组中每个索引键的 Array Iterator 对象。一个新的 Array 迭代器对象。

var arr = ["a", , "c"];
var sparseKeys = Object.keys(arr);
var denseKeys = [...arr.keys()];
console.log(sparseKeys); // ['0', '2']
console.log(denseKeys);  // [0, 1, 2]

Array.prototype.lastIndexOf()

理解为反向的 Array.prototype.indexOf() 即可。

Array.prototype.map()

Array.prototype.map()ES5 提供的数组方法。map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

forEach() 相同,接受两个参数,第一个是 callback 回调函数,接受三个参数:当前元素值,当前元素索引,数组对象本身(后两个可选);第二个是回调函数执行时的 this。注意点如下:

  • 稀疏数组的空位会被跳过并被保留在返回的数组中(根据规范中定义的算法,如果被 map 调用的数组是离散的,新数组将也是离散的保持相同的索引为空)。
  • 只有在你需要返回数组或需要回调函数返回值的时候再使用 map,否则应该使用 forEachfor ... of
  • 和所有遍历方法一样,在回调中修改数组可能会让一些元素无法执行回调。
  • 利用 callapply 可以用在其他可迭代对象上,比如 arguments,字符串。
  • 注意回调函数的参数个数和顺序,一个经典的问题就是 ["1", "2", "3"].map(parseInt); 返回 [1, NaN, NaN]
  • 回调函数没有返回值,生成的元素是 undefined

Array.prototype.pop()

ES3 就存在的数组方法。pop() 方法从数组中删除最后一个元素,并返回该元素的值(当数组为空时返回 undefined)。此方法更改数组的长度。可应用在类似数组的对象上。pop 方法根据 length 属性来确定最后一个元素的位置。如果不包含 length 属性或 length 属性不能被转成一个数值,会将 length 置为 0,并返回 undefined

//数组即对象
let a = {length: 10, name: 'clloz'}
Array.prototype.pop.call(a)
console.log(a) //{ length: 9 }

//length 为 0,下标 0 处本来就没有元素,不会有任何变化
var obj ={
    '2': 3,
    '3': 4,
    'length': 0,
    'pop': Array.prototype.pop,
}

obj.pop()
console.log(obj) //{ '2': 3, '3': 4, length: 0, pop: [Function: pop] }

//length 为 3,下标为 2 的元素是 `3`,该元素奖杯删除,`length` 减一
var obj ={
    '2': 3,
    '3': 4,
    'length': 0,
    'pop': Array.prototype.pop,
}

obj.pop()
console.log(obj) //{ '2': 3, '3': 4, length: 0, pop: [Function: pop] }

//没有 length 或 length不能转为数字,会添加 length属性,属性值为 0
var obj ={
    '2': 3,
    '3': 4,
    'pop': Array.prototype.pop,
}

obj.pop()
console.log(obj) //{ '2': 3, '3': 4, pop: [Function: pop], length: 0 }

Array.prototype.push()

ES3 就存在的数组方法。push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

可以接受多个参数,作为添加到数组末尾的元素,返回值为新的 length 属性。和 pop 一样利用 callapply 可以应用到非数组对象上。

push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。这一点非常重要,看下面的代码:

var obj ={
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}

obj.push(1)
obj.push(2)
console.log(obj)

//{
//  '2': 1,
//  '3': 2,
//  length: 4,
//  splice: [Function: splice],
//  push: [Function: push]
//}

因为对象中的 length2,所以从下标 2 开始插入,并改变 length 的值,所以最后 1 2 分别插入了下标 23 的报位置,取代了原来的值 34。而如果没有 length 则会创建 length,初始值为 0,并从 0 开始插入,最后结果如下:

var obj ={
    '2': 3,
    '3': 4,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}

obj.push(1)
obj.push(2)
console.log(obj)

//{
//  '0': 1,
//  '1': 2,
//  '2': 3,
//  '3': 4,
//  splice: [Function: splice],
//  push: [Function: push],
//  length: 2
//}

唯一的原生类数组(array-like)对象是 String,它们并不适用该方法,因为字符串是不可改变的。

let a = {length: 10, name: 'clloz'}
Array.prototype.push.call(a, 1, 2, 3, 4)
console.log(a)
console.log(a[0])

Array.prototype.reduce()

Array.prototype.reduce()ES5 提供的数组方法。reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

该方法接受两个参数,回调函数 reducer 和 初始值 initialValuereducer 接受四个参数,accumulator 为上一次回调函数执行累积的值或者 initialValuecurrentValue 当前正在处理的元素,index 当前处理元素的索引,array 调用 reducer 的数组,后两个参数可选。initialValue 作为第一次调用 callback 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。返回值为累积处理的结果。

回调函数第一次执行时,accumulatorcurrentValue 的取值有两种情况:如果调用 reduce() 时提供了 initialValueaccumulator 取值为 initialValuecurrentValue 取数组中的第一个值;如果没有提供 initialValue,那么 accumulator 取数组中的第一个值,currentValue 取数组中的第二个值。如果没有提供 initialValuereduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供 initialValue,从索引 0 开始。

如果数组为空且没有提供 initialValue,会抛出 TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供 initialValue, 或者有提供 initialValue 但是数组为空,那么此唯一值将被返回并且 callback 不会被执行。

//reduce 实现 map
if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function(callback, thisArg) {
    return this.reduce(function(mappedArray, currentValue, index, array) {
      mappedArray[index] = callback.call(thisArg, currentValue, index, array)
      return mappedArray
    }, [])
  }
}

[1, 2, , 3].mapUsingReduce(
  (currentValue, index, array) => currentValue + index + array.length
) // [5, 7, , 10]

//多函数管道 将多个函数当做数组进行 reduce实现特定功能
// Building-blocks to use for composition
const double = x => x + x;
const triple = x => 3 * x;
const quadruple = x => 4 * x;

// Function composition enabling pipe functionality
const pipe = (...functions) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
);

// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);

// Usage
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240

Array.prototype.reduceRight()

Array.prototype.reduceRight()ES5 提供的数组方法。从右向左进行遍历的 reduce()

Array.prototype.reverse()

ES1 就存在的数组方法。reverse() 方法将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。利用 callapply 可以用在其他可迭代对象上,比如 arguments。唯一的原生类数组(array-like)对象是 String,它们并不适用该方法,因为字符串是不可改变的。

Array.prototype.shift()

ES3 增加的数组方法。shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。该方法移除索引为 0 的元素(即第一个元素),并返回被移除的元素,其他元素的索引值随之减 1。如果 length 属性的值为 0 (长度为 0),则返回 undefined。该方法能够通过 callapply 方法作用于类似数组的对象上,若对象没有 length 属性,调用该方法不会有其他操作,但是会添加 length 属性,属性值 0

//没有length会给对象添加值为 0 的 length 属性,不执行其他操作
var obj ={
    '2': 3,
    '3': 4,
    'shift': Array.prototype.pop,
}

obj.shift()
console.log(obj) //{ '2': 3, '3': 4, shift: [Function: pop], length: 0 }

//length 为 0 不进行任何操作,返回undefined
var obj ={
    '2': 3,
    '3': 4,
    length: 0,
    'shift': Array.prototype.pop,
}

obj.shift()
console.log(obj) //{ '2': 3, '3': 4, length: 0, shift: [Function: pop] }

其实就是一个反向的 Array.prototype.pop()

Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin不包括end)。原始数组不会被改变。

该方法接受两个可选参数 beginend,表示提取元素的索引,[begin, end),包括 begin 不包括 end

若没有传入 begin 则从 0 开始提取;没有传入 end,提取到原数组末尾。若 beginend 为负,则计算 length + begin,若计算值在数组的索引范围内则从计算值开始提取,若依然为负则从 0 开始。如果 begin 大于原数组的长度,则会返回空数组。如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。

需要注意 slice 是浅拷贝,如果某个元素是对象引用,在返回的新数组中依然是相同的引用。对于字符串、数字及布尔值来说(不是 StringNumber 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

可以使用 applycall 将一个类数组(Array-like)对象/集合转换成一个新数组。

Array.prototype.some()

some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。

该方法可以类比 Array.prototype.every()every() 是找到一个 callback 返回值可转换为为 false 的就立即返回,否则遍历所有元素,最后返回 truesome() 是找到一个 callback 返回值可转换为为 true 的就立即返回,否则遍历所有元素,最后返回 false。其他机制基本相同。

Array.prototype.sort()

ES1 就提供的数组方法。sort() 方法用原地算法对数组的元素进行排序,并返回数组。由于它取决于具体实现,因此无法保证排序的时间和空间复杂性。

该方法接受一个可选的 compareFunction 函数,如果没有指明该函数,那么元素会按照转换为的字符串的诸个字符的 Unicode 位点进行排序。

//默认按Unicode进行排序,我们记住 ACSII 的顺序即可
let arr = [3, 15, 8, 29, 102, 22]
console.log(arr.sort())
//[ 102, 15, 22, 29, 3, 8 ]

如果指明了 compareFunction ,那么数组会按照调用该函数的返回值排序。即 ab 是两个将要被比较的元素:

  • 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前;
  • 如果 compareFunction(a, b) 等于 0ab 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla2003 年之前的版本);
  • 如果 compareFunction(a, b) 大于 0b 会被排列到 a 之前。
  • compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。

Array.prototype.splice()

Array.prototype.splice()ES3 提供的数组方法。 splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

该方法的前两个参数 startdeleteCount 表示从哪个索引开始删除多少个元素,删除的元素包含 start 位置的元素。如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于 start 之后的所有元素的数量),那么 start 之后数组的所有元素都会被删除。如果 deleteCount0 或者负数,则不移除元素,这种情况下,至少应添加一个新元素,否则没有意义。

从第三个参数开始就是要天剑的新元素,添加的位置就是从 start 开始。如果添加进数组的元素个数不等于被删除的元素个数,数组的长度会发生相应的改变。

返回值是由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

Array.prototype.unshift()

类比 Array.prototype.push()

//没有length属性的对象调用会添加值为 0 的length属性
var obj ={
    '0': 3,
    '1': 4,
    'unshift': Array.prototype.unshift,
}

obj.unshift()
console.log(obj) //{ '0': 3, '1': 4, unshift: [Function: unshift], length: 0 }

//从 length 处插入
var obj ={
    '0': 3,
    '1': 4,
    length: 0,
    'unshift': Array.prototype.unshift,
}

obj.unshift(1,2)
console.log(obj) //{ '0': 1, '1': 2, length: 2, unshift: [Function: unshift] }

Array.prototype.toString()

toString() 返回一个字符串,表示指定的数组及其元素。

Array 对象覆盖了 ObjecttoString 方法。对于数组对象,toString 方法连接数组并返回一个字符串,其中包含用逗号分隔的每个数组元素。

当一个数组被作为文本值或者进行字符串连接操作时,将会自动调用其 toString 方法。

API 的几点共通点

  1. 很多方法存在对应,比如 someeverypopshiftpushunshift 等,他们的机制基本相同。
  2. 几乎所有 callback 的机制都相同,即在第一次 callback 执行之前元素遍历范围就已经确定,在 callback 执行过程中如果对原数组进行改动可能引起部分元素不会被遍历到。
  3. 大多数 API 都可以使用 callapply 拓展到任何带有 length 属性的对象(javascript 的数组本质就是这样一个对象)。部分方法不支持字符串调用,因为字符串不可被更改。
  4. 大多数 callback 需要传入索引作为参数的方法,机制都相同。当索引为负的时候,会计算 length + index,若计算值是一个在数组范围内的值,则以这个值执行;若计算值依然是负值,则忽略该参数。
  5. push, pop, shift, unshift 这几个添加删除元素的方法都是以 length 为基准进行操作,对于没有 length 的对象会添加一个值为 0length 属性。以 length 为基准的意思举个例子就是,我们执行 push(1,2),如果我们的对象 length2,那么即使对象中索引 23 已经有元素值,那么也会用 12 进行覆盖。

查找数组

  1. Array.prototype.find()
  2. Array.prototype.findIndex()
  3. Array.prototype.includes()
  4. Array.prototype.indexOf()
  5. Array.prototype.lastIndexOf()

遍历数组

  1. Array.prototype.every()
  2. Array.prototype.filter()
  3. Array.prototype.forEach()
  4. Array.prototype.map()
  5. Array.prototype.reduce()
  6. Array.prototype.reduceRgiht()
  7. Array.prototype.some()

Array Iterator

  1. Array.prototype.entries()
  2. Array.prototype.keys()
  3. Array.prototype.values

复制数组

  1. Array.prototype.slice.call()

  2. Array.from(),和 Array.prototype.slice.call() 的区别就是 Array.from()ES6 才有的方法。他们的性能存在差异,我只用了一个简单的数组进行了测试:结果就是当数组长度比较小的时候,Array.from() 速度更快一点,当数组长度越长,Array.prototype.slice.call() 速度更快,我只是用的一个 flat 的数组,没有测试嵌套数组和对象, Set Map 等情况。

let a = Array.from({length: 1000000}, (v, i) => i);
console.time('slice')
for (let i = 0; i < 1000; i++) {
    Array.prototype.slice.call(a)
}
console.timeEnd('slice') //slice: 5995.096ms
console.time('from')
for (let i = 0; i < 1000; i++) {
    Array.from(a)
}
console.timeEnd('from') //from: 6668.785ms

创建元素值等于下标的数组

这个源于知乎上的一道题目,解法有如下几种:

Object.keys(Array.apply(null, {length: 100}))

Array.from(Array(100).keys())

Array.from({length: 100}, (v, i) => i);

[...Array(100).keys()]

Array.apply(null, Array(100)).map(Function.prototype.call.bind(Number))

//自定义迭代器
Number.prototype[Symbol.iterator] = function() {
    return {
        v: 0,
        e: this,
        next() {
            return {
                value: this.v++,
                done: this.v > this.e
            }
        }
    }
}

[...100]

需要特别注意,在 JavaScript 中参数的个数是有上限的,JavaScriptCore 引擎中有被硬编码的 参数个数上限:65536。但是实际能接受多少参数取决于当前的系统和浏览器,并不确定。比如我用上面的用 apply 生成元素值为元素下标的数组,在 safari 中的上限是 65536,在 chrome 中是 125382。。任何用到超大栈空间的行为都有可能出现这个现象,超出限制则会报错 Uncaught RangeError: Maximum call stack size exceeded

去重

  1. 利用 Array.fromSet
function combine(){
    let arr = [].concat.apply([], arguments);  //没有去重复的新数组
    return Array.from(new Set(arr));
}

var m = [1, 2, 2], n = [2,3,3];
console.log(combine(m,n)); // [1, 2, 3]
  1. reduce
let myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd']
let myOrderedArray = myArray.reduce(function (accumulator, currentValue) {
    if (accumulator.indexOf(currentValue) === -1) {
        accumulator.push(currentValue)
    }
    return accumulator
}, [])
console.log(myOrderedArray)

let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];
let result = arr.sort().reduce((init, current) => {
    if(init.length === 0 || init[init.length-1] !== current) {
        init.push(current);
    }
    return init;
}, []);
console.log(result); //[1,2,3,4,5]
  1. 两层循环比较
function unique(arr){
    for(var i=0; i<arr.length; i++){
        for(var j=i+1; j<arr.length; j++){
            if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                arr.splice(j,1);
                j--;
            }
        }
    }
    return arr;
}
  1. filter + indexOf
function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
  1. forEach + includes
function unique(arr) {
    let result = []
    arr.forEach((v) => {
        if(!result.includes(v)) result.push(v)
    })      
    return result
}

去重还有很多种实现,不过本质都是一样的,遍历数组比较去除重复(根据利用的 API 不同可能要创建一个中间数组)。或者利用像 Set 或者 Object 的键不可重复的特性。

扁平化

  1. Array.prototype.flat()

  2. reduce 与 concat

var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
    function(a, b) {
        return a.concat(b);
    },
    []
);
// flattened is [0, 1, 2, 3, 4, 5]

var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
    ( acc, cur ) => acc.concat(cur),
    []
);
  1. reduce + concat + isArray + recursivity
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];

function flatDeep(arr, d = 1) {
   return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
                : arr.slice();
};

flatDeep(arr1, Infinity);
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
  1. forEach+isArray+push+recursivity
// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) => {
  const result = []; // 缓存递归结果
  // 开始递归
  (function flat(arr, depth) {
    // forEach 会自动去除数组空位
    arr.forEach((item) => {
      // 控制递归深度
      if (Array.isArray(item) && depth > 0) {
        // 递归数组
        flat(item, depth - 1)
      } else {
        // 缓存元素
        result.push(item)
      }
    })
  })(arr, depth)
  // 返回递归结果
  return result;
} 

// for of 循环不能去除数组空位,需要手动去除
const forFlat = (arr = [], depth = 1) => {
  const result = [];
  (function flat(arr, depth) {
    for (let item of arr) {
      if (Array.isArray(item) && depth > 0) {
        flat(item, depth - 1)
      } else {
        // 去除空元素,添加非undefined元素
        item !== void 0 && result.push(item);
      }
    }
  })(arr, depth)
  return result;
}
  1. 使用堆栈stack
// 无递归数组扁平化,使用堆栈
// 注意:深度的控制比较低效,因为需要检查每一个值的深度
// 也可能在 shift / unshift 上进行 w/o 反转,但是末端的数组 OPs 更快
var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
function flatten(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop();
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素,不会改动原始输入
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  // 反转恢复原数组的顺序
  return res.reverse();
}
flatten(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
// 递归版本的反嵌套
function flatten(array) {
  var flattend = [];
  (function flat(array) {
    array.forEach(function(el) {
      if (Array.isArray(el)) flat(el);
      else flattend.push(el);
    });
  })(array);
  return flattend;
}
  1. Generator
function* flatten(array) {
    for (const item of array) {
        if (Array.isArray(item)) {
            yield* flatten(item);
        } else {
            yield item;
        }
    }
}

var arr = [1, 2, [3, 4, [5, 6]]];
const flattened = [...flatten(arr)];
// [1, 2, 3, 4, 5, 6]

计算元素出现的次数

  1. reduce
var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

var countedNames = names.reduce(function (allNames, name) { 
    if (name in allNames) {
        allNames[name]++;
    }
    else {
        allNames[name] = 1;
    }
    return allNames;
}, {});
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

清除

  1. 设置数组的 length0

按属性分类对象

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }

求两数组交集

let arr1 = [0,1,2,3,4,5,4,5]
let arr2 = [4,5,6,7,8,9,0]
let result = Array.from(new Set(arr1.filter(v => arr2.includes(v))))
console.log(result) //[ 0, 4, 5 ]

参考文章

  1. JS 中的稀疏数组和密集数组
  2. 如何不使用loop循环,创建一个长度为100的数组,并且每个元素的值等于它的下标? – Gaubee的回答 – 知乎

Clloz

Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

 

00:00/00:00