underscore源码学习(一)

前言

最近在社区浏览文章的时候,看到了一位大四学长在寻求前端工做中的面经,看完不得不佩服,掌握知识点真是全面,不管是前端后台仍是其余,都有涉猎。javascript

在他写的文章中,有这么一句话,大概意思是,没有看过一个库或者框架的源码还敢出来混。而后本身心虚了一下,一直以来,都只是学习使用框架或库,或者在过程当中有学习框架的思想,但并不深刻。例如,在学习Vue.js中,我曾经去探索过Vue中的双向绑定是如何实现的,经过什么模式,什么API,做者的思想是什么,也曾经实现过简单版的双向绑定。
<!--more-->
可是感受本身在这方面并无什么提升,尤为在原生JavaScript的学习中,一些不经常使用的API常常忘,思惟也不够好。因此有了学习优秀的库的源码的想法,一方面可以学习做者的思想,提升本身的分析能力,另外一方面我以为若是能好好分析一个库的源码,对本身的提高也是有的。前端

因此,刚开始,我从源码比较短的underscore.js(包含注释只有1.5k行)开始学习起。java

什么是underscore

Underscore一个JavaScript实用库,提供了一整套函数式编程的实用功能,可是没有扩展任何JavaScript内置对象。它是这个问题的答案:“若是我在一个空白的HTML页面前坐下, 并但愿当即开始工做, 我须要什么?“...它弥补了部分jQuery没有实现的功能,同时又是Backbone.js必不可少的部分。——摘自Underscore中文文档node

个人学习之路是基于Underscore1.8.3版本开始的。编程

// Current version.
 _.VERSION = '1.8.3';

做用域包裹

与其余第三方库同样,underscore最外层是一个当即执行函数(IIFE),来包裹本身的业务逻辑。通常使用IIFE有以下好处,能够建立一个独立的沙箱似的做用域,避免全局污染,还能够防止其余代码对该函数内部形成影响。(但凡在当即执行函数中声明的函数、变量等,除非是本身想暴露,不然绝无可能在外部得到)数组

(function(){

    // ...执行逻辑
    
}.call(this))

学习的点,当咱们要写本身的库或者封装某个功能函数时,能够给本身的库或函数在最外层包裹一个当即执行函数,这样既不会受外部影响,也不会给外部添麻烦。浏览器

_对象

underscore有下划线的意思,因此underscore经过一个下划线变量_来标识自身,值得注意的是,_是一个函数对象或者说是一个构造函数,而且支持无new调用的构造的函数,全部API都会挂载在这个对象上,如_.each,_.map缓存

var _ = function(obj) {
    if(obj instanceof _) return obj;
    if(!(this instanceof _)) return new _(obj) //实例化
    this._wrapped = obj
}

全局命名空间

underscore使用root变量保存了全局的this安全

var root = this;

为了防止其余库对_的冲突或影响,underscore作了以下处理,app

var previousUnderscore = root._
_.noConflict = function() {
    root._ = perviousUnderscore;
    return this;
}

执行环境判断

underscore 既可以服务于浏览器,又可以服务于诸如 nodejs 所搭建的服务端。
通常,在客户端(浏览器)环境中,_即为window._=_,暴露在全局中。若在node环境中,_将被做为模块导出,而且向后兼容老的API,即require。

if (typeof exports !== 'undefined') {
  if (typeof module !== 'undefined' && module.exports) {
    exports = module.exports = _;  
  }
  exports._ = _ ;
} esle {
  root._ = _;
}

缓存局部变量及快速引用

underscore自己用到了很多ES5的原生方法,在浏览器支持的条件下,underscore率先使用原生的ES5方法。以下代码所示,underscore经过局部变量来保存一些经常使用到的方法或者属性。
这样作有几个好处:

  • 便于压缩代码
  • 提升代码性能,减小在原型链中的查找次数
  • 同时也可减小代码量,避免在使用时冗长的书写
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
var
  push             = ArrayProto.push,
  slice            = ArrayProto.slice,
  toString         = ObjProto.toString,
  hasOwnProperty   = ObjProto.hasOwnProperty;
