博客 / 詳情

返回

underscore數組遍歷函數分析(一)

這是underscore源碼剖析系列第三篇文章,主要介紹underscore中each、map、filter、every、reduce等我們常用的一些遍歷數組的方法。

each

在underscore中我們最常用的就是each和map兩個方法了,這兩個方法一般接收三個參數,分別是數組/對象、函數、上下文。

// iteratee函數有三個參數,分別是item、index、array或者value、key、obj
_.each = _.forEach = function(obj, iteratee, context) {
    // 如果不傳context,那麼each方法裏面的this就會指向window
    iteratee = optimizeCb(iteratee, context);
    var i, length;
    // 如果是類數組,一般來説包括數組、arguments、DOM集合等等
    if (isArrayLike(obj)) {
        for (i = 0, length = obj.length; i < length; i++) {
            iteratee(obj[i], i, obj);
        }
    // 一般是指對象
    } else {
        var keys = _.keys(obj);
        for (i = 0, length = keys.length; i < length; i++) {
            iteratee(obj[keys[i]], keys[i], obj);
        }
    }
    return obj;
};

each函數的源碼很簡單,函數內部會使用isArrayLike方法來判斷當前傳入的第一個參數是類數組或者對象,如果是類數組,直接使用訪問下標的方式來遍歷,並將數組的項和index傳給iteratee函數,如果是對象,則先獲取到對象的keys,再進行遍歷後將對象的value和key傳給iteratee函數

不過在這裏,我們主要分析optimizeCb和isArrayLike兩個函數。

optimizeCb

    // 這個函數主要是給傳進來的func函數綁定context作用域。
    var optimizeCb = function (func, context, argCount) {
        // 如果沒有傳context,那就直接返回func函數
        if (context === void 0) return func;
        // 如果沒有傳入argCount,那就默認是3。這裏是根據第二次傳入的參數個數來給call函數傳入不同數量的參數
        switch (argCount == null ? 3 : argCount) {
            case 1: return function (value) {
                return func.call(context, value);
            };
            case 2: return function (value, other) {
                return func.call(context, value, other);
            };
            // 一般是each、map等
            case 3: return function (value, index, collection) {
                return func.call(context, value, index, collection);
            };
            // 一般是reduce等
            case 4: return function (accumulator, value, index, collection) {
                return func.call(context, accumulator, value, index, collection);
            };
        }
        // 如果參數數量大於4
        return function () {
            return func.apply(context, arguments);
        };
    };

其實我們很容易就看出來optimizeCb函數只是幫func函數綁定context的,如果不存在context,那麼直接返回func,否則則會根據第二次傳給func函數的參數數量來判斷給call函數傳幾個值。
這裏有個重點,為什麼要用這麼麻煩的方式,而不直接用apply來將arguments全部傳進去?
原因是call方法的速度要比apply方法更快,因為apply會對數組參數進行檢驗和拷貝,所以這裏就對常用的幾種形式使用了call,其他情況下使用了apply,詳情可以看這裏:call和apply

isArrayLike

關於isArrayLike方法,我們來看underscore的實現。(這個延伸比較多,如果沒興趣,可以跳過)

// 一個高階函數,返回對象上某個具體屬性的值
var property = function (key) {
    return function (obj) {
        return obj == null ? void 0 : obj[key];
    };
};

// 這裏有個ios8上面的bug,會導致類似var pbj = {1: "a", 2: "b", 3: "c"}這種對象的obj.length = 4; jQuery中也有這個bug。
// https://github.com/jashkenas/underscore/issues/2081 
// https://github.com/jquery/jquery/issues/2145
// MAX_SAFE_INTEGER is 9007199254740991 (Math.pow(2, 53) - 1).
// http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

// 據説用obj["length"]就可以解決?我沒有ios8的環境,有興趣的可以試試
var getLength = property('length');

