数组方法使用和实现

57 阅读24分钟

数组方法副作用

会改变原来数组的有:
pop()---删除数组的最后一个元素并返回删除的元素。

push()---向数组的末尾添加一个或更多元素,并返回新的长度。

shift()---删除并返回数组的第一个元素。

unshift()---向数组的开头添加一个或更多元素,并返回新的长度。

reverse()---反转数组的元素顺序。

sort()---对数组的元素进行排序。

splice()---用于插入、删除或替换数组的元素。

特殊的map方法

var arr = [{ a: 1, b: 12 }]

var b = arr.map(item => { item.a = 10; return item })

console.log(arr)  // [{a: 10, b: 12}]

数组方法总结

一、 检测方法

Array.isArray()

判断传入的值是否是一个数组。

// true
Array.isArray([1, 2, 3])
// false
Array.isArray({foo: 123})
// false
Array.isArray('foobar')   
// false
Array.isArray(undefined)  

二、 创建数组方法

Array.from()

Array.from()方法用于将类数组对象可迭代对象转为真正的数组,并且返回一个新的,浅拷贝的数组实例。

// 报错
Array.from(undefined)
// 报错
Array.from(null)
// ["f", "o", "o"]
console.log(Array.from('foo'))
// []
console.log(Array.from(''))
// []
console.log(Array.from(123))
// []
console.log(Array.from(NaN))

// arguments对象转为数组
function foo() {
  const args = Array.from(arguments)
  //true
  console.log(Array.isArray(args))
}
foo(1, 2, 3)

// NodeList对象转为数组
Array.from(document.querySelectorAll('p'))

// Set对象转为数组:['a','b']
Array.from(new Set(['a', 'b'])) 

// Map对象转为数组:[[1, 2], [2, 4]]
Array.from(new Map([[1, 2], [2, 4]])) 
// 传入第二个参数回调函数:[2, 4, 6]
Array.from([1, 2, 3], x => x + x)
let obj = {
  num: 1,
  handle: function(value){
    return n + this.num
  }
}
// 传入第三个参数修改this指向:[2, 3, 4, 5, 6]
const arrs = Array.from([1, 2, 3, 4, 5], obj.handle, obj)
// 得到数组对象里的id属性:[1, 2]
const obj = [{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}]
Array.from(obj,(el) => {
  return el.id
})

注意: Array.from(null)或者Array.from(undefined)会抛出异常

Array.of()

Array.of()创建一个包含所有传入参数的数组,不考虑参数的数量或类型,返回一个新数组。

使用Array.of() 创建新数组:

Array.of()                  // []
Array.of(undefined)         // [undefined]
Array.of(null)              // [null]
Array.of(NaN)               // [NaN]
Array.of(1)                 // [1]
Array.of(1, 2)              // [1, 2]
Array.of([1,2,3])           // [[1,2,3]]
Array.of({id: 1},{id: 2})   // [{id:1}, {id:2}]

三、 遍历(迭代)方法

forEach()

对数组中的每一项运行指定的函数。这个方法返回undefined,即使你return了一个值。

Array.forEach()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}]
// 1 - zhangsan
// 2 - lisi
arr.forEach(el => {
    console.log(`${el.id} - ${el.name}`);
});

const obj = {
  handle: function(n){
    return n + 2
  }
};
// true 
[{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}].forEach(function(el,index,arr){
  if(el.id === 1) {
    return
  }
  console.log(this === obj)
},obj);

Array.forEach()不能中断循环(使用break,或continue语句)。只能用return退出本次回调,进行下一次回调。

map()

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

Array.map()参数语法:

  1. 第一个参数(必填): callback生成新数组元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const newArr = arr.map((el,index,arr) => {
  el.age = 20
  return el
});
//[{id: 1,age: 20},{id: 2,age: 20},{id: 3,age: 20}]
console.log(newArr);

filter()

对数组中的每一项运行指定的函数,返回该函数会返回true的项组成的新的数组。如果没有任何数组元素通过测试,则返回空数组。

Array.filter()参数语法:

  1. 第一个参数(必填): callback用来测试数组的每个元素的函数。返回true 表示该元素通过测试,保留该元素,false 则不保留。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const newArr = arr.filter((el,index,arr) => {
  el.age = 20
  return el
});
// [{id: 1,age: 20},{id: 2,age: 20},{id: 3,age: 20}]
console.log(newArr);

some()

检测数组中的是否有满足判断条件的元素。

对数组中的每一项运行指定的函数,如果该函数对任一项返回true,则返回true,并且剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false

