underscore.js源码学习

作者 likaiqiang 日期 2018-06-30
underscore.js源码学习

前言

在es6大行其道以及ie全家桶被吊打的今天,像underscore这样的类库已经过时了,但是它的源码还是值得一读的。本文抱着学习的心态,尝试解读underscore.js@0.1.0版本的源码。

源码解读

underscore.js(0.1.0版本,下同)源码分为四个部分:集合、数组、函数、对象、工具方法。

集合方法

each

each: function (obj, iterator, context) {
var index = 0;
try { //try catch 是为了实现一种通用的方法,从而达到跳出各类循环(for循环、forEach循环、for in 循环...)的目的,后续会说明
if (obj.forEach) { //判断obj是否有forEach方法,如果有,使用forEach遍历
obj.forEach(iterator, context);
} else if (obj.length) {//判断obj是否为数组(forEach为es5的方法,某些低版本浏览器不支持es5)或者字符串
for (var i = 0; i < obj.length; i++) iterator.call(context, obj[i], i); //使用for循环遍历,并依次执行iterator函数
} else if (obj.each) { //判断obj是否有each方法,如果有,使用each遍历
obj.each(function (value) { iterator.call(context, value, index++); });
} else {//如果上述条件都不符合(一般是一个对象字面量),则使用for in 循环,并依次执行iterator函数
var i = 0;
for (var key in obj) {
var value = obj[key], pair = [key, value];
pair.key = key;
pair.value = value;
iterator.call(context, pair, i++);
}
}
} catch (e) {
if (e != '__break__') throw e;//巧妙的跳出各类循环
}
return obj;
}

underscore几乎所有方法都会调用此方法,很重要。

map

map: function (obj, iterator, context) {
if (obj && obj.map) return obj.map(iterator, context); //如果浏览器支持map方法,则调用map并返回
var results = [];
_.each(obj, function (value, index) { //如果浏览器不支持map,调用each方法依次执行iterator函数(注:iterator必须有返回值),把结果push到results数组并返回iterator
results.push(iterator.call(context, value, index));
});
return results;
}

inject(reduce)

inject: function (obj, memo, iterator, context) {
_.each(obj, function (value, index) {
memo = iterator.call(context, memo, value, index);
});
return memo;
},

实现思路和map差不多,map是把所有结果push到results数组里,inject有一个memo参数,这个参数会依次传递给iterator函数,同时iterator函数的返回结果又赋值给memo,有点滚雪球的感觉,最后的memo返回。

select(filter)

select: function (obj, iterator, context) {
if (obj.filter) return obj.filter(iterator, context); //如果obj有filter方法,则调用filter并返回
var results = [];
_.each(obj, function (value, index) { //循环遍历obj,依次测试iterator函数,如果通过,把结果push到results数组里,最后返回results
if (iterator.call(context, value, index)) results.push(value);
});
return results;
},

这个函数的作用相当于es5中的filter,即返回所有符合iterator函数测试的数组

detect(filterOne)

detect: function (obj, iterator, context) {
var result;
_.each(obj, function (value, index) {
if (iterator.call(context, value, index)) {
result = value;
throw '__break__';//通过throw '__break__'跳出循环。
}
});
return result;
}

作用与filte类似,不同的是它只会返回符合测试的第一项

reject(filter的反函数)

reject: function (obj, iterator, context) {
var results = [];
_.each(obj, function (value, index) {
if (!iterator.call(context, value, index)) results.push(value);
});
return results;
}

filter的反函数,实现思路也与filter大同小异。

all(every)

all: function (obj, iterator, context) {
iterator = iterator || function (v) { return v; };//如果没有iterator,iterator=function(v){return v}
if (obj.every) return obj.every(iterator, context);//如果obj有every方法,则调用every并返回
var result = true; //默认测试结果为true
_.each(obj, function (value, index) {
result = result && !!iterator.call(context, value, index);//有一项为false,result=false,跳出循环并返回result
if (!result) throw '__break__';
});
return result;
}

