2

本文同步自我得博客:http://www.joeray61.com

我在这个系列的第一篇文章说过,我学underscore是为了在学backbone的时候少一些阻碍,从第一篇的写作时间到今天,大概也有个十几二十天,感觉拖得有点久,所以今天将会是underscore源码解析系列的最后一篇文章,我会在这篇文章中介绍underscore剩下的所有函数。
先附上前三篇文章的地址:Underscore源码解析(一)Underscore源码解析(二)Underscore源码解析(三)

_.zip

_.zip = function() {
    // 将参数转换为数组, 此时args是一个二维数组
    var args = slice.call(arguments);
    // 计算每一个数组的长度, 并返回其中最大长度值
    var length = _.max(_.pluck(args, 'length'));
    // 依照最大长度值创建一个新的空数组, 该数组用于存储处理结果
    var results = new Array(length);
    // 循环最大长度, 在每次循环将调用pluck方法获取每个数组中相同位置的数据(依次从0到最后位置)
    // 将获取到的数据存储在一个新的数组, 放入results并返回
    for(var i = 0; i < length; i++)
    results[i] = _.pluck(args, "" + i);
    // 返回的结果是一个二维数组
    return results;
};

这个函数将每个数组的相同位置的数据作为一个新的二维数组返回, 返回的数组长度以传入参数中最大的数组长度为准, 其它数组的空白位置使用undefined填充。zip函数应该包含多个参数, 且每个参数应该均为数组。

_.indexOf

_.indexOf = function(array, item, isSorted) {
    if(array == null)
        return -1;
    var i, l;
    // 若数组已经经过排序,则调用sortedIndex方法,获取元素插入数组中所处位置的索引号
    if(isSorted) {
        i = _.sortedIndex(array, item);
        return array[i] === item ? i : -1;
    }
    // 优先调用宿主环境提供的indexOf方法
    if(nativeIndexOf && array.indexOf === nativeIndexOf)
        return array.indexOf(item);
    // 循环并返回元素首次出现的位置
    for( i = 0, l = array.length; i < l; i++)
    if( i in array && array[i] === item)
        return i;
    // 没有找到元素, 返回-1
    return -1;
}; 

这个函数的作用是搜索一个元素在数组中首次出现的位置, 如果元素不存在则返回 -1,搜索时使用 === 对元素进行匹配

_.lastIndexOf

_.lastIndexOf = function(array, item) {
    if(array == null)
        return -1;
    // 优先调用宿主环境提供的lastIndexOf方法
    if(nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf)
        return array.lastIndexOf(item);
    var i = array.length;
    // 循环并返回元素最后出现的位置
    while(i--)
    if( i in array && array[i] === item)
        return i;
    // 没有找到元素, 返回-1
    return -1;
};

这个函数返回一个元素在数组中最后一次出现的位置, 如果元素不存在则返回 -1,搜索时使用 === 对元素进行匹配

_.range

_.range = function(start, stop, step) {
    // 参数控制
    if(arguments.length <= 1) {
        // 如果没有参数, 则start = 0, stop = 0, 在循环中不会生成任何数据, 将返回一个空数组
        // 如果有1个参数, 则参数指定给stop, start = 0
        stop = start || 0;
        start = 0;
    }
    // 生成整数的步长值, 默认为1
    step = arguments[2] || 1;

    // 根据区间和步长计算将生成的最大值
    var len = Math.max(Math.ceil((stop - start) / step), 0);
    var idx = 0;
    var range = new Array(len);

    // 生成整数列表, 并存储到range数组
    while(idx < len) {
        range[idx++] = start;
        start += step;
    }

    // 返回列表结果
    return range;
};

这个函数根据区间和步长, 生成一系列整数, 并作为数组返回,start参数表示最小数,stop参数表示最大数,step参数表示步长

_.bind