Array.some()参数语法:

  1. 第一个参数(必填): callback用来测试每个元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const someResult = arr.some((el,index,arr) => {
  return el.id === 1
});
// true
console.log(someResult)

every()

检测数组所有元素是否都符合判断条件。

对数组中的每一项运行指定的函数,如果该函数对每一项都返回true,则返回true。若收到一个空数组,此方法在一切情况下都会返回true。如果数组中检测到有一个元素不满足,则返回 false,且剩余的元素不会再进行检测。

Array.every()参数语法:

  1. 第一个参数(必填): callback用来测试每个元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
// true
[].every(() => {})

const arr = [{id: 1},{id: 2},{id: 3}]
const everyResult = arr.every((el,index,arr) => {
  return el.id > 0
});
// true
console.log(everyResult)

find()

返回数组中匹配的第一个元素的值,否则返回undefined

Array.find()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身 (可选)
  1. 第二个参数(可选):当执行回调函数时 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const findResult = arr.find((el,index,arr) => {
  return el.id  === 1
},obj);
// {id: 1}
console.log(findResult)

findIndex()

返回数组中匹配的第一个元素的索引。否则返回-1

Array.findIndex()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引值数组本身
  1. 第二个参数(可选):当执行回调函数时 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
// 2
const findResult = arr.findIndex((el,index,arr) => {
  return el.id  === 3
},obj)

entries()keys()values()

用于遍历数组,它们都返回一个遍历器Array Iterator对象。可以用for...of循环进行遍历,他们的区别是keys()是对键名的遍历、values()是对键值的遍历entries()是键值对的遍历。

// 0
// 1
for (let i of ['a', 'b'].keys()) {
  console.log(i)
}

// a
// b
for (let el of ['a', 'b'].values()) {
  console.log(el)
}

// 0-a
// 1-b
for (let [i, el] of ['a', 'b'].entries()) {
  console.log(`${i}-${el}`)
}

可以手动调用遍历器对象的next方法,进行遍历。

const arr = ['a', 'b', 'c']
const tempIterator = arr.entries()
// [0, "a"]
console.log(tempIterator.next().value)

// [1, "b"]
console.log(tempIterator.next().value)

四、操作方法

push()

将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

var numbers = [1, 2, 3]
// 5
console.log(numbers.push(4,5))
// [1,2,3,4,5]
console.log(numbers)

pop()

从数组中删除最后一个元素,并返回删除的元素。

const arr = ['a', 'b', 'c']
// c
console.log(arr.pop())
// ["a", "b"]
console.log(arr);

shift()

shift() 方法从数组中删除第一个元素,并返回删除的元素。

const arr = ['a', 'b', 'c']
// a
console.log(arr.shift())
// ["b", "c"]
console.log(arr)

unshift()

将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

const arr = ['a', 'b', 'c']
// 5
console.log(arr.unshift('d', 'e'))
// ["d", "e", "a", "b", "c"]
console.log(arr)

concat()

用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。如果省略参数,则concat会返回当前数组的浅拷贝。

const arr = [1, 2, 3]
const newArr = arr.concat()
// [1,2,3]
console.log(newArr)
// false
console.log(newArr === arr)

const arr = [1, 2, 3]
const newArr = arr.concat([4, 5])
// [1, 2, 3, 4, 5]
console.log(newArr)

indexOf()lastIndexOf()

这两个方法都返回要查找的元素在数组中的位置,或者在没找到的情况下返回-1indexOf()方法从数组的开头开始向后查找,lastIndexOf()方法则从数组的末尾开始向前查找。

Array.indexOf()、Array.lastIndexOf()参数语法:

  1. 第一个参数 searchElement(可选):被查找的元素。
  2. 第二个参数 fromIndex(可选):indexOf()方法表示开始向后查找的位置。默认值为0lastIndexOf()方法表示从此位置开始逆向查找。默认为数组的长度减 1 (arr.length - 1)。

indexOf()

const numbers = [1, 2, 3, 4, 5, 4]
// 3
console.log(numbers.indexOf(4))
// 5
console.log(numbers.indexOf(4, 4)) 

lastIndexOf()

const numbers = [1, 2, 3, 4, 5, 4]
// 5
console.log(numbers.lastIndexOf(4))
// 3
console.log(numbers.lastIndexOf(4, 4))

slice()

创建一个新的数组并返回。该方法接受两个参数:是一个由起始索引和结束索引的提取出来的原数组的浅拷贝。原始数组不会被改变。

Array.slice()参数语法:

  1. 第一个参数(可选):起始索引 begin(默认从 0 开始),从该索引开始提取原数组元素。
  2. 第二个参数(可选):结束索引 end 在该索引结束提取原数组元素。如果该参数省略,则一直提取到原数组末尾结束。slice 会提取原数组中beginend的所有元素(包含 begin,但不包含end)。
