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

經過underscore系列的一和二,咱們掌握了一些underscore框架設計的思想以及基礎的數據類型診斷,從這一節開始,咱們將慢慢進入underscore的難點和核心。這個系列的三和四,咱們會全方位的瞭解underscore中對迭代器的實現。javascript

三, 迭代器

3.1 迭代器的基本概念

javascript的迭代器咱們並不陌生。咱們能夠這樣理解,迭代器提供了一種方法按順序訪問集合中的每一項,而這種集合能夠是數組,也能夠是對象。咱們先回憶一下,在ES6以前,javascript有7種數組的迭代器,分別是java

  • 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的初始值作獨立判斷。bash

_.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 方法ide

_.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做爲參數,最終結果返回一個執行結果的數組。例如:post

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




相關文章
相關標籤/搜索