JavaScript 實現數組更多的高階函數

JavaScript 實現數組更多的高階函數

場景

雖說人人平等,但有些人更加平等。

爲什麼有了 Lodash 這種通用函數工具庫,吾輩要寫這篇文章呢?吾輩在 SegmentFault 上經常看到關於 JavaScript 數組的相關疑問,甚至於,相同類型的問題,只是數據變化了一些,就直接提出了一個新的問題(實際上,對自身並無幫助)。簡單搜索了一下 Array,居然有 2360+ 條的結果,足可見這類問題的頻率之高。若是有一篇適合 JavaScript 萌新閱讀的自己實現數組更多操作的文章,情況是否會發生變化呢?

下面吾輩便來實現以下幾種常見的操作

  • uniqueBy: 去重
  • sortBy: 排序
  • filterItems: 過濾掉一些元素
  • diffBy: 差異
  • groupBy: 分組
  • 遞歸操作
前言:
你至少需要了解 ES6 的一些特性你才能愉快的閱讀

uniqueBy: 去重

相關問題

/**
 * js 的數組去重方法
 * @param arr 要進行去重的數組
 * @param kFn 唯一標識元素的方法,默認使用 {@link returnItself}
 * @returns 進行去重操作之後得到的新的數組 (原數組並未改變)
 */
function uniqueBy(arr, kFn = val => val) {
  const set = new Set()
  return arr.filter((v, ...args) => {
    const k = kFn(v, ...args)
    if (set.has(k)) {
      return false
    }
    set.add(k)
    return true
  })
}

使用

console.log(uniqueBy([1, 2, 3, '1', '2'])) // [ 1, 2, 3, '1', '2' ]
console.log(uniqueBy([1, 2, 3, '1', '2'], i => i + '')) // [ 1, 2, 3 ]

sortBy: 排序

相關問題

/**
 * 快速根據指定函數對數組進行排序
 * 注: 使用遞歸實現,對於超大數組(其實前端的數組不可能特別大吧?#笑)可能造成堆棧溢出
 * @param arr 需要排序的數組
 * @param kFn 對數組中每個元素都產生可比較的值的函數,默認返回自身進行比較
 * @returns 排序後的新數組
 */
function sortBy(arr, kFn = v => v) {
  // TODO 此處爲了讓 typedoc 能生成文檔而不得不加上類型
  const newArr = arr.map((v, i) => [v, i])
  function _sort(arr, fn) {
    // 邊界條件,如果傳入數組的值
    if (arr.length <= 1) {
      return arr
    }
    // 根據中間值對數組分治爲兩個數組
    const medianIndex = Math.floor(arr.length / 2)
    const medianValue = arr[medianIndex]
    const left = []
    const right = []
    for (let i = 0, len = arr.length; i < len; i++) {
      if (i === medianIndex) {
        continue
      }
      const v = arr[i]
      if (fn(v, medianValue) <= 0) {
        left.push(v)
      } else {
        right.push(v)
      }
    }
    return _sort(left, fn)
      .concat([medianValue])
      .concat(_sort(right, fn))
  }
  return _sort(newArr, ([t1, i1], [t2, i2]) => {
    const k1 = kFn(t1, i1, arr)
    const k2 = kFn(t2, i2, arr)
    if (k1 === k2) {
      return 0
    } else if (k1 < k2) {
      return -1
    } else {
      return 1
    }
  }).map(([_v, i]) => arr[i])
}

使用

console.log(sortBy([1, 3, 5, 2, 4])) // [ 1, 2, 3, 4, 5 ]
console.log(sortBy([1, 3, 5, '2', '4'])) // [ 1, '2', 3, '4', 5 ]
console.log(sortBy([1, 3, 5, '2', '4'], i => -i)) // [ 5, '4', 3, '2', 1 ]

filterItems: 過濾掉一些元素

相關問題

/**
 * 從數組中移除指定的元素
 * 注: 時間複雜度爲 1~3On
 * @param arr 需要被過濾的數組
 * @param deleteItems 要過濾的元素數組
 * @param kFn 每個元素的唯一鍵函數
 */
function filterItems(arr, deleteItems, kFn = v => v) {
  const kSet = new Set(deleteItems.map(kFn))
  return arr.filter((v, i, arr) => !kSet.has(kFn(v, i, arr)))
}

使用

console.log(filterItems([1, 2, 3, 4, 5], [1, 2, 0])) // [ 3, 4, 5 ]
console.log(filterItems([1, 2, 3, 4, 5], ['1', '2'], i => i + '')) // [ 3, 4, 5 ]

diffBy: 差異

相關問題

/**
 * 比較兩個數組的差異
 * @param left 第一個數組
 * @param right 第二個數組
 * @param kFn 每個元素的唯一標識產生函數
 * @returns 比較的差異結果
 */
function diffBy(left, right, kFn = v => v) {
  // 首先得到兩個 kSet 集合用於過濾
  const kThanSet = new Set(left.map(kFn))
  const kThatSet = new Set(right.map(kFn))
  const leftUnique = left.filter((v, ...args) => !kThatSet.has(kFn(v, ...args)))
  const rightUnique = right.filter(
    (v, ...args) => !kThanSet.has(kFn(v, ...args)),
  )
  const kLeftSet = new Set(leftUnique.map(kFn))
  const common = left.filter((v, ...args) => !kLeftSet.has(kFn(v, ...args)))
  return { left: leftUnique, right: rightUnique, common }
}

使用

