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

前言

对象函数

// toString 属性名是否可枚举
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
  'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];

// 不可枚举的属性被重写后,在 IE 上仍然是不可枚举属性并且也不会被 for in 遍历,
// 所以这个方法就是为了兼容 IE 9 的,把 nonEnumerableProps 中重写的属性也放到 keys 数组中
var collectNonEnumProps = function (obj, keys) {
  var nonEnumIdx = nonEnumerableProps.length;
  var constructor = obj.constructor;
  // 如果是 constructor 是函数则返回 constructor.prototype 否则 返回对应的原型 ObjProto (Object.prototype)
  var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;


  // Constructor is a special case.
  var prop = 'constructor';
  // 如果对象 obj 中存在 constructor 属性并且 keys 中不含有 constructor,则把 constructor 推进 keys 中
  if (has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);


  // 遍历 nonEnumerableProps
  while (nonEnumIdx--) {
      prop = nonEnumerableProps[nonEnumIdx];
      // 如果 obj 中设置了 nonEnumerableProps 中的同名属性,
      // 并且对象上的此属性的值不等于 proto 上的此属性的值,
      // 同时 prop 属性不存在于 keys 中
      // 把 prop 属性放到 keys 中
      if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
          keys.push(prop);
      }
  }
};


// for ... in ... 遍历对象的除Symbol以外的可枚举属性(含原型上的可枚举属性)
// 获取对象的 key 组成的数组
_.keys = function (obj) {
  // 如果不是对象,直接返回空数组
  if (!_.isObject(obj)) return [];
  // 如果存在原生的 (Object.keys)方法,则使用原生的 keys 方法
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
  // 把仅存在于 obj 中的属性放到 keys 中
  for (var key in obj) if (has(obj, key)) keys.push(key);
  // 如果 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 = [];
  // 把存在于 obj 及其原型链上的属性放到 keys 中
  for (var key in obj) keys.push(key);
  // Ahem, IE < 9.
  if (hasEnumBug) collectNonEnumProps(obj, keys);
  return keys;
};


// 获取对象上的值
_.values = function (obj) {
  // 通过 _.keys() 方法先获取对象上的属性
  var keys = _.keys(obj);
  var length = keys.length;
  var values = Array(length);
  // 通过 keys 长度遍历 obj 对象,并把对应的值放到 values 中
  for (var i = 0; i < length; i++) {
      values[i] = obj[keys[i]];
  }
  return values;
};