any

any: function (obj, iterator, context) {
iterator = iterator || function (v) { return v; };
if (obj.some) return obj.some(iterator, context);
var result = false;
_.each(obj, function (value, index) {
if (result = !!iterator.call(context, value, index)) throw '__break__'; //与all实现方式类似,不同的是这一句。只要有一项测试通过则跳出循环,并返回result
});
return result;
}

include

include:function (obj, target) {
if (_.isArray(obj)) return _.indexOf(obj, target) != -1;//如果是数组,调用数组的indexOf方法并返回。
var found = false; //初始化结果为false
_.each(obj, function (pair) {
if (pair.value === target) { 循环遍历obj,如果某一项的值==target,found=true,跳出循环并返回found
found = true;
throw '__break__';
}
});
return found;
}

invoke

invoke: function (obj, method) {
var args = _.toArray(arguments).slice(2); //取出除obj、method以外的参数并赋值给args
return _.map(obj, function (value) {
return (method ? value[method] : value).apply(value, args); //实际上要保证这句话能跑起来,只要保证(method ? value[method] : value)的结果是个函数。那么只能是value[method]了,除非method没传,且value本身就是个函数,不过这样做又有什么意义呢
});
}

很牛B的一个方法,具体介绍可以去看这篇博文。在0.1.0版本中,此方法并不完整,method只能传一个字符串,并且只能是obj的某个属性字符串,这个属性字符串的值还要是个函数,这一点,从源码里面就能看出。

pluck(map?)

pluck: function (obj, key) {
var results = [];
_.each(obj, function (value) { results.push(value[key]); });//关键代码
return results;
}

便携版map方法,从源码就能看出来,此处不表。

max

max: function (obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); //如果没有iterator并且obj是个数组,直接调用Math.max方法并返回
var result; //初始化结果对象
_.each(obj, function (value, index) {
var computed = iterator ? iterator.call(context, value, index) : value; //记录当前iterator函数执行结果
if (result == null || computed >= result.computed) result = { value: value, computed: computed };//当前的结果与上一次进行比对,如果当前的大于上一次,则给result重新赋值
});
return result.value; //返回最后的result.value
}

min

min: function (obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
var result;
_.each(obj, function (value, index) {
var computed = iterator ? iterator.call(context, value, index) : value;
if (result == null || computed < result.computed) result = { value: value, computed: computed };
});
return result.value;
}

与max大同小异

sortBy

sortBy: function (obj, iterator, context) {
return _.pluck(_.map(obj, function (value, index) {
return { //先map obj
value: value,
criteria: iterator.call(context, value, index) //返回criteria用来存放iterator函数执行结果,作为sort排序的依据
};
}).sort(function (left, right) {
var a = left.criteria, b = right.criteria; //根据criteria的值排序
return a < b ? -1 : a > b ? 1 : 0;//升序排序
}), 'value'); //调用pluck方法,返回排序后的obj副本。
}

返回一个排序后的list拷贝副本(升序排序)。如果传递iterator参数,iterator将作为list中每个值的排序依据。在0.1.0版本中iterator必须为函数

sortedIndex