console.log(diffBy([1, 2, 3], [2, 3, 4])) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4])) // { left: [ 1, 2 ], right: [ '2', 4 ], common: [ 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4], i => i + '')) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }

groupBy: 分組

相關問題

/**
 * js 數組按照某個條件進行分組
 *
 * @param arr 要進行分組的數組
 * @param kFn 元素分組的唯一標識函數
 * @param vFn 元素分組的值處理的函數。第一個參數是累計值,第二個參數是當前正在迭代的元素,如果你使用過 {@link Array#reduce} 函數的話應該對此很熟悉
 * @param init 每個分組的產生初始值的函數。類似於 reduce 的初始值,但它是一個函數,避免初始值在所有分組中進行累加。
 * @returns 元素標識 => 數組映射 Map
 */
function groupBy(
  arr,
  kFn = v => v,
  /**
   * 默認的值處理函數
   * @param res 最終 V 集合
   * @param item 當前迭代的元素
   * @returns 將當前元素合併後的最終 V 集合
   */
  vFn = (res, item) => {
    res.push(item)
    return res
  },
  init = () => [],
) {
  // 將元素按照分組條件進行分組得到一個 條件 -> 數組 的對象
  return arr.reduce((res, item, index, arr) => {
    const k = kFn(item, index, arr)
    // 如果已經有這個鍵了就直接追加, 否則先將之初始化再追加元素
    if (!res.has(k)) {
      res.set(k, init())
    }
    res.set(k, vFn(res.get(k), item, index, arr))
    return res
  }, new Map())
}

使用

console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], i => i)) // Map { 1 => [ 1 ],  2 => [ 2, 2, 2 ],  4 => [ 4, 4 ],  5 => [ 5, 5 ],  6 => [ 6 ] }
console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], i => i % 2 === 0)) // Map { false => [ 1, 5, 5 ], true => [ 2, 2, 2, 4, 4, 6 ] }
console.log(
  groupBy(
    [1, 2, 2, 2, 4, 4, 5, 5, 6],
    i => i % 2 === 0,
    (res, i) => res.add(i),
    () => new Set(),
  ),
) // Map { false => Set { 1, 5 }, true => Set { 2, 4, 6 } }

arrayToMap: 轉換爲 Map

相關問題

/**
 * 將數組映射爲 Map
 * @param arr 數組
 * @param k 產生 Map 元素唯一標識的函數,或者對象元素中的一個屬性名
 * @param v 產生 Map 值的函數,默認爲返回數組的元素,或者對象元素中的一個屬性名
 * @returns 映射產生的 map 集合
 */
export function arrayToMap(arr, k, v = val => val) {
  const kFn = k instanceof Function ? k : item => Reflect.get(item, k)
  const vFn = v instanceof Function ? v : item => Reflect.get(item, v)
  return arr.reduce(
    (res, item, index, arr) =>
      res.set(kFn(item, index, arr), vFn(item, index, arr)),
    new Map(),
  )
}

使用

const county_list = [
  {
    id: 1,
    code: '110101',
    name: '東城區',
    citycode: '110100',
  },
  {
    id: 2,
    code: '110102',
    name: '西城區',
    citycode: '110100',
  },
  {
    id: 3,
    code: '110103',
    name: '崇文區',
    citycode: '110100',
  },
]
console.log(arrayToMap(county_list, 'code', 'name')) // Map { '110101' => '東城區', '110102' => '西城區', '110103' => '崇文區' }
console.log(arrayToMap(county_list, ({ code }) => code, ({ name }) => name)) // Map { '110101' => '東城區', '110102' => '西城區', '110103' => '崇文區' }

遞歸

相關問題

以上種種操作皆是對一層數組進行操作,如果我們想對嵌套數組進行操作呢?例如上面這兩個問題?其實問題是類似的,只是遞歸遍歷數組而已。

/**
 * js 的數組遞歸去重方法
 * @param arr 要進行去重的數組
 * @param kFn 唯一標識元素的方法,默認使用 {@link returnItself},只對非數組元素生效
 * @returns 進行去重操作之後得到的新的數組 (原數組並未改變)
 */
function deepUniqueBy(arr, kFn = val => val) {
  const set = new Set()
  return arr.reduce((res, v, i, arr) => {
    if (Array.isArray(v)) {
      res.push(deepUniqueBy(v))
      return res
    }
    const k = kFn(v, i, arr)
    if (!set.has(k)) {
      set.add(k)
      res.push(v)
    }
    return res
  }, [])
}

使用

const testArr = [
  1,
  1,
  3,
  'hello',
  [3, 4, 4, 'hello', '5', [5, 5, ['a', 'r']]],
  {
    key: 'test',
  },
  4,
  [3, 0, 2, 3],
]
console.log(deepUniqueBy(testArr)) // [ 1,  3,  'hello',  [ 3, 4, 'hello', '5', [ 5, [Object] ] ],  { key: 'test' },  4,  [ 3, 0, 2 ] ]

反例

事實上,目前 SegmentFault 上存在着大量低質量且重複的問題及回答,關於這點確實比不上 StackOverflow。下面是兩個例子,可以看一下能否發現什麼問題

事實上,不管是問題還是答案,都沒有突出核心 -- Array 映射爲 Map/Array 分組,而且這種問題和答案還層出不窮。如果對 Array 的 API 都沒有看過一遍就來詢問的話,對於幫助者來說卻是太失禮了!

總結

JavaScript 對函數式編程支持很好,所以習慣高階函數於我們而言是一件好事,將問題的本質抽離出來,而不是每次都侷限於某個具體的問題上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章