之前的一篇文章:從一道面試題咬最,到“我可能看了假源碼”討論了bind方法的各種進(jìn)階Pollyfill,今天再分享一個有意思的題目欠动。
從解這道題目出發(fā)永乌,我會談到數(shù)組的Reduce方法,ES6特性和Redux數(shù)據(jù)流框架中Reducer的命名等等具伍。一道典型的題目翅雏,卻如唐代詩人章碣《對月》詩中所云:“別有洞天三十六,水晶臺殿冷層層人芽⊥福”
題目背景
完成一個'flatten'的函數(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ù)往下讀名挥。因為會有“不一樣”的知識點疟羹。
深入解讀
第一個想到的念頭肯定是遞歸,遞歸自然就想到遞歸的“盡頭”躺同,那就要判斷數(shù)組某項元素是否還是數(shù)組類型阁猜。
好吧,我們開始動手實現(xiàn)一個方案蹋艺,其實是上面解法的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);
}, []);
};
可能這樣寫剃袍,對于很多人來說,并不能完全理解捎谨。因為我們使用了較多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)為是“功能最強大”的類型判斷語句章鲤。在對數(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)行“科普”凤粗,因為下面我要圍繞它進(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.”
我并不打算對他直接翻譯今豆,因為這樣會變的更加晦澀難懂嫌拣。
我們看他的使用語法:
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可選項要销。
其值用于第一次調(diào)用callback的第一個參數(shù)。如果此參數(shù)為空夏块,則拿數(shù)組第一項來作為第一次調(diào)用callback的第一個參數(shù)疏咐。
比如纤掸,我們分析一個常用用法:
[0,1,2,3,4].reduce(function(previous, item, currentIndex, array){
return previous + item;
});
// 10
這里并未提供reduce的第二個參數(shù)initialValue,所以從數(shù)組第一項開始進(jìn)行回調(diào)函數(shù)的執(zhí)行浑塞。并且每次回調(diào)函數(shù)執(zhí)行完之后的結(jié)果借跪,作為下一次的previous執(zhí)行回調(diào)。
所以酌壕,上述代碼便是一個累加器的實現(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最新版的實現(xiàn)溅蛉。
如何實現(xiàn)一個reduce的pollyfill
現(xiàn)在明白了reduce的秘密,接下來我們需要充分發(fā)揮對JS的理解他宛,來手動實現(xiàn)一個reduce函數(shù)船侧。畢竟,reduce是ES5帶來的數(shù)組新特性厅各,在不使用ES5-shim的情況下镜撩,需要手動兼容。另外队塘,其實reduce方法可以實現(xiàn)的邏輯袁梗,大多都能夠使用循環(huán)來實現(xiàn)。但是了解這樣一個優(yōu)雅的方法憔古,不管是在程序的可讀性上遮怜,還是在設(shè)計理解層面上,還是很有必要的鸿市。
同樣锯梁,在MDN上也有實現(xiàn),但是我覺得下面的代碼實現(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;
};
如果讀者有不同實現(xiàn)思路焰情,也歡迎與我討論陌凳。
ES5-shim的pollyfill
我也同樣看了下ES5-shim里的pollyfill,跟我的思路基本完全一致内舟。唯一有一點區(qū)別的地方在于我用了forEach迭代而ES5-shim使用的是簡單for循環(huán)合敦。
當(dāng)然,數(shù)組的forEach方法也是ES5新增的谒获。但我這里是為了用簡單明了的思路蛤肌,實現(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其實是根據(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)行了深入討論诵肛,我們還實現(xiàn)了reduce的pollyfill屹培。在充分理解的基礎(chǔ)上,又簡要延伸到redux數(shù)據(jù)架構(gòu)里面reducer的命名怔檩。熟悉Redux的同學(xué)一定會有所感觸褪秀。
最后希望對讀者有所啟發(fā),也歡迎同我討論珠洗。
PS:百度知識搜索部大前端繼續(xù)招兵買馬溜歪,高級工程師、實習(xí)生職位均有许蓖,有意向者火速聯(lián)系蝴猪。。膊爪。