const arr = [1, 2, 3, 4]
const newArr = arr.slice(1)
// [2,3,4]
console.log(newArr);
const newArr1 = arr.slice(1, 3)
// [2,3]
console.log(newArr1)

如果结束位置小于起始位置,则返回空数组。

const arr = [1, 2, 3, 4]
const newArr = arr.slice(2, 1)
// []
console.log(newArr)

splice()

向数组的中删除插入替换元素。返回值是被删除的元素组成的一个数组。如果没有删除元素,则返回空数组。此方法会改变原数组。

删除任意数量的元素,传入 2 个参数,要删除的元素开始索引和要删除的个数。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
//删除前两个元素
arr.splice(0, 2)
// [{id: 3}]
console.log(arr)

向指定位置插入任意数量的元素,传入3个参数:起始位置、0(要删除的元素个数) 和要插入的元素。如果要插入多个元素,可以再传入第四、第五,以至任意多个元素。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从索引 1 开始插入两个元素
arr.splice(1, 0, { id: 4 }, { id: 5 })
// [{ id: 1 }, { id: 4 }, { id: 5 },{ id: 2 }, { id: 3 }]
console.log(arr)

向指定位置插入任意数量的元素,且同时删除任意数量的元素。传入3个参数:起始位置、要删除的元素个数和要插入的元素。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从索引 1 开始,删除一个元素,并切插入两个元素
arr.splice(1, 1, { id: 4 }, { id: 5 })
// [{ id: 1 }, { id: 4 }, { id: 5 },{ id: 3 }]
console.log(arr)

copyWithin()

在数组内部替换自身元素,返回修改后的当前数组。

Array.copyWithin()参数语法:

  1. 第一个参数(必填):从该位置开始替换元素。
  2. 第二个参数(可选):从该位置开始复制数据,默认为 0
  3. 第三个参数(可选):停止复制的索引(不包含自身),默认值为数组的长度。
// 将数组的前两个元素替换数组的最后两个位置:[1,2,1,2]
// 从索引2的位置开始替换
// 从索引0的位置开始复制数据
[1, 2, 3, 4].copyWithin(2,0)

const arr = [{id: 1},{id: 2},{id: 3}]
// [{id: 3},{id: 2},{id: 3}]
arr.copyWithin(0, 2)

// 从索引2的位置开始替换
// 从索引0的位置开始复制
// 在遇到索引1的时候停止复制(不包含自身)
// [1,2,1,4]
[1, 2, 3, 4].copyWithin(2,0)

fill()

使用固定值填充一个数组中一个或多个元素。

  1. 第一个参数:用来填充数组元素的值。
  2. 第二个参数(可选):起始索引,默认值为0
  3. 第二个参数(可选):终止索引,默认值为数组的长度。

当传入一个参数的时候,会用这个参数的值填充整个数组:

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]

arr.fill({ id: 4 })
// [{ id: 4 }, { id: 4 }, { id: 4 }]
console.log(arr)
// true
console.log(arr[0] === arr[1])

当传入多个个参数的时候,用这个参数的值填充部分数组:

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从数组下标索引为1的元素开始填充
arr.fill({ id: 4 }, 1)
// [{ id: 1 }, { id: 4 }, { id: 4 }]
console.log(arr)

// 填充的元素不包括终止的索引元素。
const numbers = [1, 2, 3, 4]
numbers.fill(0, 1, 2)
// [1, 0, 3, 4]
console.log(numbers)

flat()

将嵌套的数组,变成一维的数组。返回一个新数组。

Array.flat()参数语法:

  1. 第一个参数(可选):指定要提取嵌套数组的结构深度,默认值为 1。

展开一层

const arr = [1, 2, [3, 4]]
const newArr = arr.flat()
//[1,2,3,4]
console.log(newArr)

展开两层

const arr = [1, 2, [3, [4, 5]]]
const newArr = arr.flat(2)
// [1, 2, 3, 4, 5]
console.log(newArr)

使用 Infinity,可展开任意深度的嵌套数组

var arr = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
const newArr = arr.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(newArr)

移除数组中的空项

var arr = [1, 2, , 4, 5]
const newArr = arr.flat()
// [1, 2, 4, 5]
console.log(newArr)

flatMap()

对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

Array.flatMap()参数语法:

  1. 第一个参数(必填): callback遍历函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引(可选)数组对象本身(可选)
  1. 第二个参数(可选):当执行回调函数时this的值。
