面試題目別有洞天:優雅es6+智慧polyfill+redux迷之命名

LucasHC發表於2017-03-09

之前的一篇文章:從一道面試題,到“我可能看了假原始碼”討論了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的到底是什麼?

我們注意到上面的寫法return使用了()表示式。括號內容前半句是為了執行。這樣寫也許稍微晦澀難懂一些。請看下面的程式碼示例,你就會明白:

function t() {
    var a = 1;
    return (a++, a);
}
t(); // 2複製程式碼

Object.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:百度知識搜尋部大前端繼續招兵買馬,高階工程師、實習生職位均有,有意向者火速聯絡。。。


本文對你有幫助?歡迎掃碼加入前端學習小組微信群:

面試題目別有洞天:優雅es6+智慧polyfill+redux迷之命名

相關文章