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来暂存递归中的子对象。这里一个小技巧的就是先判断数组/属性的长度,如果不相等可以有效地减少递归。
';