// 判斷是否是類數組,如果有length屬性並且值為number類型即可視作類數組
var isArrayLike = function (collection) {
    var length = getLength(collection);
    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

在underscore中,只要帶有length屬性,都可以被認為是類數組,所以即使是{length: 10}這種情況也會被歸為類數組。
我個人感覺這樣寫其實太過片面,我還是更喜歡jQuery裏面isArrayLike方法的實現。

function isArrayLike(obj) {
    // Support: real iOS 8.2 only (not reproducible in simulator)
    // `in` check used to prevent JIT error (gh-2145)
    // hasOwn isn't used here due to false negatives
    // regarding Nodelist length in IE
    var length = !!obj && "length" in obj && obj.length,
        type = toType(obj);
    // 排除了obj為function和全局中有length變量的情況
    if (isFunction(obj) || isWindow(obj)) {
        return false;
    }
    return type === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}

jQuery中使用in來解決ios8下面那個JIT的錯誤,同時還會排除obj是函數和window的情況,因為如果obj是函數,那麼obj.length則是這個函數參數的個數,而如果obj是window,那麼我在全局中定義一個var length = 10,這個同樣也能獲取到length。

最後的三個判斷分別是:

  1. 如果obj的類型是數組,那麼返回true
  2. 如果obj的length是0,也返回true。即使是{length: 0}這種情況,因為在調用isArrayLike的each和map等方法中會在for循環裏面判斷length,所以也不會造成影響。
  3. 最後這個(length - 1) in obj我個人理解就是為了排除{length: 10}這種情況,因為這個可以滿足length>0和length==="number"的情況,但是一般情況下是無法滿足最後(length - 1) in obj的,但是NodeList和arguments這些卻可以滿足這個條件。

map

説完了each,我們再來説説map,map函數其實和each的實現很類似,不過不一樣的一個地方在於,map函數的第二個參數不一定是函數,我們可以什麼都不傳,甚至還可以傳個對象。

var arr = [{name:'Kevin'}, {name: 'Daisy', age: 18}]
var result1 = _.map(arr); // [{name:'Kevin'}, {name: 'Daisy', age: 18}]
var result2 = _.map(arr, {name: 'Daisy'}) // [false, true]

所以這裏就會對傳入map的第二個參數進行判斷,整體來説map函數的實現比each更加簡潔。

_.map = _.collect = function (obj, iteratee, context) {
        // 因為在map中,第二個參數可能不是函數,所以用cb,這點和each的實現不一樣。
        iteratee = cb(iteratee, context);
        // 如果不是類數組(是對象),則獲取到keys
        var keys = !isArrayLike(obj) && _.keys(obj),
            length = (keys || obj).length,
            results = Array(length);
        // 這裏根據keys是否存在來判斷傳給iteratee是key還是index
        for (var index = 0; index < length; index++) {
            var currentKey = keys ? keys[index] : index;
            results[index] = iteratee(obj[currentKey], currentKey, obj);
        }
        return results;
    };

cb

我們來看看map函數中這個cb函數到底是什麼來歷?

_.identity = function (value) {
    return value;
};
var cb = function (value, context, argCount) {
    // 如果value不存在
    if (value == null) return _.identity;
    // 如果傳入的是個函數
    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
    // 如果傳入的是個對象
    if (_.isObject(value)) return _.matcher(value);
    return _.property(value);
};

cb函數在underscore中一般是用在遍歷方法中,大多數情況下value都是一個函數,我們結合上面map的源碼和例子來看。

  1. 如果value不存在,那就對應上面的_.map(obj)的情況,map中的iteratee就是_.identity函數,他會將後面接收到的obj[currentKey]直接返回。
  2. 如果value是一個函數,就對應_.map(obj, func)這種情況,那麼會再調用optimizeCb方法,這裏就和each的實現是一樣的
  3. 如果value是個對象,對應_.map(obj, arrts)的情況,就會比較obj中的屬性是否在arr裏面,這個時候會調用_.matcher函數
  4. 這種情況一般是用在_.iteratee函數中,用來訪問對象的某個屬性,具體看這裏:iteratee函數

matcher

那麼我們再來看matcher函數,matcher函數內部對兩個對象做了淺比較。

_.matcher = _.matches = function (attrs) {
    // 將attrs和{}合併為一個對象(避免attrs為undefined)
    attrs = _.extendOwn({}, attrs);
    return function (obj) {
        return _.isMatch(obj, attrs);
    };
};
// isMatch方法會對接收到的attrs對象進行遍歷,同時比較obj中是否有這一項
_.isMatch = function (object, attrs) {
    var keys = _.keys(attrs), length = keys.length;
    // 如果object和attr都是空,那麼返回true,否則object為空時返回false
    if (object == null) return !length;
    // 這一步沒懂是為了做什麼?
    var obj = Object(object);
    for (var i = 0; i < length; i++) {
        var key = keys[i];
        if (attrs[key] !== obj[key] || !(key in obj)) return false;
    }
    return true;
};

matcher是個高階方法,他會將兩次接收到的對象傳給isMatch函數來進行判斷。首先是以attrs為被遍歷的對象,通過對比obj[key]和attrs[key]的值,只要obj中的值和attrs中的不想等,就會返回false。
這裏還會排除一種情況,如果attrs中對應key的value正好是undefined,而且obj中並沒有key這個屬性,這樣obj[key]和attrs[key]其實都是undefined,這裏使用!==來比較必然會返回false,實際上兩者應該是不想等的。
所以使用in來判斷obj上到底有沒有key這個屬性,如果沒有,也會返回false。如果attrs上面所有屬性在obj中都能找到,並且兩者的值正好相等,那麼就會返回true。
這也就是為什麼_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); 會返回 [false, true]。

重寫each

each和map實現原理基本上一樣,不過map更加簡潔,這裏可以用map的形式重寫一下each

_.each = _.forEach = function (obj, iteratee, context) {
        iteratee = optimizeCb(iteratee, context);
        var keys = !isArrayLike(obj) && _.keys(obj),
            length = (keys || obj).length,
            results = Array(length);
        for (var index = 0; index < length; index++) {
            var currentKey = keys ? keys[index] : index;
            iteratee(obj[currentKey], currentKey, obj);
        }
        return obj;
    };

filter、every、some、reject

這幾種方法的實現和上面的each、map類似,這裏就不多做解釋了,有興趣的可以自己去看一下。

user avatar xiangjiaochihuanggua 頭像 guizimo 頭像 peter-wilson 頭像 tigerandflower 頭像 yaofly 頭像 chongdianqishi 頭像 _raymond 頭像 susouth 頭像 jianqiangdepaobuxie 頭像 ailim 頭像 gaoming13 頭像 mulander 頭像
66 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.