sortedIndex: function (array, obj, iterator) {
iterator = iterator || function (val) { return val; };
var low = 0, high = array.length;
while (low < high) { //二分查找法
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
}

找出obj在array中的位置,并返回这个位置。如果传入iterator,iterator将作为array每一项的包装函数且要有返回值,默认iterator 返回每一项的原始值

toArray

toArray: function (iterable) {
if (!iterable) return [];
if (_.isArray(iterable)) return iterable;
return _.map(iterable, function (val) { return val; });
}

把一个任何可以迭代的对象转换成一个数组(利用map)

size

size: function (obj) {
return _.toArray(obj).length;
}

返回数组的长度

数组方法

first

first: function (array) {
return array[0];
}

last

last: function (array) {
return array[array.length - 1];
}

compact

compact: function (array) {
return _.select(array, function (value) { return !!value; }); //把数组的每一项转换成Boolean类型,返回结果为true的项
}

flatten

flatten: function (array) {
return _.inject(array, [], function (memo, value) { //使用inject方法,给memo传入初始值[]
if (_.isArray(value)) return memo.concat(_.flatten(value)); //如果array的某一项是数组,递归调用flatten方法,最后把结果拼接起来
memo.push(value);
return memo;
});
}

把一个无限嵌套的数组转换成一维数组,例如[1,2,3,[4,[5,[6],[7,8]]]],转换后的结果为[1,2,3,4,5,6,7,8]

without

uniq

uniq: function (array, isSorted) {
return _.inject(array, [], function (memo, el, i) {
if (0 == i || (isSorted ? _.last(memo) != el : !_.include(memo, el))) memo.push(el);
return memo;
});
}

数组去重。我们平常的数组去重是这样写的

uniq:function(array){
return array.reduce(function(memo,item,index){
if(memo.indexOf(item)==-1) memo.push(item) //利用reduce,如果array的某一项在memo中找不到,则push到memo中,返回memo,最后的memo就是去重后的数组
return memo
},[])
}

underscore的数组去重和我们的整体思路差不多,不过人家支持第二个参数isSorted。如果传入的数组是排过序的,则采用更快的方法,即不用调用include/indexOf来判断当前的元素是否存在与memo中,只需比较当前元素与memo的最后一项是否相等就ok了。

intersect

zip

indexOf

function (array, item) {
if (array.indexOf) return array.indexOf(item); //如果有array.indexOf,则调用array.indexOf并返回
var length = array.length;
for (i = 0; i < length; i++) if (array[i] === item) return i;//遍历array,返回array[i] === item的数组索引值
return -1;//找不到,返回-1
}

返回item在array中的索引值

函数方法

bind

bind : function(func, context) {
if (!context) return func; //如果没传context(函数执行时的上下文),则返回func
var args = _.toArray(arguments).slice(2);//获取除func、context以外的参数
return function() { 返回一个新的函数(闭包),在这个函数内部,改变func的context并返回执行结果
var a = args.concat(_.toArray(arguments)); //合并函数参数
return func.apply(context, a); //改变func的context并返回执行结果
};
}

类似于ES5的bind函数:改变一个函数的this,并返回新的函数

delay

delay : function(func, wait) {
var args = _.toArray(arguments).slice(2);
return window.setTimeout(function(){ return func.apply(func, args); }, wait); //利用setTimeout延迟执行函数
}

延迟func函数wait毫秒后执行

defer

defer : function(func) {
return _.delay.apply(_, [func, 1].concat(_.toArray(arguments).slice(1))); //调用delay方法:相当于执行 delay(func,1,args)
}

延迟func函数1毫秒后执行

wrap

wrap : function(func, wrapper) {
return function() {
var args = [func].concat(_.toArray(arguments));//合并参数
return wrapper.apply(wrapper, args); //执行wrapper方法,并把args作为参数传给wrapper。arg[0] = func
};
}

将第一个函数 function 封装到函数 wrapper 里面, 并把函数 function 作为第一个参数传给 wrapper.

对象方法

keys

values

extend

extend : function(destination, source) {
for (var property in source) destination[property] = source[property]; //遍历source对象,把source对象的属性赋给destination
return destination;
}

对象合并(浅拷贝)

clone

clone : function(obj) {
return _.extend({}, obj) //调用extend方法
}

对象的浅拷贝

isEqual

isElement

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

通过判断nodeType == 1 ,确定是否是一个元素节点

isArray

isArray : function(obj) {
return Object.prototype.toString.call(obj) == '[object Array]';
}

代码一目了然,现在都用Array.isArray喽

isFunction

isFunction : function(obj) {
return typeof obj == 'function';
}

判断obj是否是一个function

isUndefined

isUndefined : function(obj) {
return typeof obj == 'undefined';
}

//判断obj是否是undefined

工具方法