_.bind = function bind(func, context) {
    var bound, args;
    // 优先调用宿主环境提供的bind方法
    if(func.bind === nativeBind && nativeBind)
        return nativeBind.apply(func, slice.call(arguments, 1));
    // func参数必须是一个函数(Function)类型
    if(!_.isFunction(func))
        throw new TypeError;
    // args变量存储了bind方法第三个开始的参数列表, 每次调用时都将传递给func函数
    args = slice.call(arguments, 2);
    return bound = function() {
        if(!(this instanceof bound))
            return func.apply(context, args.concat(slice.call(arguments)));
        ctor.prototype = func.prototype;
        var self = new ctor;
        var result = func.apply(self, args.concat(slice.call(arguments)));
        if(Object(result) === result)
            return result;
        return self;
    };
};

这个函数为一个函数绑定执行上下文, 任何情况下调用该函数, 函数中的this均指向context对象,绑定函数时, 可以同时给函数传递调用形参

_.bindAll

_.bindAll = function(obj) {
    // 第二个参数开始表示需要绑定的函数名称
    var funcs = slice.call(arguments, 1);
    // 如果没有指定特定的函数名称, 则默认绑定对象本身所有类型为Function的属性
    if(funcs.length == 0)
        funcs = _.functions(obj);
    // 循环并将所有的函数上下本设置为obj对象本身
    // each方法本身不会遍历对象原型链中的方法, 但此处的funcs列表是通过_.functions方法获取的, 它已经包含了原型链中的方法
    each(funcs, function(f) {
        obj[f] = _.bind(obj[f], obj);
    });
    return obj;
};

这个函数将指定的函数, 或对象本身的所有函数上下本绑定到对象本身, 被绑定的函数在被调用时, 上下文对象始终指向对象本身

_.memoize

_.memoize = function(func, hasher) {
    // 用于存储缓存结果的memo对象
    var memo = {};
    // hasher参数应该是一个function, 它用于返回一个key, 该key作为读取缓存的标识
    // 如果没有指定key, 则默认使用函数的第一个参数作为key, 如果函数的第一个参数是复合数据类型, 可能会返回类似[Object object]的key, 这个key可能会造成后续计算的数据不正确
    hasher || ( hasher = _.identity);
    // 返回一个函数, 该函数首先通过检查缓存, 再对没有缓存过的数据进行调用
    return function() {
        var key = hasher.apply(this, arguments);
        return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
    };
};

这个函数将返回一个函数, 该函数集成了缓存功能, 将经过计算的值缓存到局部变量并在下次调用时直接返回

_.delay

_.delay = function(func, wait) {
    var args = slice.call(arguments, 2);
    // 通过setTimeout来延时执行
    return setTimeout(function() {
        return func.apply(null, args);
    }, wait);
};

这个函数的作用是延时执行一个函数,wait单位为ms, 第3个参数开始将被依次传递给执行函数

_.defer

_.defer = function(func) {
    // 相当于_.delay(func, 1, [arguments]);
    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};

这个函数的作用是延迟1ms执行函数,javascript是一个单线程的程序,setTimeout(func, time)作用是把func放到处理任务的队列末尾,在其他任务都完成之后的time ms 后执行func

_.throttle

