面試官: 你爲何這麼強,什麼都敢盤(reduce)

關於遍歷,只要具有可遍歷結構,均可以使用reduce解決,無論是數組、字符串、對象、set、map前端

1. 用reduce實現數組一些api

給數組prototype加上基於reduce實現的api:node

Object.assign(Array.prototype, {
  myMap(cb, _this = this) {
    return this.reduce((res, cur, index, array) => [...res, cb.call(_this, cur, index, array)], []);
  },
  myFind(cb, _this = this) {
    return this.reduce((res, cur, index, array) => res || (cb.call(_this, cur, index, array) ? cur : undefined), undefined)
  },
  myFilter(cb, _this = this) {
    return this.reduce((res, cur, index, array) => [...res, ...(cb.call(_this, cur, index, array) ? [cur] : [])], []);
  },
  myEvery(cb, _this = this) {
    return this.reduce((res, cur, index, array) => res && !!cb.call(_this, cur, index, array), true);
  },
  mySome(cb, _this = this) {
    return this.reduce((res, cur, index, array) => res || !!cb.call(_this, cur, index, array), false);
  },
});
複製代碼

接下來寫測試用例:git

// 函數用例
const tests = {
  map: [
    item => item * 2,
    function(_, index) { return this[index] } // 這this是專門測cb傳入第二個參數使用的
  ],
  find: [
    item => item,
    item => item === 6,
    item => item === Symbol(),
    function(_, index) { return this[index] === 6 }
  ],
  filter: [
    item => item > 6,
    item => item,
    function(_, index) { return this[index] > 6 }
  ],
  every: [
    item => item,
    item => item > 6,
    function(_, index) { return this[index] > 6 }
  ],
  some: [
    item => item,
    item => item > 6,
    function(_, index) { return this[index] > 6 }
  ],
}

// 數據源
const example = [
  [1,2,3,4,5,6,7],
  [1,2,3,4,5],
  [11,12,13,14,15],
];
複製代碼

測試用例考慮普通狀況以及第二個改變this的參數的狀況,最後須要一個用例執行的方法:github

// 簡單的比較相等
function isEqual(a, b) {
  if (typeof a !== 'object' && typeof b !== 'object') {
    return a === b
  }
  // 這是測試[1, 2, 3]和[1, 2, 3]用的
  // 本文只有number和number[]沒有其餘數據結構
  return `${a}` === `${b}`;
}

function doTest(example, tests) {
  // 以數據源爲key,數組的isEqual是經過隱式轉換比較
  return example.reduce((res, cur) => {
  // 對函數用例逐個執行,把有沒有相等的true和false寫進去
    res[cur] = Object.entries(tests).reduce((result, [key, fns]) => {
      result[key] = fns.map(fn =>
        example.map(eg =>
          isEqual(
            eg[key](fn, [5, 6, 7]),
            eg[`my${key[0].toUpperCase()}${key.slice(1)}`](fn, [5, 6, 7])
            )
        ));
      return result;
    }, {});
    return res;
  }, {});
}

doTest(example, tests)
// 若是所有都是true,說明測試經過
複製代碼

2. 不是數組怎麼reduce

上面的測試也用了reduce,是對一個對象reduce。只要是遍歷某個數據結構,產生一個結果,那麼均可以使用reduce解決:npm

  • 普通對象:使用Object.keys,Object.values,Object.entries再reduce
  • 類數組對象:使用[...o]
  • 字符串: [].reduce.call(string, (res, cur) => {}, result)
  • 假數組: 如{ 0: 'a', 1: 'b', length: 2 },使用Array.from(o)、Array.apply(null, o)
  • 有symbol作key的對象:使用getOwnPropertySymbols

下面先來幾個最簡單的例子,但願平時基本沒用reduce的人,能夠經過幾個例子找到一點reduce的感受。reduce能夠簡化代碼,讓思路更加清晰,而不是被for循環的下標迷惑了本身json

根據對象生成一個簡單schema:api

// value值變成對應的type,若是是對象,則遞歸下一級
function transformSchema(o) {
  return Object.entries(o).reduce((res, [key, value]) => {
    res[key] = typeof value !== 'object' ? typeof value : transformSchema(value);
    return res;
  }, Array.isArray(o) ? [] : {});
}

transformSchema({ a: 1, b: '2', c: { d: 1, e: [{a: 1, b:2}]} })
複製代碼

統計頁面上a標籤的個數數組

[...document.querySelectorAll('*')]
  .reduce((sum, node) => node.nodeName === 'A' ? sum : sum + 1, 0)
複製代碼

統計字符串每個字符出現次數:數據結構

;[].reduce.call('asfsdhvui3u2498rfrvh 93c 293ur0jvdf', (res, cur) => {
  res[cur] = res[cur] || 0;
  res[cur] ++;
  return res;
}, {})
複製代碼

扁平化數組(不用flat和join)app

function flattern(arr) {
  return arr.reduce((res, cur) => 
    res.concat(Array.isArray(cur) ? flattern(cur) : [cur]),
  []);
}
複製代碼

數組去重,兼容各類類型,比較完美的版本:

function isNotSimple(o) {
  return Object.prototype.toString.call(o) === '[object Object]' || Array.isArray(o) || typeof o === 'function'
}

