Skip to content
大纲

实现一些 Array 方法

  • arrayLike

Array 内置函数( 🟡 会改变原数组)

大部分参数都是 xxx(callbackFn, ?thisArg)

  • 原型方法
    • at(index)
    • concat(value0, value1, /*...*/ valueN)
    • copyWithin(target, start, ?end) 🟡
    • entries()
    • every
    • fill(value, ?start, ?end) 🟡
    • filter
    • find
    • findIndex
    • findLast
    • findLastIndex
    • flat(?depth)
    • flatMap
    • forEach
    • group
    • groupToMap
    • includes(searchElement, ?fromIndex)
    • indexOf(searchElement, ?fromIndex)
    • join(?separator)
    • keys()
    • lastIndexOf(searchElement, ?fromIndex)
    • map
    • pop() 🟡
    • push(...items) 🟡
    • reduce(callbackFn, initialValue)
    • reduceRight(callbackFn, initialValue)
    • reverse() 🟡
    • shift() 🟡
    • slice(?start, ?end)
    • some
    • sort(?compareFn) 🟡
    • splice(start, ?deleteCount, ...items) 🟡
    • toLocaleString()
    • toString()
    • unshift(...items) 🟡
    • values()
    • with(index, value) ES2023
    • toReversed() ES2023
    • toSorted() ES2023
    • toSpliced() ES2023
  • 静态方法
    • from(arrayLike, mapFn, ?thisArg)
    • fromAsync(arrayLike, mapFn, ?thisArg)
    • isArray(value)
    • of(...items)

类数组

js
// 类数组转化为数组

let arrayLike = {
  0: 'tom',
  1: '65',
  2: '',
  3: ['jane', 'john', 'Mary'],
  length: 4
}

// 1. [].slice
console.log([].slice.call(arrayLike))

// 2. Array.from
console.log(Array.from(arrayLike))

// 3. Array.apply
console.log(Array.apply(null, arrayLike))

// 4. [].concat
// 这个有点问题
console.log([].concat.apply([], arrayLike))

// 5. [...arr]
console.log([...arrayLike])

如何把一个数组随机打乱

第一个想到的就是随机排序

js
// 如何把一个数组随机打乱

// 方案一
// 使用原生实现,Math.rondom() - 0.5 有时大于 0,有时小于 0 会达成这样的效果
;[1, 2, 3, 4].sort((x, y) => Math.random() - 0.5)

// 方案二
// 借用 lodash 可更方便
_.shuffle([1, 2, 3, 4])
//-> [3, 2, 4, 1]

创建一个数组,并填充值

比如创建一个数组大小为 100,每个值都为 0 的数组

点我查看详细
js
// 方法一:
Array(100).fill(0)

// 方法二:
// 注:如果直接使用 map,会出现稀疏数组
Array.from(Array(100), (x) => 0)

// 方法二变体:
Array.from({ length: 100 }, (x) => 0)

如果要求填充自然数呢?

点我查看详细
js
// 方法一
let i = 0;
new Array(n).fill(i++)

// 方法二
Array.from({ length: n }, (v, i) => i)

// 方法三
[...Array(n).keys()]

// 方法四
// lodash 底层通过 new Array 和 while 循环实现
_.times(n)

// 方案五
function* generateNaturalNumber(n) {
  let i = 0;
  while(i <= n) {
    yield i;
    i++;
  }
}
// 使用
[...generateNaturalNumber(100)]

// 方案六, 自定义迭代器
function createNaturalNumber(n) {
  var i = 0;
  return {
    next: function next() {
      return {
        done: i >= n,
        value: i++,
      }
    }
  };
}
const myNaturalNumber = {
  [Symbol.iterator]: function() {
    return createNaturalNumber(100)
  }
};

// 使用
[...myNaturalNumber]
// 或
for(var i of myNaturalNumber) {
  console.log(i)
}
// 或
Array.from(myNaturalNumber)

实现数组的方法

手写数组 forEach 方法

手写之前,分析一位同学的如下写法,一直拿不到接口数据,是为什么?

js
const data = []
const urls = ['api1', 'api2', 'api3']
urls.forEach(async (url) => {
  const res = await request(url)
  data.push(res)
})

// 一直是空数组
console.log(data)

手写 forEach 方法,从 forEach 原理再看上面问题的所在?

js
Array.prototype.myForEach = function (fn, thisValue = []) {
  for (let i = 0; i < this.length; i++) {
    fn(this[i], i, this)
  }
}

forEach 的问题

  • 无法中断
  • 无法进行异步排队

手写数组 filter 方法

js
Array.prototype.myFilter = function (fn, thisValue = []) {
  let res = []
  for (let i = 0; i < this.length; i++) {
    if (fn(this[i], i, this)) {
      res.push(this[i])
    }
  }
  return res
}

手写数组 map 方法

js
Array.prototype.myMap = function (fn, thisValue = []) {
  let arr = this
  const len = arr.length
  let res = new Array(len)

  for (let i = 0; i < len; i++) {
    if (typeof arr[i] !== 'undefined') {
      res[i] = fn(arr[i], i, arr)
    } else {
      // 对于空槽,赋值就变成 undefined 了
      // 可通过 new Array(len) 生成带空槽的数组来处理此问题,
      // 但这样如果值本身是 undefined 就又有问题了
      res[i] = arr[i]
    }
  }

  return res
}