_.throttle = function(func, wait) {
    var context, args, timeout, throttling, more, result;
    // whenDone变量调用了debounce方法, 因此在多次连续调用函数时, 最后一次调用会覆盖之前调用的定时器, 清除状态函数也仅会被执行一次
    // whenDone函数在最后一次函数执行的时间间隔截止时调用, 清除节流和调用过程中记录的一些状态
    var whenDone = _.debounce(function() {
        more = throttling = false;
    }, wait);
    // 返回一个函数, 并在函数内进行节流控制
    return function() {
        // 保存函数的执行上下文和参数
        context = this;
        args = arguments;
        // later函数在上一次函数调用时间间隔截止时执行
        var later = function() {
            // 清除timeout句柄, 方便下一次函数调用
            timeout = null;
            // more记录了在上一次调用至时间间隔截止之间, 是否重复调用了函数
            // 如果重复调用了函数, 在时间间隔截止时将自动再次调用函数
            if(more)
                func.apply(context, args);
            // 调用whenDone, 用于在时间间隔后清除节流状态
            whenDone();
        };
        // timeout记录了上一次函数执行的时间间隔句柄
        // timeout时间间隔截止时调用later函数, later中将清除timeout, 并检查是否需要再次调用函数
        if(!timeout)
            timeout = setTimeout(later, wait);
        // throttling变量记录上次调用的时间间隔是否已经结束, 即是否处于节流过程中
        // throttling在每次函数调用时设为true, 表示需要进行节流, 在时间间隔截止时设置为false(在whenDone函数中实现)
        if(throttling) {
            // 节流过程中进行了多次调用, 在more中记录一个状态, 表示在时间间隔截止时需要再次自动调用函数
            more = true;
        } else {
            // 没有处于节流过程, 可能是第一次调用函数, 或已经超过上一次调用的间隔, 可以直接调用函数
            result = func.apply(context, args);
        }
        // 调用whenDone, 用于在时间间隔后清除节流状态
        whenDone();
        // throttling变量记录函数调用时的节流状态
        throttling = true;
        // 返回调用结果
        return result;
    };
};

这是函数节流方法, throttle方法主要用于控制函数的执行频率, 在被控制的时间间隔内, 频繁调用函数不会被多次执行,在时间间隔内如果多次调用了函数, 时间隔截止时会自动调用一次, 不需要等到时间截止后再手动调用(自动调用时不会有返回值),throttle函数一般用于处理复杂和调用频繁的函数, 通过节流控制函数的调用频率, 节省处理资源

_.debounce

_.debounce = function(func, wait, immediate) {
    // timeout用于记录函数上一次调用的执行状态(定时器句柄)
    // 当timeout为null时, 表示上一次调用已经结束
    var timeout;
    // 返回一个函数, 并在函数内进行节流控制
    return function() {
        // 保持函数的上下文对象和参数
        var context = this, args = arguments;
        var later = function() {
            // 设置timeout为null
            // later函数会在允许的时间截止时被调用
            // 调用该函数时, 表明上一次函数执行时间已经超过了约定的时间间隔, 此时之后再进行调用都是被允许的
            timeout = null;
            if(!immediate)
                func.apply(context, args);
        };
        // 如果函数被设定为立即执行, 且上一次调用的时间间隔已经过去, 则立即调用函数
        if(immediate && !timeout)
            func.apply(context, args);
        // 创建一个定时器用于检查和设置函数的调用状态
        // 创建定时器之前先清空上一次setTimeout句柄, 无论上一次绑定的函数是否已经被执行
        // 如果本次函数在调用时, 上一次函数执行还没有开始(一般是immediate设置为false时), 则函数的执行时间会被推迟, 因此timeout句柄会被重新创建
        clearTimeout(timeout);
        // 在允许的时间截止时调用later函数
        timeout = setTimeout(later, wait);
    };
};

debounce与throttle方法类似, 用于函数节流, 它们的不同之处在于:

-- throttle关注函数的执行频率, 在指定频率内函数只会被执行一次
-- debounce函数更关注函数执行的间隔, 即函数两次的调用时间不能小于指定时间

如果两次函数的执行间隔小于wait, 定时器会被清除并重新创建, 这意味着连续频繁地调用函数, 函数一直不会被执行, 直到某一次调用与上一次调用的时间不小于wait毫秒

_.once

_.once = function(func) {
    // ran记录函数是否被执行过
    // memo记录函数最后一次执行的结果
    var ran = false, memo;
    return function() {
        // 如果函数已被执行过, 则直接返回第一次执行的结果
        if(ran)
            return memo;
        ran = true;
        return memo = func.apply(this, arguments);
    };
};

这个函数创建一个只会被执行一次的函数, 如果该函数被重复调用, 将返回第一次执行的结果

_.wrap

_.wrap = function(func, wrapper) {
    return function() {
        // 将当前函数作为第一个参数, 传递给wrapper函数
        var args = [func].concat(slice.call(arguments, 0));
        // 返回wrapper函数的处理结果
        return wrapper.apply(this, args);
    };
};

