js高手进阶之路:underscore源码经典
发布在web前端到js全栈的成长之路2015年10月28日view:3425
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

underscore 源码版本 1.8.2

起因

很多人向我推荐研究js,可以看看一些第三方js类库的源码,而源码之中最好解读也最简短的就是underscore,它也是我平常比较喜欢的一个库,因为它性价比高:体积小、能力强。打开一看,才1000多行,试着读了一下,确实很值得一看,所以对精彩部分做了一下整理。

闭包

整个函数在一个闭包中,避免污染全局变量。通过传入this(其实就是window对象)来改变函数的作用域。和jquery的自执行函数其实是异曲同工之妙。这种传入全局变量的方式一方面有利于代码阅读,另一方面方便压缩。 underscore写法:

(function(){
    ...
}.call(this));

jquery写法:

(function(window, undefined) {
    ...
})(window);
<!-- more -->

原型赋值

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;
}

这里是对简单对象进行判断,分为两类,一类是StringRegExp,这种数据直接toString然后判断。另一类是NumberDateBoolean,通过转换成数字判断。

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

先看一下两个比较重要的内部函数 <!-- more -->

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)来传入该参数比较有意思。

后面的代码真是越看越难理解,经常需要结合内部/接口函数一起,所以采用跳读的方式解析,基本按照模块由易到难的顺序解析。 <!-- more -->

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中的值。

对象

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;
  };

keysallKeys这两个函数很有对比意义。 前者是该对象的枚举属性,先用默认函数处理,再考虑循环遍历,最后考虑老版本浏览器的时候返回默认属性解决方案。 后者是该对象的枚举属性以及继承的属性,直接进行深度遍历,然后考虑老版本浏览器的时候直接返回默认属性解决方案。

<!-- more -->
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闭包特性的学习一探函数式编程的精妙。


博客:http://yalishizhude.github.io 作者:亚里士朱德

评论
发表评论
4年前
赞了此文章!
WRITTEN BY
亚里士朱德
前端工程师,jsp工程师,大型国际oss项目开发经验,擅长开发PC网站以及微信应用。
TA的新浪微博
PUBLISHED IN
web前端到js全栈的成长之路

web前端和js后端以及数据库的知识整理

我的收藏