var
  nativeIsArray      = Array.isArray,
  nativeKeys         = Object.keys,
  nativeBind         = FuncProto.bind,
  nativeCreate       = Object.create;

undefined处理

在underscore中,有不少函数都会有一个context函数,也就是当前函数的执行上下文,underscore对其进行了处理,若是没有传入contextcontextundefined,则返回原函数。
这里判断值为undefined用的是void 0,以下:

if (context === void 0) return func

做为一只涉猎尚浅的小白,查阅资料以后终于知道这里做者为何要用void 0来作判断了。

详情可点连接了解,这样作更加安全可靠。
在还没看到这个代码时, 若是我要判断一个值是否是undefined,我会这样写

if (context === undefined) {}

可是,在发现做者的void 0以后,才发现这样写并不可靠,在JavaScript中,咱们能够这样写:

args => {
  let undefined = 1
  console.log(undefined) // => 1
  if (args === undefined) {
    //...
  }
}

若是这样写,undefined就被轻易地修改成了1,因此对于咱们以后定义的undefined的理解有歧义。因此,在JavaScript中,把undefined直接解释为“未定义”是有风险的,由于它可能被修改。

学习:之后判断undefined直接使用void 0, 看起来也优雅一点(滑稽脸)。

处理类数组

// getLength 函数
// 该函数传入一个参数,返回参数的 length 属性值
// 用来获取 array 以及 arrayLike 元素的 length 属性值
var getLength = property('length');

// 判断是不是 ArrayLike Object
// 类数组,即拥有 length 属性而且 length 属性值为 Number 类型的元素
// 包括数组、arguments、HTML Collection 以及 NodeList 等等
// 包括相似 {length: 10} 这样的对象
// 包括字符串、函数等
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

对象建立的特殊处理

为了处理Object.create的跨浏览器的兼容性,underscore进行了特殊的处理。咱们知道,原型是没法直接实例化的,所以咱们先建立一个空对象,而后将其原型指向这个咱们想要实例化的原型,最后返回该对象其一个实例。其代码以下:

var Ctor = function() {};  // 用于代理原型转换的空函数

var baseCreate = function(prototype) {
  if (!(_.isObject(prototype))) return {}; // 若是参数不是对象,直接返回空对象
  if (nativeCreate) return nativeCreate(prototype); // 若是原生的对象建立可使用,返回该方法根据原型建立的对象
    
  // 处理没有原生对象建立的状况
  Ctor.prototype = prototype;  // 将空函数的原型指向要使用的原型
  var result = new Ctor();  // 建立一个实例
  Ctor.prototype = null;  // 恢复Ctor的原型供下次使用
  return result;  // 返回该实例
};

underscore中的迭代(iteratee)

在函数式编程中,使用更多的是迭代,而不是循环。
迭代:

var res = _.map([1,2], function(item){
  return item * 2
})

循环:

var arr = [1,2]
var res = []
for(var i = 0; i < arr.length; i++) {
  res.push(arr[i] * 2)
}

在underscore中迭代使用很是巧妙,源码也写的很是好,经过传入的数据类型不一样而选择不一样的迭代函数。
首先,在underscore中_.map的实现以下:

_.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) //(value, index, obj)
  }
  return results;
}

能够看到,在_.map函数中的第二个参数iteratee,这个参数的格式能够是函数,对象,字符串。underscore会将其处理成一个函数,这将由回调函数cb来完成,咱们来看一下cb的实现:

var cb = function(value, context, argCount) {
 // 是否用默认的迭代器 若是没有传入value 则返回当前迭代元素自身
 if (value == null) return _.identity;
 // 若是value是一个回调函数, 则须要优化回调 优化函数为optimizeCb
 if (_.isFunction(value)) return optimizeCb(value, context, argCount);
 // 若是value是个对象, 则返回一个matcher进行对象匹配
 if (_.isObject(value)) return _.matcher(value)
 // 不然, 若是value只是一个字面量, 则把value看作是属性名称, 返回一个对应的属性得到函数
 return _.property(value);
}

