underscore源码经典(完)
最后更新于:2022-04-01 04:59:34
## 对象
~~~
921 _.keys = function(obj) {
if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
// Retrieve all the property names of an object.
_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
~~~
`keys`和`allKeys`这两个函数很有对比意义。
前者是该对象的枚举属性,先用默认函数处理,再考虑循环遍历,最后考虑老版本浏览器的时候返回默认属性解决方案。
后者是该对象的枚举属性以及继承的属性,直接进行深度遍历,然后考虑老版本浏览器的时候直接返回默认属性解决方案。
~~~
31 nativeKeys = Object.keys,
~~~
`Object`自带的这个`keys`函数来判断对象的枚举属性很方便,不过也是ECMAScript5新增的函数。
~~~
1260 _.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};
~~~
`hasOwnProperty`函数可以有效的判断枚举属性。
## 函数
~~~
758 _.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){
return func.apply(null, args);
}, wait);
};
~~~
一个闭包的好例子,把参数缓存起来供延迟函数加载。
~~~
870 _.after = function(times, func) {
return function() {
if (--times < 1) {
return func.apply(this, arguments);
}
};
};
~~~
这个代码没有太多新意,不过这个函数的使用方法很有意思。但用于多个异步请求的时候非常有用。例如这个例子:
~~~
function render(){//...}
var renderNotes = _.after(notes.length, render);
_.each(notes, function(note) {
note.asyncSave({success: renderNotes});
});
~~~
## 总结
underscore之所以经典,一方面它是一个JavaScript实用库,提供了很多工具函数,可以有效地减少我们的代码量,尤其是在对数组和对象的操作上;另一方面是其体现了JavaScript函数式编程的特点,源码中随处可见函数的嵌套。像函数式编程的无状态、高阶编程这些特点和闭包都有着紧密的联系,所以下一阶段希望能透过JavaScript闭包特性的学习一探函数式编程的精妙。
underscore源码经典(三)
最后更新于:2022-04-01 04:59:32
后面的代码真是越看越难理解,经常需要结合内部/接口函数一起,所以采用跳读的方式解析,基本按照模块由易到难的顺序解析。
~~~
456 _.initial = function(array, n, guard) {
return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
};
~~~
这个guard参数说是保证默认返回第一个值,不过我看了很久觉得意义不大,猜测可能是以前的接口。这里的`slice = Array.prototype.slice`。这里有两个地方比较好,一是用Array的原型函数来处理array参数,这样可以避免判断array是否数组,二是用max来避免超出数组长度。
~~~
671 _.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = step || 1;
var length = Math.max(Math.ceil((stop - start) / step), 0);
var range = Array(length);
for (var idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
};
~~~
这个函数本什没有什么特别的,只不过我在调用的时候将step赋值为小数,就出现了javascript著名的精度问题。网上应该有不少的解决方案,这里提供一种目前我在项目中用到的解决方法:将数字转为字符串在去小数点之后转为整数进行计算,计算完成后转字符串加上小数点再转成小数,举个例子
~~~
1.11 ==> "1.11" ==> "111" ==> 111
2.2 ==> "2.2" ==> "220" ==> 220
111+220 ==> 331 ==> "331" ==> "3.31" ==> 3.31
~~~
数组代码看得累了,从后面的看起吧~
~~~
1269 _.noConflict = function() {
root._ = previousUnderscore;
return this;
};
~~~
这个让渡函数其实原理比较简单,就是初始化underscore前 `previousUnderscore=window._`对原有对象进行储存,如果需要让渡避免冲突,则进行还原同时返回当前underscore对象。再来看看JQuery的实现原理:
~~~
noConflict: function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
}
~~~
刚开始看的时候犯2了,函数可以用`===`来比较?不是只能比较简单的数据对象?呵呵,其实这个是通过引用地址来判断的,因为之前初始化的时候`window.$=JQuery`所以$会指向JQuery的地址,所以可以这样判断。后面一个if考虑到JQuery这个变量名也被占用的时候,也进行让渡,最后返回JQuery对象。可能是考虑多版本的情况吧~
~~~
1487 _.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result(this, func.apply(_, args));
};
});
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
~~~
这个用于扩展underscore自身的接口函数,通过循环遍历对象来浅拷贝对象属性。这里考虑到函数的实例化,所以一方面将函数作为内部函数使用,同时扩展到prototype属性上。考虑得比较周全。
~~~
1469 _.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
~~~
这个函数让underscore支持链式调用,链式调用写起来很美观,但是调试略显不方便。结合之前的代码来看,underscore实现链式调用的基本原理是:将参数缓存在内部属性_wrapped中,调用函数时传入该属性值,执行后返回当前this指针。直到执行value函数时返回_wraped中的值。
underscore源码经典(二)
最后更新于:2022-04-01 04:59:30
先看一下两个比较重要的内部函数
~~~
63 var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
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);
};
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
~~~
这个函数是underscore内部很重要的函数,主要用来执行函数并改变所执行函数的作用域,最后加了一个argCount参数来指定参数个数,对参数个数小于等于4的情况进行分类处理。对不同参数的解释大概是:
1的情况一般是用在接受单值的情况,比如times,sortedIndex之类的函数。
2的情况据说是给比如jQuery,zepto事件绑定,代理什么的,但是在源代码中没有看到被调用。
3的情况用于迭代器函数,比如foreach,map,pick等。
4的情况用reduce和reduceRight函数。
~~~
87 var cb = function(value, context, argCount) {
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matcher(value);
return _.property(value);
};
~~~
这也是一个比较常用的内部函数,只是对参数进行了判断:如果是函数则返回上面说到的回调函数;如果是对象则返回一个能判断对象是否相等的函数;默认返回一个获取对象属性的函数。
~~~
140 _.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
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;
};
// Return the results of applying the iteratee to each element.
_.map = _.collect = function(obj, iteratee, context) {
iteratee = cb(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;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
~~~
从代码上看,each函数是用来遍历操作数组/对象,map用来处理数组/对象后并以数组的形式返回结果。至于forEach和collect在API文档中看不到,应该是为了兼容以前老版本做的别名处理。
~~~
170 function createReduce(dir) {
// Optimized iterator function as using arguments.length
// in the main function will deoptimize the, see #1991.
function iterator(obj, iteratee, memo, keys, index, length) {
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
}
return function(obj, iteratee, memo, context) {
iteratee = optimizeCb(iteratee, context, 4);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
// Determine the initial value if none is provided.
if (arguments.length < 3) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
return iterator(obj, iteratee, memo, keys, index, length);
};
}
~~~
这个是reduce和reduceRight调用的内部函数,将memo这个变量作为入参传递给iterator函数,调用自定义的iteratee函数进行循环处理,每次处理完的结果都赋值给memo变量,最后返回memo变量的结果。这里有两个问题
* 为什么这里不按照常理逻辑来写代码而要用闭包呢?闭包大致有这么几个作用:避免命名冲突;私有化变量;变量持久化。这里的作用主要就是变量(函数)持久化,好处就是重复调用的时候不需要再重新创建函数,从而提升执行速度。
* 为什么要用两层闭包呢?第一层闭包持久化iterator函数,调用reduce和reduceRight函数避免重复新建函数。第二层闭包保存keys,index,length这些变量。
263 _.invoke = function(obj, method) {
~~~
var args = slice.call(arguments, 2);
var isFunc = _.isFunction(method);
return _.map(obj, function(value) {
var func = isFunc ? method : value[method];
return func == null ? func : func.apply(value, args);
});
~~~
};
这里用`slice.call(arguments, 2)`来获取后面的不定参数,然后用`func.apply(value, args)`来传入该参数比较有意思。
underscore源码经典(一)
最后更新于:2022-04-01 04:59:28
> 原文:[http://yalishizhude.github.io](http://yalishizhude.github.io/2015/09/22/underscore-source/)
> 作者:[亚里士朱德](http://yalishizhude.github.io/about/)
> underscore 源码版本 1.8.2
## 起因
很多人向我推荐研究js,可以看看一些第三方js类库的源码,而源码之中最好解读也最简短的就是underscore,它也是我平常比较喜欢的一个库,因为它性价比高:体积小、能力强。打开一看,才1000多行,试着读了一下,确实很值得一看,所以对精彩部分做了一下整理。
## 闭包
整个函数在一个闭包中,避免污染全局变量。通过传入this(其实就是window对象)来改变函数的作用域。和jquery的自执行函数其实是异曲同工之妙。这种传入全局变量的方式一方面有利于代码阅读,另一方面方便压缩。
underscore写法:
~~~
(function(){
...
}.call(this));
~~~
jquery写法:
~~~
(function(window, undefined) {
...
})(window);
~~~
## 原型赋值
~~~
18 var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
~~~
Array,Object,Function这些本质都是函数,获取函数原型属性prototype也是为了便于压缩。简单解释一下,如果代码中要扩展属性,可能这样写
~~~
Object.prototype.xxx = ...
~~~
而这种代码是不可压缩的,`Object`,`prototype`这些名字改了浏览器就不认得了。
但是上面的代码中创建了`ObjProto`之后,源生代码经过压缩之后,`ObjProto`就可能命名成a变量,那么原来的代码就压缩成
~~~
a.xxx = ...
~~~
一个小建议就是凡事一段代码被使用两次以上都建议定义变量(函数),有利于修改和压缩代码。
## 格式
~~~
29 var
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind,
nativeCreate = Object.create;
~~~
这种定义的方式省略了多余的var,格式也美观,让我想到了sublime中的一个插件alignment。
## 数据判断
~~~
1194 _.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};
~~~
判断是否为dom,dom的nodeType属性值为1。这里用`!!`强转为boolean值
~~~
1200 _.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
~~~
判断是否为数组。由于Array.isArray函数是ECMAScript 5新增函数,所以为了兼容之前的版本,在原生判断函数不存在的情况下,后面重写了一个判断函数。用call函数来改变作用域可以避免当obj没有toString函数报错的情况。
~~~
1205 _.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
~~~
判断是否为对象。先用typeof判断数据类型。函数也属于对象,但是由于typeof null也是object,所以用!!obj来区分这种情况。
~~~
1219 if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return _.has(obj, 'callee');
};
}
~~~
判断是否为arguments,很简单,arguments有个特有属性callee。
~~~
1239 _.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
~~~
NaN这个值有两个特点:1.它是一个数;2.不等于它自己。
‘+’放在变量前面一般作用是把后面的变量变成一个数,在这里已经判断为一个数仍加上’+’,是为了把`var num = new Number()`这种没有值的数字也归为NaN。
~~~
1244 _.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};
~~~
是不是以为如果是布尔值不是true就是false?还有第3中情况`var b = new Boolean()`。b也是布尔值。
~~~
1254 _.isUndefined = function(obj) {
return obj === void 0;
};
~~~
用void 0来表示undefined,非常有意思的小技巧。不过常用方式还是if(xxx)来判断是不是undefined。
`eq`是underscore的一个内置函数,代码太长,不粘贴了。isEmpty调用了这个函数。整个思路由易到难,先用===比较简单数据,然后用toString来判断是否相等,最后用递归处理复杂的Array、Function和Object对象。
~~~
1091 if (a === b) return a !== 0 || 1 / a === 1 / b;
~~~
这里为了区分’+0’和’-0’,因为这两个数对计算结果是有影响的。
~~~
1098 var className = toString.call(a);
if (className !== toString.call(b)) return false;
switch (className) {
// Strings, numbers, regular expressions, dates, and booleans are compared by value.
case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return '' + a === '' + b;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a === +b;
}
~~~
这里是对简单对象进行判断,分为两类,一类是`String`和`RegExp`,这种数据直接`toString`然后判断。另一类是`Number`、`Date`和`Boolean`,通过转换成数字判断。
~~~
1150 aStack.push(a);
bStack.push(b);
if (areArrays) {
length = a.length;
if (length !== b.length) return false;
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} else {
var keys = _.keys(a), key;
length = keys.length;
if (_.keys(b).length !== length) return false;
while (length--) {
key = keys[length];
if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
aStack.pop();
bStack.pop();
~~~
对于数组和对象只能用递归了,同时用aStack和bStack来暂存递归中的子对象。这里一个小技巧的就是先判断数组/属性的长度,如果不相等可以有效地减少递归。