这个函数返回一个函数, 该函数会将当前函数作为参数传递给一个包裹函数,在包裹函数中可以通过第一个参数调用当前函数, 并返回结果

_.compose

_.compose = function() {
    // 获取函数列表, 所有参数需均为Function类型
    var funcs = arguments;
    // 返回一个供调用的函数句柄
    return function() {
        // 从后向前依次执行函数, 并将记录的返回值作为参数传递给前一个函数继续处理
        var args = arguments;
        for(var i = funcs.length - 1; i >= 0; i--) {
            args = [funcs[i].apply(this, args)];
        }
        // 返回最后一次调用函数的返回值
        return args[0];
    };
};

这个函数将多个函数组合到一起, 按照参数传递的顺序, 后一个函数的返回值会被依次作为参数传递给前一个函数作为参数继续处理,_.compose(A, B, C)等同于 A(B(C()))

_.after

_.after = function(times, func) {
    // 如果没有指定或指定无效次数, 则func被直接调用
    if(times <= 0)
        return func();
    // 返回一个计数器函数
    return function() {
        // 每次调用计数器函数times减1, 调用times次之后执行func函数并返回func函数的返回值
        if(--times < 1) {
            return func.apply(this, arguments);
        }
    };
};

after返回一个函数, 该函数作为调用计数器, 当该函数被调用times次(或超过times次)后, func函数将被执行

_.keys

_.keys = nativeKeys ||
function(obj) {
    if(obj !== Object(obj))
        throw new TypeError('Invalid object');
    var keys = [];
    // 记录并返回对象的所有属性名
    for(var key in obj)
    if(_.has(obj, key))
        keys[keys.length] = key;
    return keys;
};

这个函数用于获取一个对象的属性名列表(不包含原型链中的属性)

_.values

_.values = function(obj) {
    return _.map(obj, _.identity);
};

这个函数返回一个对象中所有属性的值列表(不包含原型链中的属性)

_.functions / _.methods

_.functions = _.methods = function(obj) {
    var names = [];
    for(var key in obj) {
        if(_.isFunction(obj[key]))
            names.push(key);
    }
    return names.sort();
};

这个函数获取一个对象中所有属性值为Function类型的key列表, 并按key名进行排序(包含原型链中的属性)

_.extend

_.extend = function(obj) {
    // each循环参数中的一个或多个对象
    each(slice.call(arguments, 1), function(source) {
        // 将对象中的全部属性复制或覆盖到obj对象
        for(var prop in source) {
            obj[prop] = source[prop];
        }
    });
    return obj;
};

这个函数将一个或多个对象的属性(包含原型链中的属性), 复制到obj对象, 如果存在同名属性则覆盖

_.pick

_.pick = function(obj) {
    // 创建一个对象, 存放复制的指定属性
    var result = {};
    // 从第二个参数开始合并为一个存放属性名列表的数组
    each(_.flatten(slice.call(arguments, 1)), function(key) {
        // 循环属性名列表, 如果obj中存在该属性, 则将其复制到result对象
        if( key in obj)
            result[key] = obj[key];
    });
    // 返回复制结果
    return result;
};

这个函数返回一个新对象, 并从obj中复制指定的属性到新对象中,第2个参数开始为指定的需要复制的属性名

_.defaults

_.defaults = function(obj) {
    // 从第二个参数开始可指定多个对象, 这些对象中的属性将被依次复制到obj对象中(如果obj对象中不存在该属性的话)
    each(slice.call(arguments, 1), function(source) {
        // 遍历每个对象中的所有属性
        for(var prop in source) {
            // 如果obj中不存在或属性值转换为Boolean类型后值为false, 则将属性复制到obj中
            if(obj[prop] == null)
                obj[prop] = source[prop];
        }
    });
    return obj;
};

这个函数将obj中不存在或转换为Boolean类型后值为false的属性, 从参数中指定的一个或多个对象中复制到obj,一般用于给对象指定默认值