// 类似于数组的 _.map() 方法,只不过此方法是处理对象的
_.mapObject = function (obj, iteratee, context) {
  iteratee = cb(iteratee, context);
  var keys = _.keys(obj),
      length = keys.length,
      results = {};
  for (var index = 0; index < length; index++) {
      var currentKey = keys[index];
      results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results;
};


// 把一个对象转换成 [ [key1,value1],[key2,value2],... ] 的二维数组
_.pairs = function (obj) {
  var keys = _.keys(obj);
  var length = keys.length;
  var pairs = Array(length);
  for (var i = 0; i < length; i++) {
      pairs[i] = [keys[i], obj[keys[i]]];
  }
  return pairs;
};


// 生成一个 key 和 值互换的对象
_.invert = function (obj) {
  var result = {};
  var keys = _.keys(obj);
  for (var i = 0, length = keys.length; i < length; i++) {
      result[obj[keys[i]]] = keys[i];
  }
  return result;
};


// 把对象中类型为函数的引用放到一个数组中,并对数组进行排序
_.functions = _.methods = function (obj) {
  var names = [];
  for (var key in obj) {
      if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
};


// 创建一个对象合并函数(同名不覆盖)
var createAssigner = function (keysFunc, defaults) {
  return function (obj) {
      var length = arguments.length;
      if (defaults) obj = Object(obj);
      if (length < 2 || obj == null) return obj;
      for (var index = 1; index < length; index++) {
          var source = arguments[index],
              // 根据 keysFunc 的处理逻辑获取 keys
              keys = keysFunc(source),
              l = keys.length;
          for (var i = 0; i < l; i++) {
              var key = keys[i];
              if (!defaults || obj[key] === void 0) obj[key] = source[key];
          }
      }
      return obj;
  };
};


// 可合并对象上所有的可枚举属性
_.extend = createAssigner(_.allKeys);


// Assigns a given object with all the own properties in the passed-in object(s).
// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)


// 只合并对象自身的可枚举属性
_.extendOwn = _.assign = createAssigner(_.keys);


// 查看对象是否含有指定的 key,如果找到则返回 key
_.findKey = function (obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = _.keys(obj), key;
  for (var i = 0, length = keys.length; i < length; i++) {
      key = keys[i];
      if (predicate(obj[key], key, obj)) return key;
  }
};


// 对象上是否含有某个key
var keyInObj = function (value, key, obj) {
  return key in obj;
};


//
_.pick = restArguments(function (obj, keys) {
  var result = {}, iteratee = keys[0];
  if (obj == null) return result;
  // 如果传入的第二个参数为函数
  if (_.isFunction(iteratee)) {
      // keys 数组的长度大于 1
      // keys[1] 为 key 组成的数组
      if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
      // 获取所有可枚举的属性
      keys = _.allKeys(obj);
  } else {
      // 如果不是函数
      iteratee = keyInObj;
      keys = flatten(keys, false, false);
      obj = Object(obj);
  }
  for (var i = 0, length = keys.length; i < length; i++) {
      var key = keys[i];
      var value = obj[key];
      // 只在符合条件的 key 和 值
      if (iteratee(value, key, obj)) result[key] = value;
  }
  return result;
});


// 获取 obj 中不在 keys 的 key
_.omit = restArguments(function (obj, keys) {
  var iteratee = keys[0], context;
  if (_.isFunction(iteratee)) {
      // 函数结果取反
      iteratee = _.negate(iteratee);
      if (keys.length > 1) context = keys[1];
  } else {
      // 如果不为 iteratee 函数
      // 获取 key
      keys = _.map(flatten(keys, false, false), String);
      // 生新定义 iteratee 函数逻辑
      iteratee = function (value, key) {
          // 查找不在 keys 数组中的 key,并返回布尔值
          return !_.contains(keys, key);
      };
  }
  return _.pick(obj, iteratee, context);
});


// 合并对象(对象上所有的可枚举属性,包括原型上的可枚举属性)
_.defaults = createAssigner(_.allKeys, true);


// 创建一个原型链指向 prototype ,合并 props 对象的对象
_.create = function (prototype, props) {
  var result = baseCreate(prototype);
  if (props) _.extendOwn(result, props);
  return result;
};


// 浅拷贝
_.clone = function (obj) {
  if (!_.isObject(obj)) return obj;
  return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};


// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function (obj, interceptor) {
  interceptor(obj);
  return obj;
};


// attrs 中的所有键和值是否都存在于 object 并且,键与键,值与值完全相等,检测通过则返回 true
_.isMatch = function (object, attrs) {
  var keys = _.keys(attrs), length = keys.length;
  if (object == null) return !length;
  var obj = Object(object);
  for (var i = 0; i < length; i++) {
      var key = keys[i];
      if (attrs[key] !== obj[key] || !(key in obj)) return false;
  }
  return true;
};

// Internal recursive comparison function for `isEqual`.
var eq, deepEq;
eq = function (a, b, aStack, bStack) {
  // Identical objects are equal. `0 === -0`, but they aren't identical.
  // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
  // 处理 0 === -0 不等的情况
  if (a === b) return a !== 0 || 1 / a === 1 / b;
  // 前面已经判断了 null === null 的情况,所以这里只要有一个是 null 或者 undefined, a 与 b 肯定不相等
  if (a == null || b == null) return false;
  // a 不等于自身,如果 b 也一样,说明 a 与 b 都为 NaN
  if (a !== a) return b !== b;
  // Exhaust primitive checks
  var type = typeof a;
  // 如果 a 的类型不是function,不是 object(可能是 string、number、boolean、undefined)
  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
  // 走到这里时基本的类型与基本类型的判断已经完成。下面为多种情况的判断(比如:基本类型与对象、对象与对象......)
  return deepEq(a, b, aStack, bStack);
};


// Internal recursive comparison function for `isEqual`.
deepEq = function (a, b, aStack, bStack) {
  // Unwrap any wrapped objects.
  // underscore可能会用_对象将变量包裹起来,如果是这种情况需要把被包裹的值提取出来才可以进行下一步判断
  if (a instanceof _) a = a._wrapped;
  if (b instanceof _) b = b._wrapped;
  // 获取 a 的类型
  var className = toString.call(a);
  // 先从类型过滤,如果类型不相等直接返回 false
  if (className !== toString.call(b)) return false;
  // 到这里说明 a 与 b 的数据类型相同
  // 下面这个 switch 是判断通过 new 进行创建的变量
  switch (className) {
      case '[object RegExp]':
      // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
      case '[object String]':
          // 若 var a = new String("5"),此时 a (String {"5"})的 Object.prototype.toString.call(a) 的结果也是 "[object String]".
          // 这里使用 ''+a 是把 String {"5"} 的值取出来,相当于 a.valueOf(),后面的几个同理
          return '' + a === '' + b;
      case '[object Number]':
          // 如果是 NaN
          if (+a !== +a) return +b !== +b;
          // 通过+取出a中的值,如果值为 0,则返回 1 / +a === 1 / b 的结果,这里跟前面的(1 / a === 1 / b)其实是一个意思
          return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
          return +a === +b;
      case '[object Symbol]':
          return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
  }
  // 是否是数组
  var areArrays = className === '[object Array]';
  if (!areArrays) {
      // 如果 a 或者 b 有一个类型不为 'object',比如:两个都是函数,则直接返回 false
      if (typeof a != 'object' || typeof b != 'object') return false;
      // 来自不同 frames 的对象,即使键值都一样,但它们是不等的
      var aCtor = a.constructor, bCtor = b.constructor;
      if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
          _.isFunction(bCtor) && bCtor instanceof bCtor)
          && ('constructor' in a && 'constructor' in b)) {
          return false;
      }
  }
  aStack = aStack || [];
  bStack = bStack || [];
  var length = aStack.length;
  while (length--) {
      // 检测循环引用,如果 a 中有循环引用,如果 b 中也正好有对应的循环引用,则返回 true,否则返回 false
      if (aStack[length] === a) return bStack[length] === b;
  }
  // 保存当前对象项
  aStack.push(a);
  bStack.push(b);
  // 递归遍历对象或者数组
  if (areArrays) {
      length = a.length;
      // 数组长度不等,直接返回 false
      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();
  return true;
};