function deepEqual(a = {}, b = {}, cache = new Set()) {
  if (typeof a === 'function') { // 函數的狀況
    return a.toString() === b.toString()
  }
  if (cache.has(a)) { // 解決環引用
    return a === b
  }
  cache.add(a)
  const keys = Object.keys(a)
  const symbolKeys = Object.getOwnPropertySymbols(a) // 考慮symbol作key
  return (keys.length === Object.keys(b).length &&
    symbolKeys.length === Object.getOwnPropertySymbols(b).length) &&
    [...keys, ...symbolKeys].every(key => !isNotSimple(a[key]) ?
      a[key] === b[key] : deepEqual(a[key], b[key], cache))
}

function unique(arr) {
  const cache = new Set() // set能夠幹掉NaN
  const objCache = []
  // 簡單的基本類型直接來,複雜的使用deepEqual
  return arr.reduce((res, cur) => (
    !isNotSimple(cur) ? !cache.has(cur) && res.push(cur) && cache.add(cur)
      : !objCache.find(o => deepEqual(o, cur)) && objCache.push(cur) && res.push(cur),
    res
  ), []);
}
複製代碼

將傳入的全部參數生成一個單鏈表:

function createLinkList(...init) {
  let current
  return init.reduce((res, cur) => {
    current = current || res
    current.value = cur
    current.next = current.next || {}
    current = current.next
    return res
  }, {})
}
createLinkList(1,2,4,5,6);
複製代碼

建立一個樹形結構:

const ran = () => ~~(Math.random() * 2) + 1
function createTree(dept = 0) {
  if (dept > 1) {
    return null;
  }
  // 若是每一層是數組型的樹結構,用map也能夠
  // reduce還能夠兼容非數組的結構,還能夠完成其餘更復雜的需求
  return Array.apply(null, { length: ran() }).reduce((res, cur, i) => {
    res[i] = {
      value: ran(),
      nodes: createTree(dept + 1),
    }
    return res;
  }, {});
}
const tree = createTree();
複製代碼

基於上面的樹結構,找出某個節點值的出現次數:

// 若是當前節點值等於target,則+1;若是有子節點,則帶上sum遞歸計算
function targetFromTree(tree = {}, target, sum = 0) {
  return Object.values(tree).reduce((res, node) => 
    res + ~~(node.value === target) + targetFromTree(node.nodes, target, sum)
  , sum);
}
複製代碼

3. compose思想

對於數組api,常常有鏈式操做,如:

[1,2,3,4,5].filter(x => x > 3).map(x => x * 2)
複製代碼

這樣子,對每個元素filter一下,遍歷一次。對每個元素map,再遍歷一次。其實這一切咱們能夠作到只遍歷一次就完成兩個操做,遍歷的時候對每個元素作全部的函數複合起來的一個總函數的操做

class MagicArray extends Array {
  temp = []; // 存放鏈式操做的方法

  FLAG = Symbol(); // filter標記

  // 若是有filter標記則直接返回
  myMap(cb, _this = this) {
    this.temp.push((cur, index, array) => cur === this.FLAG ? this.FLAG : cb.call(_this, cur, index, array));
    return this;
  }

  // 不符合要求的打上filter標記
  myFilter(cb, _this = this) {
    this.temp.push((cur, index, array) => cb.call(_this, cur, index, array) ? cur : this.FLAG);
    return this;
  }

  run() {
    // 函數compose
    const f = this.temp.reduceRight((a, b) => (cur, ...rest) => a(b(cur, ...rest), ...rest));
    const result = this.reduce((res, cur, index, arr) => {
      const ret = f(cur, index, arr);
      // filter標記的元素直接跳過
      if (ret === this.FLAG) {
        return res;
      }
      res.push(ret);
      return res;
    }, []);
    this.temp = [];
    return result;
  }
}
複製代碼

咱們已經完成了一個具備magic的數組,接下來測試一下和原生操做誰快:

const a = new MagicArray(...Array.apply(null, { length: 10000 }).map(x => Math.random() * 10));
console.time('normal')
a.map(x => x * 2).filter(x => x > 5)
console.timeEnd('normal')

console.time('compose')
a.myMap(x => x * 2).myFilter(x => x > 5).run()
console.timeEnd('compose')
複製代碼

通過屢次測試,compose過的數組與常規數組耗時比約爲3:5

對於this.temp.reduceRight((a, b) => (cur, ...rest) => a(b(cur, ...rest), ...rest));這段代碼怎麼理解?

相似於各類框架的中間件的實現,咱們這裏的實現是傳入參數和數組的item, index, array一致,可是咱們這裏的item是上一次的運行結果,故有b(cur, ...rest), ...rest)的操做

總之,遇到遍歷一個數據結構最後生成一個或多個結果(多個結果res用一個對象多個屬性表示)的狀況,那就用reduce盤它就是了

【廣告】ts類型註解生成器

多使用幾回reduce,就會發現它帶來更好的開發體驗和提升效率,也是造輪子用的比較多的。最近寫了一個小工具,將已知的json結構轉成ts聲明。在源碼裏面,能夠感覺一下用了reduce後,遞歸、遍歷邏輯一切都十分明朗。

// 已知json
{
  "a": 1,
  "b": "1",
  "c": {
    "d": 1,
    "e": [
      "1",
      {
        "g": 1,
        "r": "asd",
        "gg": true
      },
      1
    ]
  }
}
// 轉換結果
{
  a: number;
  b: string;
  c: {
    d: number;
    e: number | {
      g: number;
      r: string;
      gg: boolean;
    } | string [];
  };
}
複製代碼

項目地址

ts-declaration-gen: npm包地址

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索