_.clone

_.clone = function(obj) {
    // 不支持非数组和对象类型的数据
    if(!_.isObject(obj))
        return obj;
    // 复制并返回数组或对象
    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};

这个函数创建一个obj的副本, 返回一个新的对象, 该对象包含obj中的所有属性和值的状态

_.tap

_.tap = function(obj, interceptor) {
    interceptor(obj);
    return obj;
};

这个函数执行一个函数, 并将obj作为参数传递给该函数, 函数执行完毕后最终返回obj对象

eq

function eq(a, b, stack) {
    // 检查两个简单数据类型的值是否相等
    // 对于复合数据类型, 如果它们来自同一个引用, 则认为其相等
    // 如果被比较的值其中包含0, 则检查另一个值是否为-0, 因为 0 === -0 是成立的
    // 而 1 / 0 == 1 / -0 是不成立的(1 / 0值为Infinity, 1 / -0值为-Infinity, 而Infinity不等于-Infinity)
    if(a === b)
        return a !== 0 || 1 / a == 1 / b;
    // 将数据转换为布尔类型后如果值为false, 将判断两个值的数据类型是否相等(因为null与undefined, false, 0, 空字符串, 在非严格比较下值是相等的)
    if(a == null || b == null)
        return a === b;
    // 如果进行比较的数据是一个Underscore封装的对象(具有_chain属性的对象被认为是Underscore对象)
    // 则将对象解封后获取本身的数据(通过_wrapped访问), 然后再对本身的数据进行比较
    // 它们的关系类似与一个jQuery封装的DOM对象, 和浏览器本身创建的DOM对象
    if(a._chain)
        a = a._wrapped;
    if(b._chain)
        b = b._wrapped;
    // 如果对象提供了自定义的isEqual方法(此处的isEqual方法并非Undersocre对象的isEqual方法, 因为在上一步已经对Undersocre对象进行了解封)
    // 则使用对象自定义的isEqual方法与另一个对象进行比较
    if(a.isEqual && _.isFunction(a.isEqual))
        return a.isEqual(b);
    if(b.isEqual && _.isFunction(b.isEqual))
        return b.isEqual(a);
    // 对两个数据的数据类型进行验证
    // 获取对象a的数据类型(通过Object.prototype.toString方法)
    var className = toString.call(a);
    // 如果对象a的数据类型与对象b不匹配, 则认为两个数据值也不匹配
    if(className != toString.call(b))
        return false;
    // 执行到此处, 可以确保需要比较的两个数据均为复合数据类型, 且数据类型相等
    // 通过switch检查数据的数据类型, 针对不同数据类型进行不同的比较
    // (此处不包括对数组和对象类型, 因为它们可能包含更深层次的数据, 将在后面进行深层比较)
    switch (className) {
        case '[object String]':
            // 如果被比较的是字符串类型(其中a的是通过new String()创建的字符串)
            // 则将B转换为String对象后进行匹配(这里匹配并非进行严格的数据类型检查, 因为它们并非来自同一个对象的引用)
            // 在调用 == 进行比较时, 会自动调用对象的toString()方法, 返回两个简单数据类型的字符串
            return a == String(b);
        case '[object Number]':
            // 通过+a将a转成一个Number, 如果a被转换之前与转换之后不相等, 则认为a是一个NaN类型
            // 因为NaN与NaN是不相等的, 因此当a值为NaN时, 无法简单地使用a == b进行匹配, 而是用相同的方法检查b是否为NaN(即 b != +b)
            // 当a值是一个非NaN的数据时, 则检查a是否为0, 因为当b为-0时, 0 === -0是成立的(实际上它们在逻辑上属于两个不同的数据)
            return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
        case '[object Date]':
        // 对日期类型没有使用return或break, 因此会继续执行到下一步(无论数据类型是否为Boolean类型, 因为下一步将对Boolean类型进行检查)
        case '[object Boolean]':
            // 将日期或布尔类型转换为数字
            // 日期类型将转换为数值类型的时间戳(无效的日期格式将被换转为NaN)
            // 布尔类型中, true被转换为1, false被转换为0
            // 比较两个日期或布尔类型被转换为数字后是否相等
            return +a == +b;
        case '[object RegExp]':
            // 正则表达式类型, 通过source访问表达式的字符串形式
            // 检查两个表达式的字符串形式是否相等
            // 检查两个表达式的全局属性是否相同(包括g, i, m)
            // 如果完全相等, 则认为两个数据相等
            return a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase;
    }
    // 当执行到此时, ab两个数据应该为类型相同的对象或数组类型
    if( typeof a != 'object' || typeof b != 'object')
        return false;
    // stack(堆)是在isEqual调用eq函数时内部传递的空数组, 在后面比较对象和数据的内部迭代中调用eq方法也会传递
    // length记录堆的长度
    var length = stack.length;
    while(length--) {
        // 如果堆中的某个对象与数据a匹配, 则认为相等
        if(stack[length] == a)
            return true;
    }
    // 将数据a添加到堆中
    stack.push(a);
    // 定义一些局部变量
    var size = 0, result = true;
    // 通过递归深层比较对象和数组
    if(className == '[object Array]') {
        // 被比较的数据为数组类型
        // size记录数组的长度
        // result比较两个数组的长度是否一致, 如果长度不一致, 则方法的最后将返回result(即false)
        size = a.length;
        result = size == b.length;
        // 如果两个数组的长度一致
        if(result) {
            // 调用eq方法对数组中的元素进行迭代比较(如果数组中包含二维数组或对象, eq方法会进行深层比较)
            while(size--) {
                // 在确保两个数组都存在当前索引的元素时, 调用eq方法深层比较(将堆数据传递给eq方法)
                // 将比较的结果存储到result变量, 如果result为false(即在比较中得到某个元素的数据不一致), 则停止迭代
                if(!( result = size in a == size in b && eq(a[size], b[size], stack)))
                    break;
            }
        }
    } else {
        // 被比较的数据为对象类型
        // 如果两个对象不是同一个类的实例(通过constructor属性比较), 则认为两个对象不相等
        if('constructor' in a != 'constructor' in b || a.constructor != b.constructor)
            return false;
        // 深层比较两个对象中的数据
        for(var key in a) {
            if(_.has(a, key)) {
                // size用于记录比较过的属性数量, 因为这里遍历的是a对象的属性, 并比较b对象中该属性的数据
                // 当b对象中的属性数量多余a对象时, 此处的逻辑成立, 但两个对象并不相等
                size++;
                // 迭代调用eq方法, 深层比较两个对象中的属性值
                // 将比较的结果记录到result变量, 当比较到不相等的数据时停止迭代
                if(!( result = _.has(b, key) && eq(a[key], b[key], stack)))
                    break;
            }
        }
        // 深层比较完毕, 这里已经可以确保在对象a中的所有数据, 对象b中也存在相同的数据
        // 根据size(对象属性长度)检查对象b中的属性数量是否与对象a相等
        if(result) {
            // 遍历对象b中的所有属性
            for(key in b) {
                // 当size已经到0时(即对象a中的属性数量已经遍历完毕), 而对象b中还存在有属性, 则对象b中的属性多于对象a
                if(_.has(b, key) && !(size--))
                    break;
            }
            // 当对象b中的属性多于对象a, 则认为两个对象不相等
            result = !size;
        }
    }
    // 函数执行完毕时, 从堆中移除第一个数据(在比较对象或数组时, 会迭代eq方法, 堆中可能存在多个数据)
    stack.pop();
    // 返回的result记录了最终的比较结果
    return result;
}