var arr = [1, 2]
const newArr = arr.flatMap(el => [el, el * 2])
[1,2,2,4]
console.log(newArr)

flatMap()只能展开一层数组

var arr = [1, 2]
const newArr = arr.flatMap(el => [[el, el * 2]])
// [[1,2],[2,4]]
console.log(newArr)

includes()

判断一个数组是否包含一个指定的值,如果包含则返回true,否则返回false。使用 includes()比较字符串和字符时是区分大小写的。

Array.includes()参数语法:

  1. 第一个参数:需要查找的元素值。
  2. 第二个参数:表示搜索的起始位置,默认为0
const obj = { id: 1 }
var arr = [obj, { id: 2 }]
// true
console.log(arr.includes(obj))

传入第二个参数

console.log([1, 2, 3].includes(3));    // true
console.log([1, 2, 3].includes(3, 3))  // false
console.log([1, 2, 3].includes(3, 2))  // true

五、排序方法

sort()

对数组的元素进行排序,并返回排序后的原数组。

Array.sort()参数语法:

  1. 第一个参数(可选):用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符串的ASCII码进行排序。该函数接收二个参数:
firstsecond
第一个用于比较的元素第二个用于比较的元素
// Array的sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。
const arr = [10, 20, 1, 2].sort()
//[1, 10, 2, 20]
console.log(arr);

可以接收一个比较函数作为参数,实现自定义的排序。该函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等返回0,如果第一个参数应该位于第二个之后则返回一个正数

const arr = [10, 20, 1, 2]
arr.sort((value1, value2) => {
  if (value1 < value2) {
    return -1
  }
  if (value1 > value2) {
    return 1
  }
  return 0
})
// [1, 2, 10, 20]
console.log(arr)

reverse()

将数组中元素的反转,并返回该数组。该方法会改变原数组。

const values = [1, 2, 3, 4, 5]
values.reverse()
//[5, 4, 3, 2, 1]
console.log(values)

六、 转换方法

toLocaleString()

toLocaleString() 返回一个字符串表示数组中的元素。数组中的元素将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串,并用逗号隔开。

const array1 = [1, 'a', { id: 1 }, new Date()]
// 1,a,[object Object],2020/1/15 上午7:50:38
console.log(array1.toLocaleString())

toString()

返回一个由逗号连接起来的字符串。

const array1 = [1, 'abc', { id: 1 }]
// 1,abc,[object Object]
console.log(array1.toString())

join()

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

Array.join()参数语法:

  1. 第一个参数(可选):指定一个字符串来分隔数组的每个元素。如果不传,默认数组元素用逗号(,)分隔。如果是空字符串(""),则所有元素之间都没有任何字符。
const arr = [1, 2, 3]
// 1,2,3
console.log(arr.join())
// 123
console.log(arr.join(''))
// 1+2+3
console.log(arr.join('+'))

七、 归并方法(迭代数组的所有项,然后构建一个最终返回的值)

reduce()

reduce()方法从数组的第一项开始,迭代数组的所有元素,构建一个最终返回的值,返回函数累计处理的结果。

Array.reduce()参数语法:

  1. 第一个参数(必填): callback执行数组中每个值的函数。该函数接收四个参数:
prevcurindexarray
初始值, 或者上一次调用回调函数返回的值(必填)当前元素值 (必填)当前元素的索引值(可选)数组对象本身(可选)

这个函数返回的任何值都会作为第一个参数自动传给下一项。

  1. 第二个参数(可选): initialValue作为第一次调用callback函数时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用reduce将报错。
 //Uncaught TypeError: Reduce of empty array with no initial value
 [].reduce(() => {})

const arr = ['L', 'O', 'V', 'E'].reduce((prev, cur) => {
  console.log('prev: ', prev)
  console.log('cur: ', cur)
  return prev + cur
})
// LOVE
console.log(arr)

第一次执行回调函数,prev 是 L,cur 是 O。第二次,prev 是 LO,cur 是 V(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果LOVE。

reduceRight()

reduceRight()reduce()作用类似,使用reduce()还是reduceRight(),主要取决于要从哪头开始遍历数组。除此之外,它们完全相同。

var values = [1,2,3,4,5]
var sum = values.reduceRight(function(prev, cur, index, array){
  return prev + cur
});
//15
alert(sum)

第一次执行回调函数,prev 是 5,cur 是 4。第二次,prev 是 9(5 加 4 的结果),cur 是 3(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果。

八、demo

实现由短划线分隔的单词变成骆驼式的

camelize("background-color") === 'backgroundColor'

function camelize(str) {
  return str
    .split('-') // my-long-word -> ['my', 'long', 'word']
    .map(
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    ) // ['my', 'long', 'word'] -> ['my', 'Long', 'Word']
    .join(''); // ['my', 'Long', 'Word'] -> myLongWord
}

数组去重

function unique(arr) {
  let result = [];
  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str)
    }
  }
  return result
}

