摘要:每次被執行時,和被傳入,這個根據進行累加或者是自身消減,英文原意,進而返回最新的。
之前的一篇文章:從一道面試題,到“我可能看了假源碼”討論了bind方法的各種進階Pollyfill,今天再分享一個有意思的題目。
從解這道題目出發,我會談到數組的Reduce方法,ES6特性和Redux數據流框架中Reducer的命名等等。一道典型的題目,卻如唐代詩人章碣《對月》詩中所云:“別有洞天三十六,水晶臺殿冷層層。”
題目背景完成一個"flatten"的函數,實現“拍平”一個多維數組為一維。示例如下:
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]解法先睹為快
先看一眼比較優雅的ES6解法:
const flatten = arr => arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatten(val) : val), []);
如果你看不明白,不要放棄。我會用ES5的思路“翻譯”一下,相信你很快就能看懂。
如果你一眼能看明白,也建議繼續往下讀。因為會有“不一樣”的知識點。
深入解讀第一個想到的念頭肯定是遞歸,遞歸自然就想到遞歸的“盡頭”,那就要判斷數組某項元素是否還是數組類型。
好吧,我們開始動手實現一個方案,其實是上面解法的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高級用法。關鍵核心還用到了類似“函數式”思想的reduce方法。
千萬不要灰心,繼續往下看。
我們注意到上面的寫法return使用了()表達式。括號內容前半句是為了執行。這樣寫也許稍微晦澀難懂一些。請看下面的代碼示例,你就會明白:
function t() { var a = 1; return (a++, a); } t(); // 2Object.prototype.toString.call是什么?
Object.prototype.toString.call可以暫且認為是“功能最強大”的類型判斷語句。在對數組類型進行判斷時,需要格外小心,比如這樣幾個“陷阱”:
var a = []; typeof a; // "object" a instanceof Array; // true; Object.prototype.toString.call(a); // "[object Array]"reduce方法到底做了什么?
現在到了最關鍵的地方。reduce方法是ES5引入,很多人使用它的場景并不多。但是了解他的特性卻是必須的。遺憾的是,社區上對于它的內容似乎都不是“太重視”。“函數式“思想也讓一些初學者望而卻步。這里我簡要進行“科普”,因為下面我要圍繞它進行延伸:
reduce在英文中譯為“減少; 縮小; 使還原; 使變弱”,MDN對方法直述為:“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])
參數分析:
1)array1:必需。
一個數組對象。即調用reduce方法的必須是一個數組類型。
2)callbackfn:必需。
一個接受最多四個參數的函數。對于數組中的每個元素,reduce方法都會調用 callbackfn 函數一次。
這個callback的4個參數為:
accumulator // 上一次調用回調返回的值,或者是提供的初始值(initialValue) currentValue // 數組中正在處理的元素 currentIndex // 數據中正在處理的元素索引,如果提供了initialValue ,從0開始;否則從1開始 array // 調用reduce的數組
3)initialValue可選項。
其值用于第一次調用callback的第一個參數。如果此參數為空,則拿數組第一項來作為第一次調用callback的第一個參數。
比如,我們分析一個常用用法:
[0,1,2,3,4].reduce(function(previous, item, currentIndex, array){ return previous + item; }); // 10
這里并未提供reduce的第二個參數initialValue,所以從數組第一項開始進行回調函數的執行。并且每次回調函數執行完之后的結果,作為下一次的previous執行回調。
所以,上述代碼便是一個累加器的實現。
ES6寫法現在理解了Reduce函數,再結合ES6特性,使解法更加優雅:
const flatten = arr => arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatten(val) : val), []);
這樣寫是不是太“函數式”了,但是思路跟之前解法完全一樣。我只不過充分使用了箭頭函數帶來的便利。并且使用了更便捷的isArray對數組類型進行判斷。這是開篇提到的解法,也是MDN最新版的實現。
如何實現一個reduce的pollyfill現在明白了reduce的秘密,接下來我們需要充分發揮對JS的理解,來手動實現一個reduce函數。畢竟,reduce是ES5帶來的數組新特性,在不使用ES5-shim的情況下,需要手動兼容。另外,其實reduce方法可以實現的邏輯,大多都能夠使用循環來實現。但是了解這樣一個優雅的方法,不管是在程序的可讀性上,還是在設計理解層面上,還是很有必要的。
同樣,在MDN上也有實現,但是我覺得下面的代碼實現更加優雅和清晰:
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; };
如果讀者有不同實現思路,也歡迎與我討論。
ES5-shim的pollyfill我也同樣看了下ES5-shim里的pollyfill,跟我的思路基本完全一致。唯一有一點區別的地方在于我用了forEach迭代而ES5-shim使用的是簡單for循環。
當然,數組的forEach方法也是ES5新增的。但我這里是為了用簡單明了的思路,實現reduce方法,根本目的還是希望對reduce有一個全面透徹的了解。
如果您還不明白,我認為還是對于reduce方法沒有掌握透徹。建議再梳理一遍。
Redux中的reducer明白了reduce函數,我們再來看一下Redux中的reducer和這個reduce有什么命名上的關聯。
熟悉Redux數據流架構的同學理解reducer做了什么,關于這個純函數的命名,在redux源碼github倉庫上也有一個官方解釋:“It"s called a reducer because it"s the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue)”,雖然是一筆帶過,但是總結的恰到好處。
我詳細說一下:Redux數據流里,reducers其實是根據之前的狀態(previous state)和現有的action(current action)更新state(這個state可以理解為上文累加器的結果(accumulation))。每次redux reducer被執行時,state和action被傳入,這個state根據action進行累加或者是“自身消減”(reduce,英文原意),進而返回最新的state。這符合一個典型reduce函數的用法:state -> action -> state.
總結這篇文章對于如何優雅地“扁平化”一個多維數組進行了解法分析。并且對于秉承函數式編程思想的reduce方法進行了深入討論,我們還實現了reduce的pollyfill。在充分理解的基礎上,又簡要延伸到redux數據架構里面reducer的命名。熟悉Redux的同學一定會有所感觸。
最后希望對讀者有所啟發,也歡迎同我討論。
PS:百度知識搜索部大前端繼續招兵買馬,高級工程師、實習生職位均有,有意向者火速聯系。。。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86850.html
摘要:接下來,我們換一種思路,用一個相對較新的來實現方法。從這道題目看出,相比考察死記硬背,這樣的實現更有意義。對數組的操作我們不能陌生,其中方法更要做到駕輕就熟。最后,我們再看下社區上著名的和的實現。 有不少剛入行的同學跟我說:JavaScript 很多 API 記不清楚怎么辦?數組的這方法、那方法總是傻傻分不清楚,該如何是好?操作 DOM 的方式今天記,明天忘,真讓人奔潰! 甚至有的開發...
摘要:接下來,我們換一種思路,用一個相對較新的來實現方法。從這道題目看出,相比考察死記硬背,這樣的實現更有意義。對數組的操作我們不能陌生,其中方法更要做到駕輕就熟。最后,我們再看下社區上著名的和的實現。 有不少剛入行的同學跟我說:JavaScript 很多 API 記不清楚怎么辦?數組的這方法、那方法總是傻傻分不清楚,該如何是好?操作 DOM 的方式今天記,明天忘,真讓人奔潰! 甚至有的開發...
摘要:此模塊包含的設計思路即為預以匹配降級方案。沒有默認編譯該模塊,以及利用該模塊判斷后提供平臺相關邏輯的主要原因在于其設計原則的代碼完成核心的功能。此處,也引出了代碼實現的另一個基本原則面向功能標準,先功能覆蓋再優雅降級。 在進入 Zepto Core 模塊代碼之前,本節簡略列舉 Zepto 及其他開源庫中一些 Polyfill 的設計思路與實現技巧。 涉及模塊:IE/IOS 3/Dete...
閱讀 1683·2023-04-25 20:16
閱讀 3838·2021-10-09 09:54
閱讀 2696·2021-09-04 16:40
閱讀 2517·2019-08-30 15:55
閱讀 830·2019-08-29 12:37
閱讀 2733·2019-08-26 13:55
閱讀 2903·2019-08-26 11:42
閱讀 3144·2019-08-23 18:26