eq函数只在isEqual方法中调用, 用于比较两个数据的值是否相等,与 === 不同在于, eq更关注数据的值,如果进行比较的是两个复合数据类型, 不仅仅比较是否来自同一个引用, 且会进行深层比较(对两个对象的结构和数据进行比较)

_.isEqual

_.isEqual = function(a, b) {
    return eq(a, b, []);
};

不多说了,就是内部函数eq的外部方法

_.isEmpty

_.isEmpty = function(obj) {
    // obj被转换为Boolean类型后值为false
    if(obj == null)
        return true;
    // 检查对象或字符串长度是否为0
    if(_.isArray(obj) || _.isString(obj))
        return obj.length === 0;
    // 检查对象(使用for in循环时将首先循环对象本身的属性, 其次是原型链中的属性), 因此如果第一个属性是属于对象本身的, 那么该对象不是一个空对象
    for(var key in obj)
    if(_.has(obj, key))
        return false;
    // 所有数据类型均没有通过验证, 是一个空数据
    return true;
};

这个函数用于检查数据是否为空值, 包含'', false, 0, null, undefined, NaN, 空数组(数组长度为0)和空对象(对象本身没有任何属性)

_.isElement

_.isElement = function(obj) {
    return !!(obj && obj.nodeType == 1);
};