在已有的数组上创建一个对象,id作为键,数组的每一项作为值。

let users = [
  { id: '111', name: "zhangsan", age: 20 },
  { id: '222', name: "lisi", age: 24 },
  { id: '333', name: "wangwu", age: 31 },
]
function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value
    return obj
  }, {})
}
/* 
  {
    111: { id: "111", name: "zhangsan", age: 20 },
    222: { id: "222", name: "lisi", age: 24 },
    333: { id: "333", name: "wangwu", age: 31 }
  } 
*/
console.log(groupById(users))

参考链接

JavaScript高级程序设计(第3版)

10 JavaScript array methods you should know

【干货】js 数组详细操作方法及解析合集

数组的扩展

MDN Array www.ecma-international.org/ecma-262/8.…

数组方法

数组方法实现

数组方法实现一般都不需要知道,只了解即可,这里可以参考【前端胖头鱼】的文章 juejin.cn/post/701276…

重要的几个方法

map的实现

Array.prototype.map = function(callbackFn, thisArg) {
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null or undefined");
  }
  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  // 草案中提到要先转换为对象
  let O = Object(this);
  let T = thisArg;

  
  let len = O.length >>> 0;
  let A = new Array(len);
  for(let k = 0; k < len; k++) {
    // 还记得原型链那一节提到的 in 吗?in 表示在原型链查找
    // 如果用 hasOwnProperty 是有问题的,它只能找私有属性
    if (k in O) {
      let kValue = O[k];
      // 依次传入this, 当前项,当前索引,整个数组
      let mappedValue = callbackfn.call(T, KValue, k, O);
      A[k] = mappedValue;
    }
  }
  return A;
}

length >>> 0, 字面意思是指"右移 0 位",但实际上是把前面的空位用0填充,这里的作用是保证len为数字且为整数。

可以看下几个常用的例子

null >>> 0  //0

undefined >>> 0  //0

void(0) >>> 0  //0

function a (){};  a >>> 0  //0

[] >>> 0  //0

var a = {}; a >>> 0  //0

123123 >>> 0  //123123

45.2 >>> 0  //45

0 >>> 0  //0

-0 >>> 0  //0

-1 >>> 0  //4294967295

-1212 >>> 0  //4294966084

reduce的实现

1、初始值不传怎么处理

2、回调函数的参数有哪些,返回值如何处理。

Array.prototype.reduce  = function(callbackfn, initialValue) {
  // 异常处理,和 map 一样
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'reduce' of null or undefined");
  }
  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  let accumulator = initialValue;
  if (accumulator === undefined) {
    for(; k < len ; k++) {
      // 查找原型链
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }
  // 表示数组全为空
  if(k === len && accumulator === undefined) 
    throw new Error('Each element of the array is empty');
  for(;k < len; k++) {
    if (k in O) {
      // 注意,核心!
      accumulator = callbackfn.call(undefined, accumulator, O[k], k, O);
    }
  }
  return accumulator;
}

其实是从最后一项开始遍历,通过原型链查找跳过空项。

push 和pop实现

Array.prototype.push = function(...items) {
  let O = Object(this);
  let len = this.length >>> 0;
  let argCount = items.length >>> 0;
  // 2 ** 53 - 1 为JS能表示的最大正整数
  if (len + argCount > 2 ** 53 - 1) {
    throw new TypeError("The number of array is over the max value restricted!")
  }
  for(let i = 0; i < argCount; i++) {
    O[len + i] = items[i];
  }
  let newLength = len + argCount;
  O.length = newLength;
  return newLength;
}
Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;
  }
  len --;
  let value = O[len];
  delete O[len];
  O.length = len;
  return value;
}

filter 实现

Array.prototype.filter = function(callbackfn, thisArg) {
  // 处理数组类型异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'filter' of null or undefined");
  }
  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let resLen = 0;
  let res = [];
  for(let i = 0; i < len; i++) {
    if (i in O) {
      let element = O[i];
      if (callbackfn.call(thisArg, O[i], i, O)) {
        res[resLen++] = element;
      }
    }
  }
  return res;
}

splice方法 实现