前面两个比较容易理解,看看当传入的数据格式为对象的状况,若是 value 传入的是一个对象,那么返回iteratee(_.matcher)的目的是想要知道当前被迭代元素是否匹配给定的这个对象:

var results = _.map([{name:'water'},{name: 'lzb',age:13}], {name: 'lzb'});
// => results: [false,true]

若是传入的是字面量,如数字,字符串等, 他会返回对应的key值,以下:

var results = _.map([{name:'water'},{name:'lzb'}],'name');
// => results: ['water', 'lzb'];

回调处理

在上面的cb函数中,咱们能够看到,当传入的数据格式是函数,则须要经过optimizeCb函数进行统一处理,返回对应的回调函数,下面是underscore中optimizeCb函数的实现:

// 回调处理
// underscore 内部方法
// 根据 this 指向(context 参数)
// 以及 argCount 参数
// 二次操做返回一些回调、迭代方法
var optimizeCb = function(func, context, argCount) {
  // // void 0 会返回纯正的undefined,这样作避免undefined已经被污染带来的断定失效
  if (context === void 0) return func;
  switch (argCount == null ? 3 : argCount) {
    // 回调参数为1时, 即迭代过程当中,咱们只须要值
    // _.times
    case 1: return function(value) {
      return func.call(context, value);
    };
    case 2: return function(value, other) {
      return func.call(context, value, other);
    };
    // 3个参数(值,索引,被迭代集合对象)
    // _.each、_.map  (value, key, obj)
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    // 4个参数(累加器(好比reducer须要的), 值, 索引, 被迭代集合对象)
    // _.reduce、_.reduceRight
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }

  // 若是都不符合上述的任一条件,直接使用apply调用相关函数
  return function() {
    return func.apply(context, arguments);
  };
}

optimizeCb 的整体思路就是:传入待优化的回调函数 func,以及迭代回调须要的参数个数argCount,根据参数个数分状况进行优化。

在underscore的_.times函数视线中,_times的做用执行一个传入iteratee函数n次,并返回由每次执行结果组成的数组。它的迭代过程iteratee只须要1个参数(当前迭代的索引)
_.times函数在underscore中的实现:

_.times = function(n, iteratee, context) {
  vat accum = Array(Math.max(0, n));
  iteratee = optimizeCb(iteratee, context, 1);
  for (var i = 0; i < n; i++) accum[i] = iteratee(i);
  return accum;
}

_.times的使用

function getIndex(index) {
  return index;
}
var results = _.times(3, getIndex); // => [0,1,2]

optimizeCb函数中当argCount的个数为2的状况并不常见,在_.each,_.map等函数中,argCount的值为3(value, key, obj),当argCount须要四个参数时,这四个参数的格式为:

  • accumulator:累加器
  • value:迭代元素
  • index:迭代索引
  • collection:当前迭代集合

underscore中reduce的实现以下:

/**
 * reduce函数的工厂函数, 用于生成一个reducer, 经过参数决定reduce的方向
 * @param dir 方向 left or right
 * @returns {function}
 */
function createReduce(dir) {
  function iterator(obj, iteratee, memo, keys, index, length) {
    for(; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      // memo 用来记录最新的 reduce 结果
      // 执行 reduce 回调, 刷新当前值
      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;
    if (arguments.length < 3) {
      // 若是没有传入memo初始值 则从左第一个为初始值 从右则最后一个为初始值
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    // return func
    return iterator(obj, iteratee, memo, keys, index, length);
  }
}

例如在_.reduce、_.reduceRight中,argCount的值为4。看看underscore中_.reduce的使用例子

var sum = _.reduce([1,2,3,4], function(accumulator, value, index, collection){
  return accumulator + value;
}, 0) // => 10
相关文章
相关标签/搜索