Underscore.js 源码分析(第一出)

前言

ES6 虽然现在越来越普及,甚至有人认为已经无 ES6 不可以地步,但至少现阶段来说 underscore.js 工具库也还有存在的必要。分析源码不是熟悉或者说是记住源码中的每一个方法,毕竟 API 放哪,记不住的去翻阅一下就行。阅读一些库的源码是为了提升自身能力,看看别人的库是怎么组织代码,一些方法又是如何实现的,同时你会发现源码中的一些用法可能你从来都没有见过。

我打算把 underscore.js 的源码看一遍,但是每次只阅读几个方法,因为我知道欲速则不达,况且也不会让自己看吐。这篇文章则是选把库里的内置方法先研究一番,毕竟这些方法是根基。那我们开始吧!

源码链接:https://github.com/jashkenas/underscore/blob/master/underscore.js

var root = typeof self == 'object' && self.self === self && self ||
            typeof global == 'object' && global.global === global && global ||
            this ||
            {};

首先要想读懂这段代码,你应该具备逻辑运算符的一些规则知识,比如:优先权、有返回值。

创建一个根对象,这段代码一看很容易让人不解。比如最后面为什么还跟了一个 && self 或者 && global。&& 的用法为如果符号左则为 true 会继续运算符号右则的值如果也为 true,则会返回符号右则的值,如果是多个 && 连用,则以此类推,即如果所有的为 true ,则最后一个 && 右则的值会作为最终返回值。而如果中间有一个值不为真,则会立即中止运算,并返回此值。

上面的代码主要是用 root 变量保存当前运行环境的全局对象,浏览器端为 self(等同于 window),服务器端为 global。运行环境如果是浏览器则取浏览器,如果是服务器端则取 global,如果两者都不是,则取 this ,而如果连 this 都不存在那么直接赋值一个新的对象。

// 保存初始值
var previousUnderscore = root._;

// 保存一些对象的原型(Array、Object、Symbol),以便后面更方便的使用,对于减少代码量有一定的帮助。
var ArrayProto = Array.prototype, ObjProto = Object.prototype;
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;

// 对 Array、Object 原型上的一些方法创建快捷访问(保存到变量).
var push = ArrayProto.push,
    slice = ArrayProto.slice,
    toString = ObjProto.toString,
    hasOwnProperty = ObjProto.hasOwnProperty;

// **ECMAScript 5** 源生支持的方法也作一个临时的保存.
var nativeIsArray = Array.isArray,
    nativeKeys = Object.keys,
    nativeCreate = Object.create;

以上的代码有两个作用,1. 创建快捷访问,2.减少代码量。

// 创建一个空间函数.
var Ctor = function(){};

// 创建一个 指向 underscore 的安全对象供后面使用.
var _ = function(obj) {
  if (obj instanceof _) return obj; // 如果参数 obj 为 _ 的实例,那么直接返回 obj
  if (!(this instanceof _)) return new _(obj); // 如果上下文不是 _ 的实例,那么就 new _ 实例并返回。
  this._wrapped = obj; // 把 obj 挂载到 上下文的 _wrapped 属性上。
};

上面的代码比较简单。

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

  // 定义当前版本.
  _.VERSION = '1.9.1';

其中 exports.nodeType 和 module.nodeType 中的 nodeType 是不是有点眼熟,在判断元素节点类型时就是通过它来判断的,那为什么这里要把这个排除掉,为什么要这么写呢?因为这里有一种神奇的存在,就是如果你给元素添加了一个 id 属性,那么这个属性名在 JS 中是可以直接获取到的。换句话说,这个变量直接挂载到了 window 对象上。但 class 属性即不会。所以库通过判断变量上是否存在 nodeType 如果存在说明这两个变量(module / exports)是来自 HTML 中的 ID,而不是环境本身存在这两个对象。如果环境支持则导出 _ 对象,并且把 _ 对象也挂载到exports._属性上,如果不支持 module/exports 则把 _ 挂载到前面定义好的 root._ 属性上。

var optimizeCb = function(func, context, argCount) {
  if (context === void 0) return func;          // 如果 context 没有传则直接返回 func
  switch (argCount == null ? 3 : argCount) {    // 如果 argCount 为 null,则取 3,否则返回 argCount 值
    case 1: return function(value) {
      return func.call(context, value);         // 把 value 作为 func 函数参数,同把 func 的 this 指向 context,最后返回 func 的运行结果
    };
    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);
  };
};

// 上面对 argCount 为 3 和 4 的值为了传门的处理,这里我估计这两个是专门用来处理数组及其它一些特殊的回调函数。最后如果前面几个都不符合,则直接使用 applay() 来处理函数,即修改函数中的 this // 指向 context 以及把所有参数原数传给 func 函数。后面我们遇到有用到的地方再具体说说。

var builtinIteratee;

