打造屬於自己的underscore系列(三)- 迭代器(上)

不做祖國的韭菜發表於2019-01-22

通過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中其他迭代器的實現我們放到下一節闡述。




相關文章