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相似,这里就不多做诠释了,有兴致的能够本身去看一下。

    原文作者:尹光耀
    原文地址: https://segmentfault.com/a/1190000013867963
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