这个函数用于验证对象是否是一个DOM对象

_.isArray

_.isArray = nativeIsArray ||
function(obj) {
    return toString.call(obj) == '[object Array]';
};

这个函数用于验证一个变量是否是数组

_.isObject

_.isObject = function(obj) {
    return obj === Object(obj);
};

这个函数用于验证对象是否是一个复合数据类型的对象(即非基本数据类型String, Boolean, Number, null, undefined)

_.isArguments

_.isArguments = function(obj) {
    return toString.call(obj) == '[object Arguments]';
};
// 验证isArguments函数, 如果运行环境无法正常验证arguments类型的数据, 则重新定义isArguments方法
if(!_.isArguments(arguments)) {
    // 对于环境无法通过toString验证arguments类型的, 则通过调用arguments独有的callee方法来进行验证
    _.isArguments = function(obj) {
        // callee是arguments的一个属性, 指向对arguments所属函数自身的引用
        return !!(obj && _.has(obj, 'callee'));
    };
}

这个函数用于检查一个数据是否是一个arguments参数对象

_.isFunction / _.isString / _.isNumber / _.isDate / _.isRegExp

这几个我就放在一起说了,他们都是通过Object.prototype.toString.call(obj)的值来进行判断的

_.isFinite

_.isFinite = function(obj) {
    return _.isNumber(obj) && isFinite(obj);
};

这个函数用于检查一个数字是否为有效数字且有效范围(Number类型, 值在负无穷大 - 正无穷大之间)

_.isNaN

_.isNaN = function(obj) {
    return obj !== obj;
};

在js里,所有数据中只有NaN与NaN不相等

_.isBoolean

_.isBoolean = function(obj) {
    // 支持字面量和对象形式的Boolean数据
    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};

这个函数用于检查数据是否是Boolean类型

_.isNull

_.isNull = function(obj) {
    return obj === null;
};

这个函数用于检查数据是否是Null值

_.isUndefined

_.isUndefined = function(obj) {
    return obj === void 0;
};

这个函数用于检查数据是否是Undefined值

_.has

_.has = function(obj, key) {
    return hasOwnProperty.call(obj, key);
};

这个函数检查一个属性是否属于对象本身, 而非原型链中

_.noConflict

_.noConflict = function() {
    // previousUnderscore变量记录了Underscore定义前_(下划线)的值
    root._ = previousUnderscore;
    return this;
};

这个函数一般用于避免命名冲突或规范命名方式,放弃_(下划线)命名的Underscore对象, 并返回Underscore对象

_.identity

_.identity = function(value) {
    return value;
};

这个函数返回与参数相同的值, 一般用于将一个数据的获取方式转换为函数获取方式(内部用于构建方法时作为默认处理器函数)

_.times

_.times = function(n, iterator, context) {
    for(var i = 0; i < n; i++)
    iterator.call(context, i);
};

这个函数的作用是使指定的函数迭代执行n次(无参数)

_.escape

