之前的一篇文章:從一道面試題,到“我可能看了假源碼”討論了bind方法的各種進(jìn)階Pollyfill,今天再分享一個有意思的題目。
從解這道題目出發(fā),我會談到數(shù)組的Reduce方法,ES6特性和Redux數(shù)據(jù)流框架中Reducer的命名等等。一道典型的題目,卻如唐代詩人章碣《對月》詩中所云:“別有洞天三十六,水晶臺殿冷層層?!?/p>
題目背景
完成一個'flatten'的函數(shù),實(shí)現(xiàn)“拍平”一個多維數(shù)組為一維。示例如下:
var testArr1 = [[0, 1], [2, 3], [4, 5]];
var testArr2 = [0, [1, [2, [3, [4, [5]]]]]];
flatten(testArr1) // [0, 1, 2, 3, 4, 5]
flatten(testArr2) // [0, 1, 2, 3, 4, 5]
解法先睹為快
先看一眼比較優(yōu)雅的ES6解法:
const flatten = arr => arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatten(val) : val), []);
如果你看不明白,不要放棄。我會用ES5的思路“翻譯”一下,相信你很快就能看懂。
如果你一眼能看明白,也建議繼續(xù)往下讀。因?yàn)闀小安灰粯印钡闹R點(diǎn)。
深入解讀
第一個想到的念頭肯定是遞歸,遞歸自然就想到遞歸的“盡頭”,那就要判斷數(shù)組某項(xiàng)元素是否還是數(shù)組類型。
好吧,我們開始動手實(shí)現(xiàn)一個方案,其實(shí)是上面解法的ES5版本:
var flatten = function(array) {
return array.reduce(function(previous, val) {
if (Object.prototype.toString.call(val) !== '[object Array]') {
return (previous.push(val), previous);
}
return (Array.prototype.push.apply(previous, flatten(val)), previous);
}, []);
};
可能這樣寫,對于很多人來說,并不能完全理解。因?yàn)槲覀兪褂昧溯^多JS高級用法。關(guān)鍵核心還用到了類似“函數(shù)式”思想的reduce方法。
千萬不要灰心,繼續(xù)往下看。
return的到底是什么?
我們注意到上面的寫法return使用了()表達(dá)式。括號內(nèi)容前半句是為了執(zhí)行。這樣寫也許稍微晦澀難懂一些。請看下面的代碼示例,你就會明白:
function t() {
var a = 1;
return (a++, a);
}
t(); // 2
Object.prototype.toString.call是什么?
Object.prototype.toString.call可以暫且認(rèn)為是“功能最強(qiáng)大”的類型判斷語句。在對數(shù)組類型進(jìn)行判斷時,需要格外小心,比如這樣幾個“陷阱”:
var a = [];
typeof a; // "object"
a instanceof Array; // true;
Object.prototype.toString.call(a); // "[object Array]"
reduce方法到底做了什么?
現(xiàn)在到了最關(guān)鍵的地方。reduce方法是ES5引入,很多人使用它的場景并不多。但是了解他的特性卻是必須的。遺憾的是,社區(qū)上對于它的內(nèi)容似乎都不是“太重視”?!昂瘮?shù)式“思想也讓一些初學(xué)者望而卻步。這里我簡要進(jìn)行“科普”,因?yàn)橄旅嫖乙獓@它進(jìn)行延伸:
reduce在英文中譯為“減少; 縮小; 使還原; 使變?nèi)酢保琈DN對方法直述為:“The reduce method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.”
我并不打算對他直接翻譯,因?yàn)檫@樣會變的更加晦澀難懂。
我們看他的使用語法:
array1.reduce(callbackfn[, initialValue])
參數(shù)分析:
1)array1:必需。
一個數(shù)組對象。即調(diào)用reduce方法的必須是一個數(shù)組類型。
2)callbackfn:必需。
一個接受最多四個參數(shù)的函數(shù)。對于數(shù)組中的每個元素,reduce方法都會調(diào)用 callbackfn 函數(shù)一次。
這個callback的4個參數(shù)為:
accumulator // 上一次調(diào)用回調(diào)返回的值,或者是提供的初始值(initialValue)
currentValue // 數(shù)組中正在處理的元素
currentIndex // 數(shù)據(jù)中正在處理的元素索引,如果提供了initialValue ,從0開始;否則從1開始
array // 調(diào)用reduce的數(shù)組
3)initialValue可選項(xiàng)。
其值用于第一次調(diào)用callback的第一個參數(shù)。如果此參數(shù)為空,則拿數(shù)組第一項(xiàng)來作為第一次調(diào)用callback的第一個參數(shù)。
比如,我們分析一個常用用法:
[0,1,2,3,4].reduce(function(previous, item, currentIndex, array){
return previous + item;
});
// 10
這里并未提供reduce的第二個參數(shù)initialValue,所以從數(shù)組第一項(xiàng)開始進(jìn)行回調(diào)函數(shù)的執(zhí)行。并且每次回調(diào)函數(shù)執(zhí)行完之后的結(jié)果,作為下一次的previous執(zhí)行回調(diào)。
所以,上述代碼便是一個累加器的實(shí)現(xiàn)。
ES6寫法
現(xiàn)在理解了Reduce函數(shù),再結(jié)合ES6特性,使解法更加優(yōu)雅:
const flatten = arr => arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatten(val) : val), []);
這樣寫是不是太“函數(shù)式”了,但是思路跟之前解法完全一樣。我只不過充分使用了箭頭函數(shù)帶來的便利。并且使用了更便捷的isArray對數(shù)組類型進(jìn)行判斷。這是開篇提到的解法,也是MDN最新版的實(shí)現(xiàn)。
如何實(shí)現(xiàn)一個reduce的pollyfill
現(xiàn)在明白了reduce的秘密,接下來我們需要充分發(fā)揮對JS的理解,來手動實(shí)現(xiàn)一個reduce函數(shù)。畢竟,reduce是ES5帶來的數(shù)組新特性,在不使用ES5-shim的情況下,需要手動兼容。另外,其實(shí)reduce方法可以實(shí)現(xiàn)的邏輯,大多都能夠使用循環(huán)來實(shí)現(xiàn)。但是了解這樣一個優(yōu)雅的方法,不管是在程序的可讀性上,還是在設(shè)計(jì)理解層面上,還是很有必要的。
同樣,在MDN上也有實(shí)現(xiàn),但是我覺得下面的代碼實(shí)現(xiàn)更加優(yōu)雅和清晰:
var reduce = function(arr, func, initialValue) {
var base = typeof initialValue === 'undefined' ? arr[0] : initialValue;
var startPoint = typeof initialValue === 'undefined' ? 1 : 0;
arr.slice(startPoint)
.forEach(function(val, index) {
base = func(base, val, index + startPoint, arr);
});
return base;
};
如果讀者有不同實(shí)現(xiàn)思路,也歡迎與我討論。
ES5-shim的pollyfill
我也同樣看了下ES5-shim里的pollyfill,跟我的思路基本完全一致。唯一有一點(diǎn)區(qū)別的地方在于我用了forEach迭代而ES5-shim使用的是簡單for循環(huán)。
當(dāng)然,數(shù)組的forEach方法也是ES5新增的。但我這里是為了用簡單明了的思路,實(shí)現(xiàn)reduce方法,根本目的還是希望對reduce有一個全面透徹的了解。
如果您還不明白,我認(rèn)為還是對于reduce方法沒有掌握透徹。建議再梳理一遍。
Redux中的reducer
明白了reduce函數(shù),我們再來看一下Redux中的reducer和這個reduce有什么命名上的關(guān)聯(lián)。
熟悉Redux數(shù)據(jù)流架構(gòu)的同學(xué)理解reducer做了什么,關(guān)于這個純函數(shù)的命名,在redux源碼github倉庫上也有一個官方解釋:“It's called a reducer because it's the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue)”,雖然是一筆帶過,但是總結(jié)的恰到好處。
我詳細(xì)說一下:Redux數(shù)據(jù)流里,reducers其實(shí)是根據(jù)之前的狀態(tài)(previous state)和現(xiàn)有的action(current action)更新state(這個state可以理解為上文累加器的結(jié)果(accumulation))。每次redux reducer被執(zhí)行時,state和action被傳入,這個state根據(jù)action進(jìn)行累加或者是“自身消減”(reduce,英文原意),進(jìn)而返回最新的state。這符合一個典型reduce函數(shù)的用法:state -> action -> state.
總結(jié)
這篇文章對于如何優(yōu)雅地“扁平化”一個多維數(shù)組進(jìn)行了解法分析。并且對于秉承函數(shù)式編程思想的reduce方法進(jìn)行了深入討論,我們還實(shí)現(xiàn)了reduce的pollyfill。在充分理解的基礎(chǔ)上,又簡要延伸到redux數(shù)據(jù)架構(gòu)里面reducer的命名。熟悉Redux的同學(xué)一定會有所感觸。
最后希望對讀者有所啟發(fā),也歡迎同我討論。
PS:百度知識搜索部大前端繼續(xù)招兵買馬,高級工程師、實(shí)習(xí)生職位均有,有意向者火速聯(lián)系。。。