splice 可以说是最受欢迎的数组方法之一,api 灵活,使用方便。现在来梳理一下用法:

    1. splice(position, count) 表示从 position 索引的位置开始,删除count个元素
    1. splice(position, 0, ele1, ele2, ...) 表示从 position 索引的元素后面插入一系列的元素
    1. splice(postion, count, ele1, ele2, ...) 表示从 position 索引的位置开始,删除 count 个元素,然后再插入一系列的元素
    1. 返回值为被删除元素组成的数组

接下来我们实现这个方法。

image.png

初步实现

Array.prototype.splice = function(startIndex, deleteCount, ...addElements)  {
  let argumentsLen = arguments.length;
  let array = Object(this);
  let len = array.length;
  let deleteArr = new Array(deleteCount);
   
  // 拷贝删除的元素
  sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  // 移动删除元素后面的元素
  movePostElements(array, startIndex, len, deleteCount, addElements);
  // 插入新元素
  for (let i = 0; i < addElements.length; i++) {
    array[startIndex + i] = addElements[i];
  }
  array.length = len - deleteCount + addElements.length;
  return deleteArr;
}

先拷贝删除的元素,如下所示:

const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
  for (let i = 0; i < deleteCount; i++) {
    let index = startIndex + i;
    if (index in array) {
      let current = array[index];
      deleteArr[i] = current;
    }
  }
};

然后对删除元素后面的元素进行挪动, 挪动分为三种情况:

  1. 添加的元素和删除的元素个数相等
  2. 添加的元素个数小于删除的元素
  3. 添加的元素个数大于删除的元素

当两者相等时,

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  if (deleteCount === addElements.length) return;
}

当添加的元素个数小于删除的元素时, 如图所示:

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  //...
  // 如果添加的元素和删除的元素个数不相等,则移动后面的元素
  if(deleteCount > addElements.length) {
    // 删除的元素比新增的元素多,那么后面的元素整体向前挪动
    // 一共需要挪动 len - startIndex - deleteCount 个元素
    for (let i = startIndex + deleteCount; i < len; i++) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i - (deleteCount - addElements.length);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
    // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素
    // 目前长度为 len + addElements - deleteCount
    for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
      delete array[i];
    }
  } 
};

当添加的元素个数大于删除的元素时, 如图所示:

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  //...
  if(deleteCount < addElements.length) {
    // 删除的元素比新增的元素少,那么后面的元素整体向后挪动
    // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题?
    for (let i = len - 1; i >= startIndex + deleteCount; i--) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i + (addElements.length - deleteCount);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
  }
};

优化一: 参数的边界情况

当用户传来非法的 startIndex 和 deleteCount 或者负索引的时候,需要我们做出特殊的处理。

const computeStartIndex = (startIndex, len) => {
  // 处理索引负数的情况
  if (startIndex < 0) {
    return startIndex + len > 0 ? startIndex + len: 0;
  } 
  return startIndex >= len ? len: startIndex;
}

const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  // 删除数目没有传,默认删除startIndex及后面所有的
  if (argumentsLen === 1) 
    return len - startIndex;
  // 删除数目过小
  if (deleteCount < 0) 
    return 0;
  // 删除数目过大
  if (deleteCount > len - startIndex) 
    return len - startIndex;
  return deleteCount;
}

Array.prototype.splice = function (startIndex, deleteCount, ...addElements) {
  //,...
  let deleteArr = new Array(deleteCount);
  
  // 下面参数的清洗工作
  startIndex = computeStartIndex(startIndex, len);
  deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);
   
  // 拷贝删除的元素
  sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  //...
}

优化二: 数组为密封对象或冻结对象

什么是密封对象?

密封对象是不可扩展的对象,而且已有成员的[[Configurable]]属性被设置为false,这意味着不能添加、删除方法和属性。但是属性值是可以修改的。

什么是冻结对象?

冻结对象是最严格的防篡改级别,除了包含密封对象的限制外,还不能修改属性值。

接下来,我们来把这两种情况一一排除。

// 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
if (Object.isSealed(array) && deleteCount !== addElements.length) {
  throw new TypeError('the object is a sealed object!')
} else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
  throw new TypeError('the object is a frozen object!')
}

好了,现在就写了一个比较完整的splice,如下:

