1. 用reduce實現數組一些api
給數組prototype加上基於reduce實現的api:
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); }, }); 複製代碼
接下來寫測試用例:
// 函數用例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的參數的情況,最後需要一個用例執行的方法:
// 簡單的比較相等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解決:
普通對象:使用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循環的下標迷惑了自己
根據對象生成一個簡單schema:
// 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)
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) + 1function 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 []; }; } 複製代碼