var cb = function(value, context, argCount) {
  // 如果 _.iteratee 与 builtinIteratee 不等,说明用户自定义了_.iteratoee() 方法,直接返回用户自定义的 _.iteratoee() 方法
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  // 如果value 为 null,则返回 _.identity 方法
  if (value == null) return _.identity;
  // 如果是一个函数,则返回 optimizeCb() 的执行结果
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  // 如果是对象则返回 _.matcher(value) 的执行结果(一个匿名函数,这个匿名函数执行后返回一个布尔值,表示一个对象是否包含给定的对象的所有键值对)
  if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
  // 如果以上都不是,则返回 _.property() 方法的执行结果,这个方法的处理逻辑为,如果不是数组返回 shallowProperty() 方法的选择结果,则否返回 deepGet() 的执行结果
  return _.property(value);
};

// 这里对 cb 再进行一层封装,对外只暴露 value 和 context 两个参数
_.iteratee = builtinIteratee = function(value, context) {
  return cb(value, context, Infinity);
};

这个方法就是实现 ES6 中的 ...rest 操作符功能

var restArguments = function(func, startIndex) {
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  return function() {
    // 求出最大值
    var length = Math.max(arguments.length - startIndex, 0),
    // 创建一个 length 长度的数组
        rest = Array(length),
        index = 0;
    // 循环把原数组中的从下标为 startindex 开始的元素有序放到 rest 数组中
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    switch (startIndex) {
      // 如果是第 0 个开始,把所有的参数收到 rest 数组中
      case 0: return func.call(this, rest);
      // 如果是第 1 个开始,则把下标为 1 及以后的参数收到 rest 数组中
      case 1: return func.call(this, arguments[0], rest);
      // 如果是第 2 个开始,则把下标为 2 及以后的参数收到 rest 数组中
      case 2: return func.call(this, arguments[0], arguments[1], rest);
    }
    // ====== 如果以上都不满足 ,执行如下代码 ======
    // 创建一个长度为 startIndex + 1 的数组
    var args = Array(startIndex + 1);
    // 遍历把下标小于 startIndex 的所有参数收到 args 数组中
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    // 把 rest 数组作为 args 的最后一个元素
    args[startIndex] = rest;
    // 通过 applay 调用 func 函数,再把 args 数组展开(处理成前面 switch case 返回的函数调用形式)
    return func.apply(this, args);
  };
};

var baseCreate = function(prototype) {
  // 如果 prototype 不是一个对象,则直接返回一个空对象
  if (!_.isObject(prototype)) return {};
  // 如果  nativeCreate(Object.create)存在则使用 nativeCreate 创建一个对象
  if (nativeCreate) return nativeCreate(prototype);
  // 把 prototype 对象挂到 Ctor 的原型上
  Ctor.prototype = prototype;
  // new 一个 Ctor 实例
  var result = new Ctor;
  // 取消 Ctor.prototype 对 prototype 的引用
  Ctor.prototype = null;
  // 返回实例对象 result
  return result;
};


// 返回一个可以判断一个对象是否含有指定 key 对应值的匿名函数
var shallowProperty = function(key) {
  return function(obj) {
    // 如果 obj 为 null 返回 void 0 (等同于 undefined),否则返回指定的 key 值
    return obj == null ? void 0 : obj[key];
  };
};


// 判断对象自身是否含有某个属性
var has = function(obj, path) {
  return obj != null && hasOwnProperty.call(obj, path);
}


// 深度获取对象某个属性(嵌套对象中的属性)
var deepGet = function(obj, path) {
  // path 是一个数组
  var length = path.length;
  for (var i = 0; i < length; i++) {
    // 如果 obj 为 null 则返回 viod 0 的运算结果 undefined
    if (obj == null) return void 0;
    // 把数组的第 i 项对为 key 值从 obj 取出,并赋值给 obj
    obj = obj[path[i]];
  }
  return length ? obj : void 0;
};


// 最大的数组索引
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;


// 保存获取对象的 length 属性方法。
var getLength = shallowProperty('length');


// 判断是不是一个数组(包括类数组)
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};


// 可以创建一个可以收缩遍历的函数
var createReduce = function(dir) {
  // Wrap code that reassigns argument variables in a separate function than
  // the one that accesses `arguments.length` to avoid a perf hit. (#1991)
  var reducer = function(obj, iteratee, memo, initial) {
    // 如果 obj 不是数组则返回 _.keys(obj) 的执行结果
    var keys = !isArrayLike(obj) && _.keys(obj),
    // 如果 keys 不为 false 说明 keys 为一个对象,否则 obj 为数组,取出 length
        length = (keys || obj).length,
        // 如果 dir 大于 0 则从第一个开始,否则从最后一个开始
        index = dir > 0 ? 0 : length - 1;
        // 如果 initial 不为真(没设置初始值)
    if (!initial) {
      // 如果 keys 不为 false 并且有值(即 obj 为对象),则从 obj 中取出 keys 数组中的第 index 值作为 key 值的值,
      // 如果 keys 为 false 说明 obj 为数组直接取下标为 index 的项
      // 保存第一个值作为初始值
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    // 遍历 memo 作为累加的变量
    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) {
    var initial = arguments.length >= 3;
    // optimizeCb(iteratee, context, 4) 返回预置的遍历函数 function(accumulator, value, index, collection){} , accumulator 参数为累加器
    return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
  };
};