// Perform a deep comparison to check if two objects are equal.
_.isEqual = function (a, b) {
  return eq(a, b);
};

// 就否为空
_.isEmpty = function (obj) {
  // 如果 obj 为 null,返回 true
  if (obj == null) return true;
  // 如果是类数组并且
  if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
  return _.keys(obj).length === 0;
};

// 是否为 HTML 元素节点
_.isElement = function (obj) {
  return !!(obj && obj.nodeType === 1);
};


// 是否为数组
_.isArray = nativeIsArray || function (obj) {
      return toString.call(obj) === '[object Array]';
  };

// 是否为对象
_.isObject = function (obj) {
  var type = typeof obj;
  return type === 'function' || type === 'object' && !!obj;
};

// 批量生成类型判断功能函数
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function (name) {
  _['is' + name] = function (obj) {
      return toString.call(obj) === '[object ' + name + ']';
  };
});

// Define a fallback version of the method in browsers (ahem, IE < 9), where
// there isn't any inspectable "Arguments" type.
if (!_.isArguments(arguments)) {
  _.isArguments = function (obj) {
      return has(obj, 'callee');
  };
}

// Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
// IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
var nodelist = root.document && root.document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
  _.isFunction = function (obj) {
      return typeof obj == 'function' || false;
  };
}

// Is a given object a finite number?
_.isFinite = function (obj) {
  return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj));
};

// Is the given value `NaN`?
_.isNaN = function (obj) {
  return _.isNumber(obj) && isNaN(obj);
};

// 是否为布尔值
_.isBoolean = function (obj) {
  return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};

// 是否为 null
_.isNull = function (obj) {
  return obj === null;
};

// 是否为 undefined
_.isUndefined = function (obj) {
  return obj === void 0;
};

// 深层判断是否含有给定的 key
_.has = function (obj, path) {
  // 如果 path 不是数组
  if (!_.isArray(path)) {
      // 返回 obj 中是否含有 path 的结果(布尔值)
      return has(obj, path);
  }
  var length = path.length;
  for (var i = 0; i < length; i++) {
      var key = path[i];
      if (obj == null || !hasOwnProperty.call(obj, key)) {
          return false;
      }
      obj = obj[key];
  }
  return !!length;
};