_.escape = function(string) {
    return ('' + string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;');
};

这个函数用于将HTML字符串中的特殊字符转换为HTML实体, 包含 & < > " ' \

_.result

_.result = function(object, property) {
    if(object == null)
        return null;
    // 获取对象的值
    var value = object[property];
    // 如果值是一个函数, 则执行并返回, 否则将直接返回
    return _.isFunction(value) ? value.call(object) : value;
};

这个函数指定一个对象的属性, 返回该属性对应的值, 如果该属性对应的是一个函数, 则会执行该函数并返回结果

_.mixin

_.mixin = function(obj) {
    // obj是一个集合一系列自定义方法的对象, 此处通过each遍历对象的方法
    each(_.functions(obj), function(name) {
        // 通过addToWrapper函数将自定义方法添加到Underscore构建的对象中, 用于支持对象式调用
        // 同时将方法添加到 _ 本身, 用于支持函数式调用
        addToWrapper(name, _[name] = obj[name]);
    });
};

这个函数添加一系列自定义方法到Underscore对象中, 用于扩展Underscore插件

_.template

_.template = function(text, data, settings) {
    // 模板配置, 如果没有指定配置项, 则使用templateSettings中指定的配置项
    settings = _.defaults(settings || {}, _.templateSettings);

    // 开始将模板解析为可执行源码
    var source = "__p+='" + text.replace(escaper, function(match) {
        // 将特殊符号转移为字符串形式
        return '\\' + escapes[match];
    }).replace(settings.escape || noMatch, function(match, code) {
        // 解析escape形式标签 <%- %>, 将变量中包含的HTML通过_.escape函数转换为HTML实体
        return "'+\n_.escape(" + unescape(code) + ")+\n'";
    }).replace(settings.interpolate || noMatch, function(match, code) {
        // 解析interpolate形式标签 <%= %>, 将模板内容作为一个变量与其它字符串连接起来, 则会作为一个变量输出
        return "'+\n(" + unescape(code) + ")+\n'";
    }).replace(settings.evaluate || noMatch, function(match, code) {
        // 解析evaluate形式标签 <% %>, evaluate标签中存储了需要执行的JavaScript代码, 这里结束当前的字符串拼接, 并在新的一行作为JavaScript语法执行, 并将后面的内容再次作为字符串的开始, 因此evaluate标签内的JavaScript代码就能被正常执行
        return "';\n" + unescape(code) + "\n;__p+='";
    }) + "';\n";
    if(!settings.variable)
        source = 'with(obj||{}){\n' + source + '}\n';
    source = "var __p='';" + "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + source + "return __p;\n";

    // 创建一个函数, 将源码作为函数执行体, 将obj和Underscore作为参数传递给该函数
    var render = new Function(settings.variable || 'obj', '_', source);
    // 如果指定了模板的填充数据, 则替换模板内容, 并返回替换后的结果
    if(data)
        return render(data, _);
    // 如果没有指定填充数据, 则返回一个函数, 该函数用于将接收到的数据替换到模板
    // 如果在程序中会多次填充相同模板, 那么在第一次调用时建议不指定填充数据, 在获得处理函数的引用后, 再直接调用会提高运行效率
    var template = function(data) {
        return render.call(this, data, _);
    };
    // 将创建的源码字符串添加到函数对象中, 一般用于调试和测试
    template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
    // 没有指定填充数据的情况下, 返回处理函数句柄
    return template;
};

这个我要介绍的最后一个函数,也是我个人认为比较重要的,它是Underscore模板解析方法, 用于将数据填充到一个模板字符串中,在模板体内, 可通过argments获取2个参数, 分别为填充数据(名称为obj)和Underscore对象(名称为_)

小结

今天一口气把剩下的所有函数都介绍完了,真是累感不爱啊,不过在写作这几篇博客的过程中,我也从Underscore这个框架中学到了很多东西,包括它的优雅的代码风格(至少比我自己写的优雅),还有一个优秀的库整个的架构是怎么搭建起来的。
以后我还会继续为大家分享其他的前端知识和学习心得,thx for reading, hope u enjoy


JoeRay61
2.3k 声望182 粉丝