// testing
let arr = [1, 2, , 3]
console.log(arr, arr.length)
console.log(arr.myMap((v) => v * 2))
console.log(arr.map((v) => v * 2))
  • 稀疏数组在使用 map() 方法后仍然是稀疏的。空槽的索引在返回的数组中仍然为空,并且回调函数不会对它们进行调用。
  • 空槽与未定义的值不同,最重要的区别是空槽不可枚举。

手写数组 reduce 方法

js
function myReduce(arr, fn, initValue) {
  var num = initValue === undefined ? (num = arr[0]) : initValue
  var i = initValue === undefined ? 1 : 0

  for (i; i < arr.length; i++) {
    if (typeof arr[i] !== 'undefined') {
      num = fn(num, arr[i], i)
    }
  }

  return num
}

// testing
let arr = [1, 2, , [3, 4, [5, [6]]]]

console.log(myReduce(arr, (v, b) => [v].concat(b), []))
console.log(
  arr.reduce((v, b) => {
    console.log(b)
    return [v].concat(b)
  }, [])
)
  • reduce() 会过滤掉空数组

如何把一个数组 Array 转化为迭代器 Iterable

js
const list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const it = list[Symbol.iterator]()

it.next()

实现扁平化 flatten

实现 flatten 模拟 Array.prototype.flat,默认展开一层,可传递参数用以展开多层

数组降维

js
// ES2019 之前,可通过 reduce + concat 实现
// Array.prototype.concat 既可以连接数组又可以连接单项,十分巧妙

// 简单
// 方案一
const flatten = (list) => list.reduce((a, b) => a.concat(b), [])

// 方案二
const flatten = (list) => [].concat(...list)

// 深层数组打平
function flatten(list, depth = 1) {
  if (depth === 0) return list
  return list.reduce(
    (a, b) => a.concat(Array.isArray(b) ? flatten(b, depth - 1) : b),
    []
  )
}

// 使用迭代器实现
const flatten = function (target, depth = 1) {
  const copy = [...target]
  for (let i = 0; i < depth; ++i) {
    const iter = copy[Symbol.iterator]()
    let item = null
    for (item = iter.next(); !item.done; ) {
      // 注意:迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是用游标来记录遍历可迭代对象的历程,
      // 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
      if (Array.isArray(item.value)) {
        const temp = [...item.value]
        let size = temp.length
        for (let j = 0; j < size; ++j) {
          item = iter.next()
        }
        copy.splice(copy.indexOf(item.value), 1, ...temp)
      } else {
        item = iter.next()
      }
    }
  }
  return copy
}

// 基于递归实现,不用 Array.concat
Array.prototype.myFlat = function (this: any[], depth: number = 1) {
  const myFlat = (
    arr: any[],
    flatLength = 1,
    resultArray = [] as any[],
    forEachCount = 0
  ) => {
    arr.forEach((d: any) => {
      if (
        Array.isArray(d) &&
        (flatLength === -1 || forEachCount < flatLength)
      ) {
        myFlat(d, flatLength, resultArray, forEachCount + 1);
      } else {
        resultArray.push(d);
      }
    });

    return resultArray;
  };

  return myFlat(this, depth);
};

ES6 flat

js
let arr = [1, 2, [3, 4, [5, [6]]]]
console.log(arr.flat(Infinity))
// flat 参数为指定要提取嵌套数组的结构深度,默认值为 1
  • flat() 默认深度为 1
  • flat() 方法会移除数组中的空项 empty(稀疏数组)

使用 reduce 实现 flat

常见的递归版本

js
// 考虑了深度控制
const myFlat = function flat(arr, depth = 1) {
  return arr.reduce((prev, cur) => {
    let temp = cur
    if (Array.isArray(cur)) {
      temp = depth > 1 ? flat(cur, depth - 1) : cur
    }
    return prev.concat(Array.isArray(cur) ? temp : cur)
  }, [])
}

let arr = [1, 2, , [3, 4, [5, [6]]]]

console.log(arr)
console.log(myFlat(arr, 2))
console.log(arr.flat(2))

console.log(myFlat(arr, Infinity))

console.log([1, 2].concat(3, [4, 5]))

concat(valueN) 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

concat 的参数 valueN 为数组和/或值,将被合并到一个新的数组中。如果省略了所有 valueN 参数,则 concat 会返回调用此方法的现存数组的一个浅拷贝。

扩展思考

能用迭代的思路去实现吗?

点我查看详细
js
function flatter(arr) {
  if (!arr.length) return arr
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr)
  }
  return arr
}

实现 flatMap

flatMap() 方法对数组中的每个元素应用给定的回调函数,然后将结果展开一级,返回一个新数组。它等价于在调用 map() 方法后再调用深度为 1 的 flat() 方法(arr.map(...args).flat()),但比分别调用这两个方法稍微更高效一些。

注意:先 map 后 flat(1)

js
// 先 map 后 flat()
function myFlatMap(fn) {
  let target = this
  return target.map((i) => fn(i)).flat()
}

Array.prototype._flatMap = function (callback) {
  let newArr = [...this]
  let result = []
  for (let i = 0; i < newArr.length; i++) {
    result.push(callback(newArr[i], i, newArr))
  }
  return result.flat()
}

参考