面試官直呼內(nèi)行!如何實現(xiàn)一個比較完美的reduce函數(shù)?
基本用法
reduce函數(shù)是js原生提供的用于處理數(shù)組結(jié)構(gòu)的函數(shù)。
先來看一下MDN中的介紹:
reduce() 方法對數(shù)組中的每個元素按序執(zhí)行一個由用戶提供的 reducer 函數(shù),每一次運(yùn)行 reducer 會將先前元素的計算結(jié)果作為參數(shù)傳入,最后將其結(jié)果匯總為單個返回值。
參數(shù)
callbackFn一個reducer函數(shù),包含四個參數(shù):
previousValue:上一次調(diào)用callbackFn時的返回值。在第一次調(diào)用時,若指定了初始值initialValue,其值則為initialValue,否則為數(shù)組索引為 0 的元素array[0]。currentValue:數(shù)組中正在處理的元素。在第一次調(diào)用時,若指定了初始值initialValue,其值則為數(shù)組索引為 0 的元素array[0],否則為array[1]。currentIndex:數(shù)組中正在處理的元素的索引。若指定了初始值initialValue,則起始索引號為 0,否則從索引 1 起始。array:用于遍歷的數(shù)組。
initialValue可選 作為第一次調(diào)用callback函數(shù)時參數(shù)previousValue的值。若指定了初始值initialValue,則currentValue則將使用數(shù)組第一個元素;否則previousValue將使用數(shù)組第一個元素,而currentValue將使用數(shù)組第二個元素。
reduce函數(shù)功能是非常強(qiáng)大的適用于非常多的場景:
比如使用reduce函數(shù)實現(xiàn)累加:
let total = [ 0, 1, 2, 3 ].reduce(
( previousValue, currentValue ) => previousValue + currentValue,
0
)
// 6
生成新數(shù)組:
let total = [ 0, 1, 2, 3 ].reduce(
function (pre, cur) {
pre.push(cur + 1)
return pre
},
[]
)
// [1, 2, 3, 4]
等等.....
那么問題來了,如何手寫實現(xiàn)一個reduce函數(shù)呢?
實現(xiàn)基礎(chǔ)版本
根據(jù)文檔可知,reduce函數(shù)接受一個運(yùn)行函數(shù)和一個初始的默認(rèn)值
/**
*
* @param {Array} data 原始數(shù)組
* @param {Function} iteratee 運(yùn)行函數(shù)
* @param {Any} memo 初始值
* @returns {boolean} True if value is an FormData, otherwise false
*/
function myReduce(data, iteratee, memo) {
// ...
}
接下來實現(xiàn)基本功能
reduce函數(shù)的重點就是要將結(jié)果再次傳入執(zhí)行函數(shù)中進(jìn)行處理
function myReduce(data, iteratee, memo) {
for(let i = 0; i < data.length; i++) {
memo = iteratee(memo, data[i], i, data)
}
return memo
}
需求一:增加this綁定
其實reduce函數(shù)可以指定自定義對象綁定this
在這里可以使用call對函數(shù)進(jìn)行重新綁定
function myReduce(data, iteratee, memo, context) {
// 重置iteratee函數(shù)的this指向
iteratee = bind(iteratee, context)
for(let i = 0; i < data.length; i++) {
memo = iteratee(memo, data[i], i, data)
}
return memo
}
// 綁定函數(shù) 使用call進(jìn)行綁定
function bind(fn, context) {
// 返回一個匿名函數(shù),執(zhí)行時重置this指向
return function(memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
};
}
需求二:增加對第二個參數(shù)默認(rèn)值的支持
reduce函數(shù)的第三個參數(shù)也是可選值,如果沒有傳遞第三個參數(shù),那么直接使用傳入數(shù)據(jù)的第一個位置初始化
function myReduce(data, iteratee, memo, context) {
// 重置iteratee函數(shù)的this指向
iteratee = bind(iteratee, context)
// 判斷是否傳遞了第三個參數(shù)
let initial = arguments.length >= 3; // 新增
// 初始的遍歷下標(biāo)
let index = 0 // 新增
if(!initial) { // 新增
// 如果用戶沒有傳入默認(rèn)值,那么就取數(shù)據(jù)的第一項作為默認(rèn)值
memo = data[index] // 新增
// 所以遍歷就要從第二項開始
index += 1 // 新增
}
for(let i = index; i < data.length; i++) { // 修改
memo = iteratee(memo, data[i], i, data)
}
return memo
}
// 綁定函數(shù) 使用call進(jìn)行綁定
function bind(fn, context) {
return function(memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
};
}
需求三:支持對象
js原生的reduce函數(shù)是不支持對象這種數(shù)據(jù)結(jié)構(gòu)的,那么如何完善我們的reduce函數(shù)呢?
其實只需要取出對象中所有的key,然后遍歷key就可以了
function myReduce(data, iteratee, memo, context) {
iteratee = bind(iteratee, context)
// 取出所有的key值
let _keys = !Array.isArray(data) && Object.keys(data) // 新增
// 長度賦值
let len = (_keys || data).length // 新增
let initial = arguments.length >= 3;
let index = 0
if(!initial) {
// 如果沒有設(shè)置默認(rèn)值初始值,那么取第一個值的操作也要區(qū)分對象/數(shù)組
memo = data[ _keys ? _keys[index] : index] // 修改
index += 1
}
for(let i = index; i < len; i++) {
// 取key值
let currentKey = _keys ? _keys[i] : i // 新增
memo = iteratee(memo, data[currentKey], currentKey, data) // 修改
}
return memo
}
function bind(fn, context) {
return function(memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
};
}
需求四:reduceRight
其實以上的內(nèi)容已經(jīng)是一個比較完整的reduce函數(shù)了,最后一個擴(kuò)展內(nèi)容是reduceRight函數(shù),其實reduceRight函數(shù)的功能也很簡單,就是在遍歷的時候倒序進(jìn)行操作,例如:
let total = [ 0, 1, 2, 3 ].reduce(
function (pre, cur) {
pre.push(cur + 1)
return pre
},
[]
)
// [1, 2, 3, 4]
其實實現(xiàn)這個功能也非常簡單,只需要初始操作的值更改為最后一個元素的位置就可以了:
// 加入一個參數(shù)dir,用于標(biāo)識正序/倒序遍歷
function myReduce(data, iteratee, memo, context, dir) { //修改
iteratee = bind(iteratee, context)
let _keys = !Array.isArray(data) && Object.keys(data)
let len = (_keys || data).length
let initial = arguments.length >= 3;
// 定義下標(biāo)
let index = dir > 0 ? 0 : len - 1 // 修改
if(!initial) {
memo = data[ _keys ? _keys[index] : index]
// 定義初始值
index += dir // 修改
}
// 每次修改只需步進(jìn)指定的值
for(;index >= 0 && index < len; index += dir) { // 修改
let currentKey = _keys ? _keys[index] : index
memo = iteratee(memo, data[currentKey], currentKey, data)
}
return memo
}
function bind(fn, context) {
if (!context) return fn;
return function(memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
};
}
調(diào)用的時候直接傳入最后一個參數(shù)為1 / \-1即可
myReduce([1, 2, 3, 4], function(pre, cur) {
console.log(cur)
}, [], 1)
myReduce([1, 2, 3, 4], function(pre, cur) {
console.log(cur)
}, [], -1)
最后將整個函數(shù)進(jìn)行重構(gòu)抽離成為一個單獨(dú)的函數(shù):
function createReduce(dir) {
function reduce() {
// ....
}
return function() {
return reduce()
}
}
最后最終的代碼如下:
function createReduce(dir) {
function reduce(data, fn, memo, initial) {
let _keys = Array.isArray(data) && Object.keys(data),
len = (_keys || data).length,
index = dir > 0 ? 0 : len - 1;
if (!initial) {
memo = data[_keys ? _keys[index] : index]
index += dir
}
for (; index >= 0 && index < len; index += dir) {
let currentKey = _keys ? _keys[index] : index
memo = fn(memo, data[currentKey], currentKey, data)
}
return memo
}
return function (data, fn, memo, context) {
let initial = arguments.length >= 3
return reduce(data, bind(fn, context), memo, initial)
}
}
function bind(fn, context) {
if (!context) return fn;
return function (memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
};
}
let reduce = createReduce(1)
let reduceRight = createReduce(-1)
而這種實現(xiàn)方式也是underscore.js所實現(xiàn)的reduce函數(shù)的方式。
寫在最后 ?
未來可能會更新實現(xiàn)mini-vue3和javascript基礎(chǔ)知識系列,希望能一直堅持下去,期待多多點贊????,一起進(jìn)步!????
關(guān)于本文
來自:pino
https://juejin.cn/post/7113743909452251167
