打造自己的underscore系列(三)
通過underscore系列的一和二,我們掌握了一些underscore框架設計的思想以及基礎的資料型別診斷,從這一節開始,我們將慢慢進入underscore的難點和核心。這個系列的三和四,我們會全方位的瞭解underscore中對迭代器的實現。
三, 迭代器
3.1 迭代器的基本概念
javascript的迭代器我們並不陌生。我們可以這樣理解,迭代器提供了一種方法按順序訪問集合中的每一項,而這種集合可以是陣列,也可以是物件。我們先回憶一下,在ES6之前,javascript有7種陣列的迭代器,分別是
- for 迴圈,它是最常規,也是最基礎的迭代
var arr = [1,2,4] for(var i=0;i<arr.length;i++) { console.log(i) } 複製程式碼
- forEach 接收一個函式作為引數,對陣列中的每一個元素使用該函式。
function add(num) {console.log(num + 1)} var arr = [1,2,3] arr.forEach(add) 複製程式碼
- every 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,當所有的元素使用該方法後都返回true, 則最終結果返回true
function judge(a) {return a > 1} var arr = [1,23,4]; console.log(arr.every(judge)); // false 複製程式碼
- some 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,只要有一個元素使用該函式後返回結果為true,則最終結果返回true。
function judge(a) {return a > 1} var arr = [1,23,4]; console.log(arr.some(judge)); // true 複製程式碼
- filter 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,返回使用該函式後返回值為true的新陣列集合。
function filterNum(a) {return a> 2} var arr = [1,45,56,2,5] console.log(arr.filter(filterNum)) // [45, 56, 5] 複製程式碼
- reduce 接收一個函式,返回一個值。該方法會從一個累加值開始,不斷對累加值和陣列中的後續元素呼叫該函式,直到陣列中的最後一個元素,最後返回得到的累加值。(reduceRight 遍歷的順序為倒序)
function add(a, b) {return a +b } var arr = [1,3,4,5,6] console.log(arr.reduce(add, 0))//19 複製程式碼
- map 接收一個函式作為引數,對陣列中的每一個元素使用該函式,返回一個執行了該函式的陣列集合,該方法也不改變原陣列
function mapNum(a) {return a + 1 } var arr = [2,4,56] console.log(arr.map(mapNum))// [3,5,57] 複製程式碼
針對物件 ,我們經常使用for in進行迭代, 也可以使用 Object.keys等方式進行迭代
var a = {b: 1, c: 2} for(var i in a) { console.log(i) //b, c } Object.keys(a) // b, c 複製程式碼
在設計underscore框架的迭代器時我們需要考慮的是, 如何擴充套件方法讓迭代器適用於陣列,物件,或者帶有length屬性的arguments類陣列以及字串。並且從實現角度講,為了相容低版本的瀏覽器,我們需要拋棄ES5規範下便捷的方法,使用最常規的for迴圈進行陣列,類陣列,字串的遍歷。同樣,也可以通過for迴圈來遍歷物件。
3.2 _.reduce - _.reduce(list, iteratee, [memo], [context])
首先從最複雜的reduce入手,reduce的基本功能前面在陣列的迭代器方法中已經介紹,我們只重點關注實現的細節。其中iteratee 為迭代器函式,該函式有四個引數memo,value 和 迭代的index(或者 key)和最後一個引用的整個 list。
reduce 和 reduceRight 唯一的區別在於遍歷順序,一個從左往右, 一個從右往左,因此可以用同一個函式來設計reduce。其中 context 改變this的指向我們稍後分析。
_.reduce = createReduce(1); _.reduceRight = createReduce(-1); var createReduce = function(dir) { // dir 來區分 return function (obj, iteratee, meno, context) { // 兩種型別,物件和類陣列需要區別處理方便for迴圈遍歷, 物件我們需要拿到所有的屬性集合,陣列,類陣列我們關注的是下標。 // 巧妙點:當為陣列,類陣列時 keys = false, 當為物件時 keys = 屬性陣列 var keys = !isArrayLike(obj) && _.keys(obj) var lengths = (keys || obj).length; // 處理遍歷方向,即引數dir的值 var index = dir > 0 ? 0 : length-1; for (; index >= 0 && index < lengths; index += dir) { // 如果是陣列,類陣列則取下標,如果是物件則取屬性值 var currentKey = keys ? keys[index] : index; // 執行迭代器函式,並把返回值賦值給meno,繼續迴圈迭代 meno = iteratee(meno, obj[currentKey], currentKey, obj); } return meno } } 複製程式碼
reduce 函式在使用的時候,meno是可選項,如果沒有傳遞meno, 則自動會把list 中的第一個元素賦值給meno。因此我們可以將處理的核心程式碼抽離為一個獨立的函式,並將是否有meno的初始值做獨立判斷。
_.reduce = createReduce(1); _.reduceRight = createReduce(-1); var createReduce = function(dir) { // dir 來區分 var reducer = function (obj, iteratee, meno, context, initial) { ··· // 增加meno 的初始值判斷賦值 if (!initial) { memo = obj[keys ? keys[index] : index]; index += dir; } ··· } return function (obj, iteratee, meno, context) { // 記錄是否有meno傳值 var initial = arguments.length >=3; return reducer(obj, iteratee, meno, context, initial) } } 複製程式碼
reduce的實現已經基本完成,然而依然留著一個懸念,那就是context可以改變this的指向。underscore原始碼中單獨將context改變this指向的方法抽離成一個獨立的函式optimizeCb,該方法可以相容underscore中 所有需要改變this指向的過程, reduce 為其中一種情形,函式呼叫call方法改變this指向所傳遞的引數分別為 meno, value, index, list
var createReduce = function(dir) { ··· return function(obj, iteratee, meno, context) { ··· return reducer(obj, optimizeCb(iteratee, context, 4), initial) //optimizeCb優化 } } var optimizeCb = function(func, context, argCount) { // 當沒有特定的this指向時, 返回原函式 if(context == void 0) return func; switch(argCount) { // 針對reduce函式的context指向 case 4: return function(context, meno, value, index, list) { return func.call(context, meno, value, index, list); } } } 複製程式碼
3.3 map - _.map(list, iteratee, [context])
underscore 中map 方法原理上會生成一個新的陣列,該陣列與目標源陣列,類陣列的length屬性相同,或者與物件的自身可列舉屬性個數相同,因此有了reduce 的基礎,我們可以簡單的實現map 方法
_.map = function (obj, iteratee, context) { var keys = !isArrayLike(obj) && _.keys(obj) var lengths = (keys || obj).length; // 生成一個個數和目標源陣列個數相同,或者目標源物件自身可列舉屬性個數相同的陣列 var result = new Array(lengths); for(var i=0; i<lengths;i++) { var currentKey = keys ? keys[i] : i; results =iteratee(obj[currentKey], currentKey, obj) } } 複製程式碼
對於map的使用,我們可以不傳遞iteratee迭代器,可以傳遞一個函式型別的迭代器,也可以傳遞一個物件型別作為迭代器,因此需要在進入迭代過程之前做一層過濾,根據不同的迭代器型別做不同的操作。
_.map = function(obj, iteratee, context) { // 迭代器型別分類 iteratee = cb(iteratee, context); ··· } 複製程式碼
- 1.當不傳遞迭代器時,如_.map(obj) 會返回obj本身,因此需要定義另一個方法,該方法返回與傳入引數相等的值。
var cb = function(iteratee, context) { if(iteratee == null) return _.identity } _.identity = function(value) { return value } 複製程式碼
- 2.當傳遞的迭代器為函式時,可以直接進入optimizeCb的迭代器優化過程。
var cb = function(iteratee, context) { if( _.isFunction(iteratee) ) return optimizeCb(iteratee, context, 3) // 此時型別為3 } // 完善optimizeCb 函式 var optimizeCb = function(func, context, argCount) { // 當沒有特定的this指向時, 返回原函式 if(context == void 0) return func; switch(argCount) { case 3: return function(context, value, index, list) { return func.call(context, value, index, list); } case 4: return function(context, meno, value, index, list) { return func.call(context, meno, value, index, list); } } } 複製程式碼
- 3.當傳遞的迭代器為物件時會返回一個斷言函式,這個具體的內容我們將在以後的篇幅分析。
var cb = function(iteratee, context) { if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); // 返回斷言函式 } 複製程式碼
3.4 _.times - _.times(n, iteratee, [context])
_.times也是underscore提供的迭代器,它會呼叫迭代器n次,每次呼叫時傳遞index作為引數,最終結果返回一個執行結果的陣列。例如:
console.log(_.times(n, function(i) {return i * 2 })) // [0, 1, 4] 複製程式碼
簡單的實現如下,關鍵點在註釋中說明:
_.times = function(n, iteratee, context) { // 必須保證n的值是比0 大的數字 var n = Math.max(0, n); // 建立一個個數為n的新陣列 var arr = new Array(n); for(var i = 0; i<n;i++) { arr[i] = iteratee(i) } return arr } 複製程式碼
同樣,涉及context 改變this指向,我們同樣可以通過optimizeCb進行優化,此時完善optimizeCb函式
_.times = function(n, iteratee, context) { ··· iteratee = optimizeCb(iteratee, context, 1) } // 完善optimizeCb 函式 var optimizeCb = function(func, context, argCount) { // 當沒有特定的this指向時, 返回原函式 if(context == void 0) return func; switch(argCount) { case 1: return function(context, value) { return func.call(context, value) } case 3: return function(context, value, index, list) { return func.call(context, value, index, list); } case 4: return function(context, meno, value, index, list) { return func.call(context, meno, value, index, list); } } } 複製程式碼
通過列舉三種迭代器的設計,我們不但掌握了underscore迭代器設計的基本思想,也對中間核心optimizeCb函式設計的可能進行了列舉,underscore中optimizeCb函式優化的型別只有三種,對應的數值分別為1,3,4(注意:並沒有2的類別)。我們也對迭代器的三種類型進行了列舉,在cb函式中分別區分了不傳遞迭代器,迭代器自身為陣列,物件時的處理。掌握了optimizeCb和cb兩個函式的設計思想,在設計剩餘的迭代器時難度便小了很多。 由於篇幅過長,underscore中其他迭代器的實現我們放到下一節闡述。