const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
  for (let i = 0; i < deleteCount; i++) {
    let index = startIndex + i;
    if (index in array) {
      let current = array[index];
      deleteArr[i] = current;
    }
  }
};

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  // 如果添加的元素和删除的元素个数相等,相当于元素的替换,数组长度不变,被删除元素后面的元素不需要挪动
  if (deleteCount === addElements.length) return;
  // 如果添加的元素和删除的元素个数不相等,则移动后面的元素
  else if(deleteCount > addElements.length) {
    // 删除的元素比新增的元素多,那么后面的元素整体向前挪动
    // 一共需要挪动 len - startIndex - deleteCount 个元素
    for (let i = startIndex + deleteCount; i < len; i++) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i - (deleteCount - addElements.length);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
    // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素
    // 目前长度为 len + addElements - deleteCount
    for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
      delete array[i];
    }
  } else if(deleteCount < addElements.length) {
    // 删除的元素比新增的元素少,那么后面的元素整体向后挪动
    // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题?
    for (let i = len - 1; i >= startIndex + deleteCount; i--) {
      let fromIndex = i;
      // 将要挪动到的目标位置
      let toIndex = i + (addElements.length - deleteCount);
      if (fromIndex in array) {
        array[toIndex] = array[fromIndex];
      } else {
        delete array[toIndex];
      }
    }
  }
};

const computeStartIndex = (startIndex, len) => {
  // 处理索引负数的情况
  if (startIndex < 0) {
    return startIndex + len > 0 ? startIndex + len: 0;
  } 
  return startIndex >= len ? len: startIndex;
}

const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  // 删除数目没有传,默认删除startIndex及后面所有的
  if (argumentsLen === 1) 
    return len - startIndex;
  // 删除数目过小
  if (deleteCount < 0) 
    return 0;
  // 删除数目过大
  if (deleteCount > len - startIndex) 
    return len - startIndex;
  return deleteCount;
}

Array.prototype.splice = function(startIndex, deleteCount, ...addElements)  {
  let argumentsLen = arguments.length;
  let array = Object(this);
  let len = array.length >>> 0;
  let deleteArr = new Array(deleteCount);

  startIndex = computeStartIndex(startIndex, len);
  deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);

  // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
  if (Object.isSealed(array) && deleteCount !== addElements.length) {
    throw new TypeError('the object is a sealed object!')
  } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
    throw new TypeError('the object is a frozen object!')
  }
   
  // 拷贝删除的元素
  sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  // 移动删除元素后面的元素
  movePostElements(array, startIndex, len, deleteCount, addElements);

  // 插入新元素
  for (let i = 0; i < addElements.length; i++) {
    array[startIndex + i] = addElements[i];
  }

  array.length = len - deleteCount + addElements.length;

  return deleteArr;
}

sort 实现

sort 方法在 V8 内部相对于其他方法而言是一个比较高深的算法,对于很多边界情况做了反复的优化,但是这里我们不会直接拿源码来干讲。我们会来根据源码的思路,实现一个 跟引擎性能一样的排序算法

V8 引擎的思路分析

首先大概梳理一下源码中排序的思路:

设要排序的元素个数是n:

  • 当 n <= 10 时,采用插入排序

  • 当 n > 10 时,采用三路快速排序

    • 10 < n <= 1000, 采用中位数作为哨兵元素
    • n > 1000, 每隔 200~215 个元素挑出一个元素,放到一个新数组,然后对它排序,找到中间位置的数,以此作为中位数

在动手之前,我觉得我们有必要为什么这么做搞清楚。

第一、为什么元素个数少的时候要采用插入排序?

虽然插入排序理论上说是O(n^2)的算法,快速排序是一个O(nlogn)级别的算法。但是别忘了,这只是理论上的估算,在实际情况中两者的算法复杂度前面都会有一个系数的, 当 n 足够小的时候,快速排序nlogn的优势会越来越小,倘若插入排序O(n^2)前面的系数足够小,那么就会超过快排。而事实上正是如此,插入排序经过优化以后对于小数据集的排序会有非常优越的性能,很多时候甚至会超过快排。

因此,对于很小的数据量,应用插入排序是一个非常不错的选择。

第二、为什么要花这么大的力气选择哨兵元素?

因为快速排序的性能瓶颈在于递归的深度,最坏的情况是每次的哨兵都是最小元素或者最大元素,那么进行partition(一边是小于哨兵的元素,另一边是大于哨兵的元素)时,就会有一边是空的,那么这么排下去,递归的层数就达到了n, 而每一层的复杂度是O(n),因此快排这时候会退化成O(n^2)级别。

这种情况是要尽力避免的!如果来避免?

就是让哨兵元素进可能地处于数组的中间位置,让最大或者最小的情况尽可能少。这时候,你就能理解 V8 里面所做的种种优化了。

插入排序及优化

最初的插入排序可能是这样写的:

const insertSort = (arr, start = 0, end) => {
  end = end || arr.length;
  for(let i = start; i < end; i++) {
    let j;
    for(j = i; j > start && arr[j - 1] > arr[j]; j --) {
      let temp = arr[j];
      arr[j] = arr[j - 1];
      arr[j - 1] = temp;
    }
  }
  return;
}

