资讯专栏INFORMATION COLUMN

underscore数组遍历函数分析(一)

DevTTL / 3381人阅读

摘要:在中我们最常用的就是和两个方法了,这两个方法一般接收三个参数,分别是数组对象函数上下文。这个函数主要是给传进来的函数绑定作用域。

这是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。

最后的三个判断分别是:

如果obj的类型是数组,那么返回true

如果obj的length是0,也返回true。即使是{length: 0}这种情况,因为在调用isArrayLike的each和map等方法中会在for循环里面判断length,所以也不会造成影响。

最后这个(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的源码和例子来看。

如果value不存在,那就对应上面的_.map(obj)的情况,map中的iteratee就是_.identity函数,他会将后面接收到的obj[currentKey]直接返回。

如果value是一个函数,就对应_.map(obj, func)这种情况,那么会再调用optimizeCb方法,这里就和each的实现是一样的

如果value是个对象,对应_.map(obj, arrts)的情况,就会比较obj中的属性是否在arr里面,这个时候会调用_.matcher函数

这种情况一般是用在_.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://www.ucloud.cn/yun/93559.html

相关文章

  • underscore源码分析之基础方法

    摘要:在上篇文章整体架构分析中,我们讲过上面的方法有两种挂载方式,一个是挂载到构造函数上以的形式直接调用在后文上统称构造函数调用,另一种则是挂到上以的形式被实例调用在后文上统称原型调用。 underscore源码分析之基础方法 本文是underscore源码剖析系列的第二篇,主要介绍underscore中一些基础方法的实现。 mixin 在上篇文章underscore整体架构分析中,我们讲...

    BigNerdCoding 评论0 收藏0
  • underscore起学数组去重

    摘要:引子数组去重是一个老生常谈的话题,在面试中也经常会被问道。其中如果数组是排序的,去重运算效率更高,因为排序能够将相同的数排列在一起,方便前后比较。当数组有序对于对象的去重,我们知道为,所以使用比较对象在实际场景中没有意义。 引子 数组去重是一个老生常谈的话题,在面试中也经常会被问道。对于去重,有两种主流思想: 先排序,线性遍历后去重,时间复杂度O(n*log2n); 使用哈希,空间换...

    flybywind 评论0 收藏0
  • 学习 underscore 源码整体架构,打造属于自己的函数式编程类库

    摘要:译立即执行函数表达式处理支持浏览器环境微信小程序。学习整体架构,利于打造属于自己的函数式编程类库。下一篇文章可能是学习的源码整体架构。也可以加微信,注明来源,拉您进前端视野交流群。 前言 上一篇文章写了jQuery整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此...

    junnplus 评论0 收藏0
  • underscore.js 源码分析之 _.each() 函数

    摘要:遍历中的所有元素,按顺序用遍历输出每个元素。如果传递了参数,则把绑定到对象上。返回以方便链式调用。 each _.each(list, iteratee, [context]) Alias: forEach 遍历list中的所有元素,按顺序用遍历输出每个元素。如果传递了context参数,则把iteratee绑定到context对象上。每次调用iteratee都会传递三个参数:(ele...

    xbynet 评论0 收藏0
  • 打造属于自己的underscore系列 ( )

    摘要:目前通行的模块规范主要集中在和,因此为了让定义的库能够适用于各种规范。在框架的定义时需检测使用环境并兼容各种规范。服务端规范,检测是否存在,满足时通过将暴露出来,不满足则通过对象暴露出来。前者回调函数处理的是值和下标,后者处理的是值和属性。 本文为博主原创文章,转载请注明出处 https://www.cnblogs.com/kidfl... underscore作为开发中比较常用的一个...

    nifhlheimr 评论0 收藏0

发表评论

0条评论

DevTTL

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<