看似可以正确的完成排序,但实际上交换元素会有相当大的性能消耗,我们完全可以用变量覆盖的方式来完成,如图所示:

优化后代码如下:

const insertSort = (arr, start = 0, end) => {
  end = end || arr.length;
  for(let i = start; i < end; i++) {
    let e = arr[i];
    let j;
    for(j = i; j > start && arr[j - 1] > e; j --)
      arr[j] = arr[j-1];
    arr[j] = e;
  }
  return;
}

接下来正式进入到 sort 方法。

找哨兵元素

sort的骨架大致如下:

Array.prototype.sort = (comparefn) => {
  let array = Object(this);
  let length = array.length >>> 0;
  return InnerArraySort(array, length, comparefn);
}

const InnerArraySort = (array, length, comparefn) => {
  // 比较函数未传入
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    comparefn = function (x, y) {
      if (x === y) return 0;
      x = x.toString();
      y = y.toString();
      if (x == y) return 0;
      else return x < y ? -1 : 1;
    };
  }
  const insertSort = () => {
    //...
  }
  const getThirdIndex = (a, from, to) => {
    // 元素个数大于1000时寻找哨兵元素
  }
  const quickSort = (a, from, to) => {
    //哨兵位置
    let thirdIndex = 0;
    while(true) {
      if(to - from <= 10) {
        insertSort(a, from, to);
        return;
      }
      if(to - from > 1000) {
        thirdIndex = getThirdIndex(a, from , to);
      }else {
        // 小于1000 直接取中点
        thirdIndex = from + ((to - from) >> 2);
      }
    }
    //下面开始快排
  }
}

我们先来把求取哨兵位置的代码实现一下:

const getThirdIndex = (a, from, to) => {
  let tmpArr = [];
  // 递增量,200~215 之间,因为任何正数和15做与操作,不会超过15,当然是大于0的
  let increment = 200 + ((to - from) & 15);
  let j = 0;
  from += 1;
  to -= 1;
  for (let i = from; i < to; i += increment) {
    tmpArr[j] = [i, a[i]];
    j++;
  }
  // 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
  tmpArr.sort(function(a, b) {
    return comparefn(a[1], b[1]);
  });
  let thirdIndex = tmpArr[tmpArr.length >> 1][0];
  return thirdIndex;
}

完成快排

接下来我们来完成快排的具体代码:

const _sort = (a, b, c) => {
  let arr = [a, b, c];
  insertSort(arr, 0, 3);
  return arr;
}

const quickSort = (a, from, to) => {
  //...
  // 上面我们拿到了thirdIndex
  // 现在我们拥有三个元素,from, thirdIndex, to
  // 为了再次确保 thirdIndex 不是最值,把这三个值排序
  [a[from], a[thirdIndex], a[to - 1]] = _sort(a[from], a[thirdIndex], a[to - 1]);
  // 现在正式把 thirdIndex 作为哨兵
  let pivot = a[thirdIndex];
  // 正式进入快排
  let lowEnd = from + 1;
  let highStart = to - 1;
  // 现在正式把 thirdIndex 作为哨兵, 并且lowEnd和thirdIndex交换
  let pivot = a[thirdIndex];
  a[thirdIndex] = a[lowEnd];
  a[lowEnd] = pivot;
  
  // [lowEnd, i)的元素是和pivot相等的
  // [i, highStart) 的元素是需要处理的
  for(let i = lowEnd + 1; i < highStart; i++) {
    let element = a[i];
    let order = comparefn(element, pivot);
    if (order < 0) {
      a[i] = a[lowEnd];
      a[lowEnd] = element;
      lowEnd++;
    } else if(order > 0) {
      do{
        highStart--;
        if(highStart === i) break;
        order = comparefn(a[highStart], pivot);
      }while(order > 0)
      // 现在 a[highStart] <= pivot
      // a[i] > pivot
      // 两者交换
      a[i] = a[highStart];
      a[highStart] = element;
      if(order < 0) {
        // a[i] 和 a[lowEnd] 交换
        element = a[i];
        a[i] = a[lowEnd];
        a[lowEnd] = element;
        lowEnd++;
      }
    }
  }
  // 永远切分大区间
  if (lowEnd - from > to - highStart) {
    // 继续切分lowEnd ~ from 这个区间
    to = lowEnd;
    // 单独处理小区间
    quickSort(a, highStart, to);
  } else if(lowEnd - from <= to - highStart) {
    from = highStart;
    quickSort(a, from, lowEnd);
  }
}