lodash中遍历方法用到的iteratee

lodash中有很多方法都涉及到了数组或者对象的遍历,一般这些方法都可以传递自定义的遍历方法,自定义的遍历方法在普通情况下都传递的是function,但是lodash也支持传递一个数组、一个对象,或者一个字符串。

这个iteratee方法的任务就是把一个数组,一个对象,或者一个字符串变成一个有效的function来遍历数组或对象找到符合要求的属性。

其中用到了isEqual方法来深度比较两个对象的值是否相等。

也用到了property将字符串或者字符串形式的属性路径变成一个获取对象的对应的属性的function。

以下是源代码:

涉及Hash,ListCache,MapCache这三个自定义类型没有注释,其他方法都注释了。

Hash类型

//Hash类构造函数,接收参数是一个包含键值对数组的数组:
//[['key1', 'values1'], ['key2', 'values2']......]
Hash就是用对象实现了一个带有特定接口的哈希表

ListCache类型

//ListCache构造函数,ListCache其实就是一个自己实现的Map数据类型
  //参数entries是键值对数组,结构如下
  /*
    [
      [key1, value1],
      [key2, value2],
      ...
    ]
  */

MapCache

MapCache类型构造函数,创建一个map缓存对象来储存键值对
/*
  SetCacheObject
  {
    __data__: {
      size: xxx,
      __data__: {
        hash: new Hash,
        map: new Map,
        string: new Hash
      }
    }
  }

  =>
  {
    __data__: {
      size: xxx,
      __data__: {
        hash: {
          size: xxx,
          __data__: {
            values4: '__lodash_hash_undefined__',
            values6: '__lodash_hash_undefined__',
            ...
          }
        },
        map: new Map,
        string: {
          size: xxx,
          __data__: {
            values3: '__lodash_hash_undefined__',
            values8: '__lodash_hash_undefined__',
            ...
          }
        }
      }
    }
  }
*/

下面是iteratee全部源码:

/**
 * lodash (Custom Build) <https://lodash.com/>
 * Build: `lodash modularize exports="npm" -o ./`
 * Copyright jQuery Foundation and other contributors <https://jquery.org/>
 * Released under MIT license <https://lodash.com/license>
 * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
 */

/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;

/** Used as the `TypeError` message for "Functions" methods. */
var FUNC_ERROR_TEXT = 'Expected a function';

/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';

/** Used to compose bitmasks for comparison styles. */
var UNORDERED_COMPARE_FLAG = 1,
    PARTIAL_COMPARE_FLAG = 2;

/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0,
    MAX_SAFE_INTEGER = 9007199254740991;

/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
    arrayTag = '[object Array]',
    boolTag = '[object Boolean]',
    dateTag = '[object Date]',
    errorTag = '[object Error]',
    funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]',
    mapTag = '[object Map]',
    numberTag = '[object Number]',
    objectTag = '[object Object]',
    promiseTag = '[object Promise]',
    regexpTag = '[object RegExp]',
    setTag = '[object Set]',
    stringTag = '[object String]',
    symbolTag = '[object Symbol]',
    weakMapTag = '[object WeakMap]';

var arrayBufferTag = '[object ArrayBuffer]',
    dataViewTag = '[object DataView]',
    float32Tag = '[object Float32Array]',
    float64Tag = '[object Float64Array]',
    int8Tag = '[object Int8Array]',
    int16Tag = '[object Int16Array]',
    int32Tag = '[object Int32Array]',
    uint8Tag = '[object Uint8Array]',
    uint8ClampedTag = '[object Uint8ClampedArray]',
    uint16Tag = '[object Uint16Array]',
    uint32Tag = '[object Uint32Array]';

/** Used to match property names within property paths. */
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
    reIsPlainProp = /^\w*$/,
    reLeadingDot = /^\./,
    rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;

/**
 * Used to match `RegExp`
 * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
 */
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;

/** Used to match backslashes in property paths. */
var reEscapeChar = /\\(\\)?/g;

/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;

/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;

/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;

/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
typedArrayTags[uint32Tag] = true;
typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
typedArrayTags[errorTag] = typedArrayTags[funcTag] =
typedArrayTags[mapTag] = typedArrayTags[numberTag] =
typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
typedArrayTags[setTag] = typedArrayTags[stringTag] =
typedArrayTags[weakMapTag] = false;

/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[argsTag] = cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
cloneableTags[boolTag] = cloneableTags[dateTag] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag] =
cloneableTags[numberTag] = cloneableTags[objectTag] =
cloneableTags[regexpTag] = cloneableTags[setTag] =
cloneableTags[stringTag] = cloneableTags[symbolTag] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;
cloneableTags[errorTag] = cloneableTags[funcTag] =
cloneableTags[weakMapTag] = false;

/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();

/** Detect free variable `exports`. */
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;

/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports && freeGlobal.process;

/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
  try {
    return freeProcess && freeProcess.binding('util');
  } catch (e) {}
}());

/* Node.js helper references. */
var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;

/**
 * Adds the key-value `pair` to `map`.
 *
 * @private
 * @param {Object} map The map to modify.
 * @param {Array} pair The key-value pair to add.
 * @returns {Object} Returns `map`.
 */
//添加键值对到map对象中
function addMapEntry(map, pair) {
  // Don't return `map.set` because it's not chainable in IE 11.
  map.set(pair[0], pair[1]);//调用map的set方法设置键值
  return map;
}

/**
 * Adds `value` to `set`.
 *
 * @private
 * @param {Object} set The set to modify.
 * @param {*} value The value to add.
 * @returns {Object} Returns `set`.
 */
function addSetEntry(set, value) {
  // Don't return `set.add` because it's not chainable in IE 11.
  set.add(value);
  return set;
}

/**
 * A specialized version of `_.forEach` for arrays without support for
 * iteratee shorthands.
 *
 * @private
 * @param {Array} [array] The array to iterate over.
 * @param {Function} iteratee The function invoked per iteration.
 * @returns {Array} Returns `array`.
 */
//类似forEach
function arrayEach(array, iteratee) {
  var index = -1,//循环索引
      length = array ? array.length : 0;//array数组长度

  while (++index < length) {//循环数组长度次数,调用iteratee,如果返回值是false,就提前跳出循环
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}

/**
 * Appends the elements of `values` to `array`.
 *
 * @private
 * @param {Array} array The array to modify.
 * @param {Array} values The values to append.
 * @returns {Array} Returns `array`.
 */
//将values数组的元素插入到array数组的结尾
function arrayPush(array, values) {
  var index = -1,//循环索引
      length = values.length,//values的长度
      offset = array.length;//插入的偏移值,就是array的长度

  while (++index < length) {//循环插入元素
    array[offset + index] = values[index];
  }
  return array;
}

/**
 * A specialized version of `_.reduce` for arrays without support for
 * iteratee shorthands.
 *
 * @private
 * @param {Array} [array] The array to iterate over.
 * @param {Function} iteratee The function invoked per iteration.
 * @param {*} [accumulator] The initial value.
 * @param {boolean} [initAccum] Specify using the first element of `array` as
 *  the initial value.
 * @returns {*} Returns the accumulated value.
 */
//对于数组类型实现的reduce方法
function arrayReduce(array, iteratee, accumulator, initAccum) {
  var index = -1,//循环索引
      length = array ? array.length : 0;//数组长度

  if (initAccum && length) {//第一次的累加值是数组第一个元素
    accumulator = array[++index];
  }
  while (++index < length) {//循环调用iteratee
    accumulator = iteratee(accumulator, array[index], index, array);
  }
  return accumulator;//返回累加值
}

/**
 * A specialized version of `_.some` for arrays without support for iteratee
 * shorthands.
 *
 * @private
 * @param {Array} [array] The array to iterate over.
 * @param {Function} predicate The function invoked per iteration.
 * @returns {boolean} Returns `true` if any element passes the predicate check,
 *  else `false`.
 */
//类似于数组的some方法
function arraySome(array, predicate) {
  var index = -1,//循环索引
      length = array ? array.length : 0;//数组长度

  while (++index < length) {//循环调用传入的回调函数,参数是值,值的索引,数组本身
    if (predicate(array[index], index, array)) {
      return true;
    }
  }//如果有一个返回true,就返回true,否则,返回false
  return false;
}

/**
 * The base implementation of `_.property` without support for deep paths.
 *
 * @private
 * @param {string} key The key of the property to get.
 * @returns {Function} Returns the new accessor function.
 */
//返回一个方法,这个方法返回object的对应key的对应value
function baseProperty(key) {
  return function(object) {
    return object == null ? undefined : object[key];
  };
}

/**
 * The base implementation of `_.times` without support for iteratee shorthands
 * or max array length checks.
 *
 * @private
 * @param {number} n The number of times to invoke `iteratee`.
 * @param {Function} iteratee The function invoked per iteration.
 * @returns {Array} Returns the array of results.
 */
//_.times的基础实现,循环n次,返回iteratee处理的index组成的数组
function baseTimes(n, iteratee) {
  var index = -1,//循环索引
      result = Array(n);//结果数组,长度为n

  while (++index < n) {//循环n次,结果数组每一个元素是当前index被iteratee处理后的结果
    result[index] = iteratee(index);
  }
  return result;
}

/**
 * The base implementation of `_.unary` without support for storing metadata.
 *
 * @private
 * @param {Function} func The function to cap arguments for.
 * @returns {Function} Returns the new capped function.
 */
//_.unary的基础实现,创建一个函数只接受一个参数,忽略之后所有参数
function baseUnary(func) {
  return function(value) {
    return func(value);
  };
}

/**
 * Gets the value at `key` of `object`.
 *
 * @private
 * @param {Object} [object] The object to query.
 * @param {string} key The key of the property to get.
 * @returns {*} Returns the property value.
 */
function getValue(object, key) {
  return object == null ? undefined : object[key];
}

/**
 * Checks if `value` is a host object in IE < 9.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
 */
//判断value是否是一个宿主对象,在IE9以下的浏览器中
function isHostObject(value) {
  // Many host objects are `Object` objects that can coerce to strings
  // despite having improperly defined `toString` methods.
  //很多宿主对象可以强制转成字符串尽管有一个不正确的toString方法
  var result = false;
  if (value != null && typeof value.toString != 'function') {
    try {
      result = !!(value + '');
    } catch (e) {}
  }
  return result;
}

/**
 * Converts `map` to its key-value pairs.
 *
 * @private
 * @param {Object} map The map to convert.
 * @returns {Array} Returns the key-value pairs.
 */
//转换map对象变成键值对数组
function mapToArray(map) {
  var index = -1,//循环索引
      result = Array(map.size);//结果数组,长度和map长度一样

  map.forEach(function(value, key) {//循环赋值到结果数组,每一个元素是键值组成的两个元素的数组
    result[++index] = [key, value];
  });
  return result;
}

/**
 * Creates a unary function that invokes `func` with its argument transformed.
 *
 * @private
 * @param {Function} func The function to wrap.
 * @param {Function} transform The argument transform.
 * @returns {Function} Returns the new function.
 */
//创建一个只有一个参数的函数,引用func来处理,这个函数的参数会被transform处理
function overArg(func, transform) {
  return function(arg) {
    return func(transform(arg));
  };
}

/**
 * Converts `set` to an array of its values.
 *
 * @private
 * @param {Object} set The set to convert.
 * @returns {Array} Returns the values.
 */
//转换set对象为数组
function setToArray(set) {
  var index = -1,//循环索引
      result = Array(set.size);//结果数组,长度和set长度一样

  set.forEach(function(value) {
    result[++index] = value;//循环赋值
  });
  return result;
}

/** Used for built-in method references. */
var arrayProto = Array.prototype,
    funcProto = Function.prototype,
    objectProto = Object.prototype;

/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];

/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
  var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
  return uid ? ('Symbol(src)_1.' + uid) : '';
}());

/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;

/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;

/**
 * Used to resolve the
 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
 * of values.
 */
var objectToString = objectProto.toString;

/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
  funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
  .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);

/** Built-in value references. */
var Buffer = moduleExports ? root.Buffer : undefined,
    Symbol = root.Symbol,
    Uint8Array = root.Uint8Array,
    getPrototype = overArg(Object.getPrototypeOf, Object),
    objectCreate = Object.create,
    propertyIsEnumerable = objectProto.propertyIsEnumerable,
    splice = arrayProto.splice;

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetSymbols = Object.getOwnPropertySymbols,
    nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
    nativeKeys = overArg(Object.keys, Object);

/* Built-in method references that are verified to be native. */
var DataView = getNative(root, 'DataView'),
    Map = getNative(root, 'Map'),
    Promise = getNative(root, 'Promise'),
    Set = getNative(root, 'Set'),
    WeakMap = getNative(root, 'WeakMap'),
    nativeCreate = getNative(Object, 'create');

/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = toSource(DataView),
    mapCtorString = toSource(Map),
    promiseCtorString = toSource(Promise),
    setCtorString = toSource(Set),
    weakMapCtorString = toSource(WeakMap);

/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol ? Symbol.prototype : undefined,
    symbolValueOf = symbolProto ? symbolProto.valueOf : undefined,
    symbolToString = symbolProto ? symbolProto.toString : undefined;

/**
 * Creates a hash object.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function Hash(entries) {
  var index = -1,
      length = entries ? entries.length : 0;

  this.clear();
  while (++index < length) {
    var entry = entries[index];
    this.set(entry[0], entry[1]);
  }
}

/**
 * Removes all key-value entries from the hash.
 *
 * @private
 * @name clear
 * @memberOf Hash
 */
function hashClear() {
  this.__data__ = nativeCreate ? nativeCreate(null) : {};
}

/**
 * Removes `key` and its value from the hash.
 *
 * @private
 * @name delete
 * @memberOf Hash
 * @param {Object} hash The hash to modify.
 * @param {string} key The key of the value to remove.
 * @returns {boolean} Returns `true` if the entry was removed, else `false`.
 */
function hashDelete(key) {
  return this.has(key) && delete this.__data__[key];
}

/**
 * Gets the hash value for `key`.
 *
 * @private
 * @name get
 * @memberOf Hash
 * @param {string} key The key of the value to get.
 * @returns {*} Returns the entry value.
 */
function hashGet(key) {
  var data = this.__data__;
  if (nativeCreate) {
    var result = data[key];
    return result === HASH_UNDEFINED ? undefined : result;
  }
  return hasOwnProperty.call(data, key) ? data[key] : undefined;
}

/**
 * Checks if a hash value for `key` exists.
 *
 * @private
 * @name has
 * @memberOf Hash
 * @param {string} key The key of the entry to check.
 * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
 */
function hashHas(key) {
  var data = this.__data__;
  return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
}

/**
 * Sets the hash `key` to `value`.
 *
 * @private
 * @name set
 * @memberOf Hash
 * @param {string} key The key of the value to set.
 * @param {*} value The value to set.
 * @returns {Object} Returns the hash instance.
 */
function hashSet(key, value) {
  var data = this.__data__;
  data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
  return this;
}

// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;

/**
 * Creates an list cache object.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function ListCache(entries) {
  var index = -1,
      length = entries ? entries.length : 0;

  this.clear();
  while (++index < length) {
    var entry = entries[index];
    this.set(entry[0], entry[1]);
  }
}

/**
 * Removes all key-value entries from the list cache.
 *
 * @private
 * @name clear
 * @memberOf ListCache
 */
function listCacheClear() {
  this.__data__ = [];
}

/**
 * Removes `key` and its value from the list cache.
 *
 * @private
 * @name delete
 * @memberOf ListCache
 * @param {string} key The key of the value to remove.
 * @returns {boolean} Returns `true` if the entry was removed, else `false`.
 */
function listCacheDelete(key) {
  var data = this.__data__,
      index = assocIndexOf(data, key);

  if (index < 0) {
    return false;
  }
  var lastIndex = data.length - 1;
  if (index == lastIndex) {
    data.pop();
  } else {
    splice.call(data, index, 1);
  }
  return true;
}

/**
 * Gets the list cache value for `key`.
 *
 * @private
 * @name get
 * @memberOf ListCache
 * @param {string} key The key of the value to get.
 * @returns {*} Returns the entry value.
 */
function listCacheGet(key) {
  var data = this.__data__,
      index = assocIndexOf(data, key);

  return index < 0 ? undefined : data[index][1];
}

/**
 * Checks if a list cache value for `key` exists.
 *
 * @private
 * @name has
 * @memberOf ListCache
 * @param {string} key The key of the entry to check.
 * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
 */
function listCacheHas(key) {
  return assocIndexOf(this.__data__, key) > -1;
}

/**
 * Sets the list cache `key` to `value`.
 *
 * @private
 * @name set
 * @memberOf ListCache
 * @param {string} key The key of the value to set.
 * @param {*} value The value to set.
 * @returns {Object} Returns the list cache instance.
 */
function listCacheSet(key, value) {
  var data = this.__data__,
      index = assocIndexOf(data, key);

  if (index < 0) {
    data.push([key, value]);
  } else {
    data[index][1] = value;
  }
  return this;
}

// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;

/**
 * Creates a map cache object to store key-value pairs.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function MapCache(entries) {
  var index = -1,
      length = entries ? entries.length : 0;

  this.clear();
  while (++index < length) {
    var entry = entries[index];
    this.set(entry[0], entry[1]);
  }
}

/**
 * Removes all key-value entries from the map.
 *
 * @private
 * @name clear
 * @memberOf MapCache
 */
function mapCacheClear() {
  this.__data__ = {
    'hash': new Hash,
    'map': new (Map || ListCache),
    'string': new Hash
  };
}

/**
 * Removes `key` and its value from the map.
 *
 * @private
 * @name delete
 * @memberOf MapCache
 * @param {string} key The key of the value to remove.
 * @returns {boolean} Returns `true` if the entry was removed, else `false`.
 */
function mapCacheDelete(key) {
  return getMapData(this, key)['delete'](key);
}

/**
 * Gets the map value for `key`.
 *
 * @private
 * @name get
 * @memberOf MapCache
 * @param {string} key The key of the value to get.
 * @returns {*} Returns the entry value.
 */
function mapCacheGet(key) {
  return getMapData(this, key).get(key);
}

/**
 * Checks if a map value for `key` exists.
 *
 * @private
 * @name has
 * @memberOf MapCache
 * @param {string} key The key of the entry to check.
 * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
 */
function mapCacheHas(key) {
  return getMapData(this, key).has(key);
}

/**
 * Sets the map `key` to `value`.
 *
 * @private
 * @name set
 * @memberOf MapCache
 * @param {string} key The key of the value to set.
 * @param {*} value The value to set.
 * @returns {Object} Returns the map cache instance.
 */
function mapCacheSet(key, value) {
  getMapData(this, key).set(key, value);
  return this;
}

// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;

/**
 *
 * Creates an array cache object to store unique values.
 *
 * @private
 * @constructor
 * @param {Array} [values] The values to cache.
 */
function SetCache(values) {
  var index = -1,
      length = values ? values.length : 0;

  this.__data__ = new MapCache;
  while (++index < length) {
    this.add(values[index]);
  }
}

/**
 * Adds `value` to the array cache.
 *
 * @private
 * @name add
 * @memberOf SetCache
 * @alias push
 * @param {*} value The value to cache.
 * @returns {Object} Returns the cache instance.
 */
function setCacheAdd(value) {
  this.__data__.set(value, HASH_UNDEFINED);
  return this;
}

/**
 * Checks if `value` is in the array cache.
 *
 * @private
 * @name has
 * @memberOf SetCache
 * @param {*} value The value to search for.
 * @returns {number} Returns `true` if `value` is found, else `false`.
 */
function setCacheHas(value) {
  return this.__data__.has(value);
}

// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;

/**
 * Creates a stack cache object to store key-value pairs.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function Stack(entries) {
  this.__data__ = new ListCache(entries);
}

/**
 * Removes all key-value entries from the stack.
 *
 * @private
 * @name clear
 * @memberOf Stack
 */
function stackClear() {
  this.__data__ = new ListCache;
}

/**
 * Removes `key` and its value from the stack.
 *
 * @private
 * @name delete
 * @memberOf Stack
 * @param {string} key The key of the value to remove.
 * @returns {boolean} Returns `true` if the entry was removed, else `false`.
 */
function stackDelete(key) {
  return this.__data__['delete'](key);
}

/**
 * Gets the stack value for `key`.
 *
 * @private
 * @name get
 * @memberOf Stack
 * @param {string} key The key of the value to get.
 * @returns {*} Returns the entry value.
 */
function stackGet(key) {
  return this.__data__.get(key);
}

/**
 * Checks if a stack value for `key` exists.
 *
 * @private
 * @name has
 * @memberOf Stack
 * @param {string} key The key of the entry to check.
 * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
 */
function stackHas(key) {
  return this.__data__.has(key);
}

/**
 * Sets the stack `key` to `value`.
 *
 * @private
 * @name set
 * @memberOf Stack
 * @param {string} key The key of the value to set.
 * @param {*} value The value to set.
 * @returns {Object} Returns the stack cache instance.
 */
function stackSet(key, value) {
  var cache = this.__data__;
  if (cache instanceof ListCache) {
    var pairs = cache.__data__;
    if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
      pairs.push([key, value]);
      return this;
    }
    cache = this.__data__ = new MapCache(pairs);
  }
  cache.set(key, value);
  return this;
}

// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;

/**
 * Creates an array of the enumerable property names of the array-like `value`.
 *
 * @private
 * @param {*} value The value to query.
 * @param {boolean} inherited Specify returning inherited property names.
 * @returns {Array} Returns the array of property names.
 */
//创建一个array-like对象的可枚举的属性名组成的数组
//inherited布尔值,是否指定需要返回继承来的属性
function arrayLikeKeys(value, inherited) {
  // Safari 8.1 makes `arguments.callee` enumerable in strict mode.
  // Safari 9 makes `arguments.length` enumerable in strict mode.
  var result = (isArray(value) || isArguments(value))
    ? baseTimes(value.length, String)
    : [];
    //结果数组初始化,如果value是数组或者arguments对象,使用baseTimes处理,baseTimes返回的数组的元素是索引的字符串值组成的数组,否则为空数组

  var length = result.length,//结果数组长度
      skipIndexes = !!length;//是否需要跳过数字索引

  for (var key in value) {//for in循环value的可枚举属性
    if ((inherited || hasOwnProperty.call(value, key)) &&
        !(skipIndexes && (key == 'length' || isIndex(key, length)))) {
      //如果需要返回继承来的属性值,或者当前key是value自身属性
      //并且判断是否是不是length属性或者数字索引属性,因为这两种属性需要跳过
      result.push(key);
    }
  }
  return result;
}

/**
 * Assigns `value` to `key` of `object` if the existing value is not equivalent
 * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
 * for equality comparisons.
 *
 * @private
 * @param {Object} object The object to modify.
 * @param {string} key The key of the property to assign.
 * @param {*} value The value to assign.
 */
//给object上指定key赋值value,使用SameValueZero规则比较值是否相等
function assignValue(object, key, value) {
  var objValue = object[key];//赋值前object上key属性的值
  if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
      (value === undefined && !(key in object))) {
    //如果object上有key键,但是对应值和新值不相等,或者object上没有key键,直接赋值
    object[key] = value;
  }
}

/**
 * Gets the index at which the `key` is found in `array` of key-value pairs.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @param {*} key The key to search for.
 * @returns {number} Returns the index of the matched value, else `-1`.
 */
function assocIndexOf(array, key) {
  var length = array.length;
  while (length--) {
    if (eq(array[length][0], key)) {
      return length;
    }
  }
  return -1;
}

/**
 * The base implementation of `_.assign` without support for multiple sources
 * or `customizer` functions.
 *
 * @private
 * @param {Object} object The destination object.
 * @param {Object} source The source object.
 * @returns {Object} Returns `object`.
 */
//_.assign的基础实现,将source的属性复制到object上,不支持多个source参数或者自定义function
function baseAssign(object, source) {
  return object && copyObject(source, keys(source), object);
  //调用copyObject
}

/**
 * The base implementation of `_.clone` and `_.cloneDeep` which tracks
 * traversed objects.
 *
 * @private
 * @param {*} value The value to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @param {boolean} [isFull] Specify a clone including symbols.
 * @param {Function} [customizer] The function to customize cloning.
 * @param {string} [key] The key of `value`.
 * @param {Object} [object] The parent object of `value`.
 * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
 * @returns {*} Returns the cloned value.
 */
//_.clone和_.cloneDeep的基础实现,追踪遍历对象
function baseClone(value, isDeep, isFull, customizer, key, object, stack) {
  var result;//克隆结果
  if (customizer) {//如果传递了自定义克隆方法,就调用来克隆value
    result = object ? customizer(value, key, object, stack) : customizer(value);
  }
  if (result !== undefined) {//如果自定义克隆方法处理后结果不是undefined,就返回克隆结果
    return result;
  }
  if (!isObject(value)) {//如果value不是array,object,function,regexp对象或者字符串或数字对象,直接返回value
    return value;
  }
  var isArr = isArray(value);//value是否是数组的标记
  if (isArr) {//如果value是数组
    result = initCloneArray(value);//调用initCloneArray初始化数组克隆
    if (!isDeep) {//如果不需要深度克隆,直接返回copyArray操作后的结果
      return copyArray(value, result);
    }
  } else {//如果value不是数组
    var tag = getTag(value),//value的toStringTag
        isFunc = tag == funcTag || tag == genTag;//value是否是函数的标记

    if (isBuffer(value)) {//如果是buffer对象,就调用cloneBuffer来克隆
      return cloneBuffer(value, isDeep);
    }
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      //value的toStringTag是[object Object]或者[object Arguments],或者value是函数并且没有父对象
      if (isHostObject(value)) {//如果value是宿主对象,如果有父级对象,就返回原value否则返回空对象
        return object ? value : {};
      }
      result = initCloneObject(isFunc ? {} : value);
      //调用initCloneObject初始化克隆对象,如果value是函数,赋值为空对象,否则不变
      if (!isDeep) {
        //如果不是深度克隆,先用baseAssign将value的属性复制到result上,然后再调用copySymbols复制symbols属性
        //复制完普通属性和symbol属性后返回结果
        return copySymbols(value, baseAssign(result, value));
      }
    } else {//如果toStringTag不是[object Object]也不是[object Arguments],也不是没有父对象的函数
      if (!cloneableTags[tag]) {//如果当前value的类型不支持克隆,返回value或者空对象
        return object ? value : {};
      }
      //否则调用initCloneByTag处理
      result = initCloneByTag(value, tag, baseClone, isDeep);
    }
  }
  // Check for circular references and return its corresponding clone.
  //检查循环引用并且返回它对应的克隆
  stack || (stack = new Stack);//新实例化一个stack对象,用于存储key-value键值对
  var stacked = stack.get(value);//如果stack中已经存了value先获取到
  if (stacked) {//如果有直接返回
    return stacked;
  }
  stack.set(value, result);//如果没有存,就存下当前的value和对应的result

  if (!isArr) {//如果value不是数组
    //isFull标识指定是否需要克隆symbol属性
    //如果需要克隆symbol,调用getAllKeys获取value所有属性名组成的数组,包括symbol
    //如果不需要克隆symbol,调用keys获取所有普通属性名组成的数组,不包括symbol
    //props是value的所有可枚举属性组成的数组
    var props = isFull ? getAllKeys(value) : keys(value);
  }
  arrayEach(props || value, function(subValue, key) {
    //forEach循环对象的键数组或者数组本身
    if (props) {//如果是键数组
      key = subValue;//key键
      subValue = value[key];//value值
    }
    // Recursively populate clone (susceptible to call stack limits).
    //调用assignValue给result上指定key赋值value,递归调用vaseClone继续深层克隆
    assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack));
  });
  return result;
}

/**
 * The base implementation of `_.create` without support for assigning
 * properties to the created object.
 *
 * @private
 * @param {Object} prototype The object to inherit from.
 * @returns {Object} Returns the new object.
 */
//_.create的基础实现,不支持分配属性到创建好的对象上
function baseCreate(proto) {
  return isObject(proto) ? objectCreate(proto) : {};
  //如果proto是对象,就调用objectCreate创建新的继承自proto的对象,否则返回空对象
}

/**
 * The base implementation of `_.get` without support for default values.
 *
 * @private
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @returns {*} Returns the resolved value.
 */
//_.get方法的基础实现,获取object上path路径对应的值
function baseGet(object, path) {
  path = isKey(path, object) ? [path] : castPath(path);
  //判断path是不是合法key,如果是,就变成[path],否则用castPath处理

  var index = 0,//循环路径数组的索引
      length = path.length;//路径数组的长度

  while (object != null && index < length) {//如果object不为空,循环路径数组
    object = object[toKey(path[index++])];//根据当前路径获取到深一层的对象或值赋值给object
  }
  return (index && index == length) ? object : undefined;
  //循环结束后返回找到的值,找不到返回undefined
}

/**
 * The base implementation of `getAllKeys` and `getAllKeysIn` which uses
 * `keysFunc` and `symbolsFunc` to get the enumerable property names and
 * symbols of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @param {Function} keysFunc The function to get the keys of `object`.
 * @param {Function} symbolsFunc The function to get the symbols of `object`.
 * @returns {Array} Returns the array of property names and symbols.
 */
//获取对象的可枚举属性名和symbol属性名组成数组返回
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
  var result = keysFunc(object);//用keysFunc获取objcet的键
  return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
  //判断object是否是数组,如果是就返回result
  //如果不是,说明是对象,可能会有symbol属性,使用symbolsFunc获取symbol属性然后插入结果数组
}

/**
 * The base implementation of `getTag`.
 *
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
//getTap基础实现,获取值的toStringTag
function baseGetTag(value) {
  return objectToString.call(value);
}

/**
 * The base implementation of `_.hasIn` without support for deep paths.
 *
 * @private
 * @param {Object} [object] The object to query.
 * @param {Array|string} key The key to check.
 * @returns {boolean} Returns `true` if `key` exists, else `false`.
 */
//_.hasIn的基础实现,不支持深层属性路径判断
function baseHasIn(object, key) {
  return object != null && key in Object(object);
}

/**
 * The base implementation of `_.isEqual` which supports partial comparisons
 * and tracks traversed objects.
 *
 * @private
 * @param {*} value The value to compare.
 * @param {*} other The other value to compare.
 * @param {Function} [customizer] The function to customize comparisons.
 * @param {boolean} [bitmask] The bitmask of comparison flags.
 *  The bitmask may be composed of the following flags:
 *     1 - Unordered comparison
 *     2 - Partial comparison
 * @param {Object} [stack] Tracks traversed `value` and `other` objects.
 * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
 */
//_.isEqual的基础实现,深度比较两个对象是否是相等的
//value要比较的对象,other要比较的另一个对象,customizer自定义比较方法
//bitmask为1说明是无序的比较,为2说明是部分比较
//stack追踪传入的参数
function baseIsEqual(value, other, customizer, bitmask, stack) {
  if (value === other) {//如果value和other严格相等,直接返回true
    return true;
  }
  if (value == null || other == null || (!isObject(value) && !isObjectLike(other))) {
    //判断有值为空或者NaN,反正不是数组或对象的情况的情况
    return value !== value && other !== other;
  }
  return baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack);
  //调用baseIsEqualDeep来深度循环比较value和other是否相等
}

/**
 * A specialized version of `baseIsEqual` for arrays and objects which performs
 * deep comparisons and tracks traversed objects enabling objects with circular
 * references to be compared.
 *
 * @private
 * @param {Object} object The object to compare.
 * @param {Object} other The other object to compare.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Function} [customizer] The function to customize comparisons.
 * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual`
 *  for more details.
 * @param {Object} [stack] Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
//深度比较数组和对象是否相等
function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) {
  var objIsArr = isArray(object),//判断object是不是数组
      othIsArr = isArray(other),//判断other是不是数组
      objTag = arrayTag,//'[object Array]'
      othTag = arrayTag;//'[object Array]'

  if (!objIsArr) {
    objTag = getTag(object);//如果object不是数组,就获取object的toStringTag
    objTag = objTag == argsTag ? objectTag : objTag;
  }
  if (!othIsArr) {
    othTag = getTag(other);//如果other不是数组,就获取other的toStringTag
    othTag = othTag == argsTag ? objectTag : othTag;
  }
  var objIsObj = objTag == objectTag && !isHostObject(object),//object是对象类型且不是宿主对象
      othIsObj = othTag == objectTag && !isHostObject(other),//other是对象类型且不是宿主对象
      isSameTag = objTag == othTag;//object和other的toStringTag是否一样

  if (isSameTag && !objIsObj) {//object和other的tag相等且它们不是本地对象类型
    stack || (stack = new Stack);//新建一个ListCache类型数据,ListCache其实就是一个自己实现的Map数据类型
    return (objIsArr || isTypedArray(object))
      ? equalArrays(object, other, equalFunc, customizer, bitmask, stack)
      : equalByTag(object, other, objTag, equalFunc, customizer, bitmask, stack);
      //如果都是数组类型或者typedArray类型就调用equalArrays方法判断
      //否则调用equalByTag判断
  }
  if (!(bitmask & PARTIAL_COMPARE_FLAG)) {//如果不是部分比较,且是object类型
    var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
        othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
        //判断object和other上有没有__wrapped__属性,就是判断它们是不是lodash包裹对象
        //单文件的lodash代码中有LodashWrapper方法会创建`lodash` wrapper objects

    if (objIsWrapped || othIsWrapped) {//如果object和other其中有一个是lodash包裹对象
      var objUnwrapped = objIsWrapped ? object.value() : object,
          othUnwrapped = othIsWrapped ? other.value() : other;
          //获取lodash包裹对象的值,然后再传递给baseIsEqual处理

      stack || (stack = new Stack);
      return equalFunc(objUnwrapped, othUnwrapped, customizer, bitmask, stack);
    }
  }
  if (!isSameTag) {//如果toStringTag不一样,直接返回false
    return false;
  }
  stack || (stack = new Stack);
  return equalObjects(object, other, equalFunc, customizer, bitmask, stack);
  //其他情况使用equalObjects来判断
}

/**
 * The base implementation of `_.isMatch` without support for iteratee shorthands.
 *
 * @private
 * @param {Object} object The object to inspect.
 * @param {Object} source The object of property values to match.
 * @param {Array} matchData The property names, values, and compare flags to match.
 * @param {Function} [customizer] The function to customize comparisons.
 * @returns {boolean} Returns `true` if `object` is a match, else `false`.
 */
//object,给定对象
//source,用来比较的源对象
//matchData,source参数的[key, value, boolean]的数组的形式,第三个布尔值表明当前值是否适合用===比较
//自定义比较函数
function baseIsMatch(object, source, matchData, customizer) {
  var index = matchData.length,//循环索引,从结尾开始循环
      length = index,//source长度
      noCustomizer = !customizer;//有没有传递自定义比较方法的标识

  if (object == null) {//如果object为空,若source有属性,返回false,若source无属性,返回true
    return !length;
  }
  object = Object(object);//强制转换object
  while (index--) {//按source属性长度循环
    var data = matchData[index];//当前matchData
    if ((noCustomizer && data[2])
          ? data[1] !== object[data[0]]
          : !(data[0] in object)
        ) {//如果没有自定义比较方法且当前source属性值适合===判断,也就是说是简单类型数据,就直接用!==判断是否不相等
          //否则用in来判断object是否不含有source key属性
      return false;
    }
  }
  //上面循环先循环一遍判断适合使用===直接判断的属性,下面循环再循环一遍判断其他需要深度循环判断的属性
  //上面循环结束后index循环索引变成0,现在再次正向循环
  while (++index < length) {
    data = matchData[index];//当前matchData
    var key = data[0],//当前source key
        objValue = object[key],//当前object 对应source key 的值
        srcValue = data[1];//当前source value

    if (noCustomizer && data[2]) {//如果没有自定义比较方法,且是简单类型,就判断objValue上是否不存在此属性
      if (objValue === undefined && !(key in object)) {
        return false;//如果不存在直接返回false
      }
    } else {
      var stack = new Stack;
      if (customizer) {//如果有自定义比较方法,就用自定义的比较
        var result = customizer(objValue, srcValue, key, object, source, stack);
      }
      if (!(result === undefined
            ? baseIsEqual(srcValue, objValue, customizer, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG, stack)
            : result
          )) {//如果没有自定义比较方法,就用baseIsEqual来比较
        return false;
      }
    }
  }
  return true;
}

/**
 * The base implementation of `_.isNative` without bad shim checks.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a native function,
 *  else `false`.
 */
function baseIsNative(value) {
  if (!isObject(value) || isMasked(value)) {
    return false;
  }
  var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor;
  return pattern.test(toSource(value));
}

/**
 * The base implementation of `_.isTypedArray` without Node.js optimizations.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
 */
function baseIsTypedArray(value) {
  return isObjectLike(value) &&
    isLength(value.length) && !!typedArrayTags[objectToString.call(value)];
}

/**
 * The base implementation of `_.iteratee`.
 *
 * @private
 * @param {*} [value=_.identity] The value to convert to an iteratee.
 * @returns {Function} Returns the iteratee.
 */
//_.iteratee的基础实现
function baseIteratee(value) {
  // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
  // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
  if (typeof value == 'function') {//如果value是function直接返回无需处理
    return value;
  }
  if (value == null) {//如果value是空,返回identity,identity方法返回第一个接收到的参数
    return identity;
  }
  if (typeof value == 'object') {//如果typeof值是object,则value有可能是数组或对象
    return isArray(value)
      ? baseMatchesProperty(value[0], value[1])
      : baseMatches(value);
      //如果value是数组,调用baseMatchesProperty处理
      //如果value是对象,否则调用baseMatches处理
  }
  return property(value);//否则调用property处理
}

/**
 * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 */
//_.keys的基础实现,类似于Object.keys()
function baseKeys(object) {
  if (!isPrototype(object)) {//如果object不是一个prototype对象,用nativekeys处理,也就是原生的Object.keys
    return nativeKeys(object);
  }
  var result = [];//结果数组
  for (var key in Object(object)) {//遍历object的属性
    if (hasOwnProperty.call(object, key) && key != 'constructor') {
      //如果当前key是object自身属性,并且不是构造函数,就插入结果数组
      result.push(key);
    }
  }
  return result;
}

/**
 * The base implementation of `_.matches` which doesn't clone `source`.
 *
 * @private
 * @param {Object} source The object of property values to match.
 * @returns {Function} Returns the new spec function.
 */
//_.matches方法的基础实现
//创建一个方法,这个方法会使用部分的(partial)深度比较来比较给定的对象和source对象,如果给定对象拥有相等的属性值,就返回true,否则false
function baseMatches(source) {
  var matchData = getMatchData(source);
  //getMatchData,把对象变成[key, value, boolean]的数组的形式,第三个布尔值表明当前值是否适合用===比较
  if (matchData.length == 1 && matchData[0][2]) {//如果source对象只有一个属性
    return matchesStrictComparable(matchData[0][0], matchData[0][1]);
    //matchesStrictComparable返回一个方法,这个方法用于比较传入对象object key值对应的值是否和给定值srcValue相等
    //matchData[0][0]就是key,matchData[0][1]就是srcValue
  }
  return function(object) {//如果source有多个属性,那么就调用baseIsMatch
    return object === source || baseIsMatch(object, source, matchData);
  };
}

/**
 * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.
 *
 * @private
 * @param {string} path The path of the property to get.
 * @param {*} srcValue The value to match.
 * @returns {Function} Returns the new spec function.
 */
//_.matchesProperty的基础实现,不克隆srcValue
/**
 * 
 * // The `_.matchesProperty` iteratee shorthand.
 * _.filter(users, _.iteratee(['user', 'fred']));
 * // => [{ 'user': 'fred', 'age': 40 }]
 * 处理传入的iteratee是数组的情况,数组有两个元素,第一个元素是对象的键key或者深层键的路径path,第二个元素是键对应的值value
 * path参数:对象的键key或者深层键的路径path
 * srcValue参数:键对应的值value
 */
function baseMatchesProperty(path, srcValue) {
  if (isKey(path) && isStrictComparable(srcValue)) {
    //第一种情况,如果path是合法键,并且srcValue适合用严格等于判断,调用matchesStrictComparable处理
    return matchesStrictComparable(toKey(path), srcValue);
  }
  return function(object) {
    //第二种情况,path参数可能是属性路径,或者srcValue是对象类型需要使用深度循环比较
    var objValue = get(object, path);//获取到object上对应path路径的值存入objValue
    return (objValue === undefined && objValue === srcValue)
      ? hasIn(object, path)
      : baseIsEqual(srcValue, objValue, undefined, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
      //如果objValue是undefined并且和提供的数组的第二个元素srcValue相等,调用hasIn判断path是否是object自身的或者继承的属性
      //否则使用baseIsEqual深层循环判断两个对象是否相等
  };
}

/**
 * A specialized version of `baseProperty` which supports deep paths.
 *
 * @private
 * @param {Array|string} path The path of the property to get.
 * @returns {Function} Returns the new accessor function.
 */
//返回一个方法,这个方法根据path返回object的对应属性值
function basePropertyDeep(path) {
  return function(object) {
    return baseGet(object, path);
  };
}

/**
 * The base implementation of `_.toString` which doesn't convert nullish
 * values to empty strings.
 *
 * @private
 * @param {*} value The value to process.
 * @returns {string} Returns the string.
 */
//toString方法的基础实现
function baseToString(value) {
  // Exit early for strings to avoid a performance hit in some environments.
  if (typeof value == 'string') {//如果value已经是字符串,直接返回
    return value;
  }
  if (isSymbol(value)) {//如果value是symbol对象,就调用symbol原型上的toString方法转换成字符串
    return symbolToString ? symbolToString.call(value) : '';
  }
  var result = (value + '');//转换成字符串
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;//处理-0情况后返回结果字符串
}

/**
 * Casts `value` to a path array if it's not one.
 *
 * @private
 * @param {*} value The value to inspect.
 * @returns {Array} Returns the cast property path array.
 */
//将值计算成一个路径数组
function castPath(value) {
  return isArray(value) ? value : stringToPath(value);
}

/**
 * Creates a clone of  `buffer`.
 *
 * @private
 * @param {Buffer} buffer The buffer to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Buffer} Returns the cloned buffer.
 */
//创建一个buffer对象的克隆
function cloneBuffer(buffer, isDeep) {
  if (isDeep) {
    return buffer.slice();
  }
  var result = new buffer.constructor(buffer.length);
  buffer.copy(result);
  return result;
}

/**
 * Creates a clone of `arrayBuffer`.
 *
 * @private
 * @param {ArrayBuffer} arrayBuffer The array buffer to clone.
 * @returns {ArrayBuffer} Returns the cloned array buffer.
 */
//创建一个arrayBuffer对象的克隆
function cloneArrayBuffer(arrayBuffer) {
  var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
  new Uint8Array(result).set(new Uint8Array(arrayBuffer));
  return result;
}

/**
 * Creates a clone of `dataView`.
 *
 * @private
 * @param {Object} dataView The data view to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the cloned data view.
 */
//创建DataView对象的克隆
function cloneDataView(dataView, isDeep) {
  var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
  return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}

/**
 * Creates a clone of `map`.
 *
 * @private
 * @param {Object} map The map to clone.
 * @param {Function} cloneFunc The function to clone values.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the cloned map.
 */
//创建map对象的克隆
function cloneMap(map, isDeep, cloneFunc) {
  var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map);
  return arrayReduce(array, addMapEntry, new map.constructor);
  //调用arrayReduce数组累加遍历方法,累加初始值是一个新map对象
}

/**
 * Creates a clone of `regexp`.
 *
 * @private
 * @param {Object} regexp The regexp to clone.
 * @returns {Object} Returns the cloned regexp.
 */
//创建正则对象的克隆
function cloneRegExp(regexp) {
  var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
  result.lastIndex = regexp.lastIndex;
  return result;
}

/**
 * Creates a clone of `set`.
 *
 * @private
 * @param {Object} set The set to clone.
 * @param {Function} cloneFunc The function to clone values.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the cloned set.
 */
//创建set对象的克隆
function cloneSet(set, isDeep, cloneFunc) {
  var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set);
  return arrayReduce(array, addSetEntry, new set.constructor);
}

/**
 * Creates a clone of the `symbol` object.
 *
 * @private
 * @param {Object} symbol The symbol object to clone.
 * @returns {Object} Returns the cloned symbol object.
 */
//创建symbol对象的克隆
function cloneSymbol(symbol) {
  return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}

/**
 * Creates a clone of `typedArray`.
 *
 * @private
 * @param {Object} typedArray The typed array to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the cloned typed array.
 */
//创建typedArray对象的克隆
function cloneTypedArray(typedArray, isDeep) {
  var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
  return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}

/**
 * Copies the values of `source` to `array`.
 *
 * @private
 * @param {Array} source The array to copy values from.
 * @param {Array} [array=[]] The array to copy values to.
 * @returns {Array} Returns `array`.
 */
//复制source数组的元素到array数组
function copyArray(source, array) {
  var index = -1,//循环索引,
      length = source.length;//source数组长度

  array || (array = Array(length));//如果没有array参数就新建和source长度一样的作为array
  while (++index < length) {//循环source复制值到array
    array[index] = source[index];
  }
  return array;
}

/**
 * Copies properties of `source` to `object`.
 *
 * @private
 * @param {Object} source The object to copy properties from.
 * @param {Array} props The property identifiers to copy.
 * @param {Object} [object={}] The object to copy properties to.
 * @param {Function} [customizer] The function to customize copied values.
 * @returns {Object} Returns `object`.
 */
//复制source上的属性到object上
function copyObject(source, props, object, customizer) {
  object || (object = {});//目标对象如果是undefined就默认为空对象

  var index = -1,//循环索引
      length = props.length;//source对象的键组成的数组的长度

  while (++index < length) {//循环键数组
    var key = props[index];//当前键

    var newValue = customizer
      ? customizer(object[key], source[key], key, object, source)
      : undefined;
      //如果传递了customizer,就调用生成新的当前属性值,否则新属性值是undefined

    assignValue(object, key, newValue === undefined ? source[key] : newValue);
    //调用assignValue将新属性值赋到object的对应key上
    //如果新值是undefined,就用source上对应key的值
  }
  return object;
}

/**
 * Copies own symbol properties of `source` to `object`.
 *
 * @private
 * @param {Object} source The object to copy symbols from.
 * @param {Object} [object={}] The object to copy symbols to.
 * @returns {Object} Returns `object`.
 */
//将source上的symbol属性复制到object上
function copySymbols(source, object) {
  return copyObject(source, getSymbols(source), object);
}

/**
 * A specialized version of `baseIsEqualDeep` for arrays with support for
 * partial deep comparisons.
 *
 * @private
 * @param {Array} array The array to compare.
 * @param {Array} other The other array to compare.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Function} customizer The function to customize comparisons.
 * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
 *  for more details.
 * @param {Object} stack Tracks traversed `array` and `other` objects.
 * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
 */
//深度比较数组是否相等
//partical部分比较的意思就是,other的长度可以比array长,other在包含array所有元素的情况下,还可以有自己的独特于array的元素
function equalArrays(array, other, equalFunc, customizer, bitmask, stack) {
  //bitmask内部使用的时候值只能是1或2,1代表无序比较,2代表部分比较
  //bitmask按位与后,只有按位与操作符两边数字相等的情况下才会返回整数,否则返回0
  var isPartial = bitmask & PARTIAL_COMPARE_FLAG,//是否部分比较
      arrLength = array.length,//array长度
      othLength = other.length;//other长度

  if (arrLength != othLength && !(isPartial && othLength > arrLength)) {//长度不符合要求,直接返回false
    return false;
  }
  // Assume cyclic values are equal.
  //假设循环值都是相等的
  //stack默认是listCache对象,是自己实现的Map数据类型
  var stacked = stack.get(array);
  if (stacked && stack.get(other)) {//如果是递归调用到了equalArrays,stack上就能够获取到值
    return stacked == other;
  }
  var index = -1,//循环索引
      result = true,//一开始先假设循环值都是相等的
      seen = (bitmask & UNORDERED_COMPARE_FLAG) ? new SetCache : undefined;
      //如果是无序比较,就实例化一个SetCache对象

  stack.set(array, other);//stack上存下array和other
  stack.set(other, array);

  // Ignore non-index properties.
  while (++index < arrLength) {//循环array的长度
    var arrValue = array[index],//当前循环的array值
        othValue = other[index];//当前循环的other值

    if (customizer) {//如果提供了customizer比较方法,就用它来比较,partial部分比较要反过来传递array和other
      var compared = isPartial
        ? customizer(othValue, arrValue, index, other, array, stack)
        : customizer(arrValue, othValue, index, array, other, stack);
    }
    if (compared !== undefined) {//如果自定义比较有了结果,且为真,就continue继续下一次循环,否则result=false,跳出循环
      if (compared) {
        continue;
      }
      result = false;
      break;
    }
    // Recursively compare arrays (susceptible to call stack limits).
    //递归地比较数组元素值是否相等(容易溢出调用栈)
    if (seen) {//如果创建了seen变量,说明是无序比较
      //seen是SetCache对象,也是利用key-value形式存储值
      //遍历other,如果有值和当前array的值相等,相等的otherValue的index就存入seen中,并且arraySome返回true,否则返回false跳出循环
      if (!arraySome(other, function(othValue, othIndex) {
            if (!seen.has(othIndex) &&
                (arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) {
              return seen.add(othIndex);
            }
          })) {
        result = false;
        break;
      }
    } else if (!(
          arrValue === othValue ||
            equalFunc(arrValue, othValue, customizer, bitmask, stack)
        )) {//如果不是无序比较,就先用严格等于比较,如果严格等于不合适,就递归调用baseIsEqual继续判断获取结果
      result = false;
      break;
    }
  }
  stack['delete'](array);//清除stack
  stack['delete'](other);
  return result;
}

/**
 * A specialized version of `baseIsEqualDeep` for comparing objects of
 * the same `toStringTag`.
 *
 * **Note:** This function only supports comparing values with tags of
 * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
 *
 * @private
 * @param {Object} object The object to compare.
 * @param {Object} other The other object to compare.
 * @param {string} tag The `toStringTag` of the objects to compare.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Function} customizer The function to customize comparisons.
 * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
 *  for more details.
 * @param {Object} stack Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
//比较toStringTag相等的两个对象是否相等
//object用来比较的对象
//other用来比较的另一个对象
//tag比较对象的toStringTag
//equalFunc基础比较相等方法,用来递归调用
//customizer自定义比较方法
//bitmask是否无序比较或者部分比较的标志
//stack,key-value结构跟踪比较的对象
function equalByTag(object, other, tag, equalFunc, customizer, bitmask, stack) {
  switch (tag) {//根据toStringTag来比较object和other对象
    case dataViewTag://'[object DataView]'
      if ((object.byteLength != other.byteLength) ||
          (object.byteOffset != other.byteOffset)) {
        return false;
      }
      object = object.buffer;
      other = other.buffer;

    case arrayBufferTag://'[object ArrayBuffer]'
      if ((object.byteLength != other.byteLength) ||
          !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
        return false;
      }
      return true;

    case boolTag: //'[object Boolean]'
    case dateTag://'[object Date]'
    case numberTag://'[object Number]'
      // Coerce booleans to `1` or `0` and dates to milliseconds.
      // Invalid dates are coerced to `NaN`.
      //强制转换布尔值到1或0,时间对象转换为毫秒,无效的时间转换为NaN
      return eq(+object, +other);

    case errorTag://'[object Error]'
      return object.name == other.name && object.message == other.message;

    case regexpTag://'[object RegExp]'
    case stringTag://'[object String]'
      // Coerce regexes to strings and treat strings, primitives and objects,
      // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
      // for more details.
      return object == (other + '');

    case mapTag://'[object Map]'
      var convert = mapToArray;
      //将map转换成数组的方法
      /*
    {key1: value1, key2: value2, ...}
    变成:
    [[key1, value1], [key2, value2], ...]
    */

    case setTag://'[object Set]'
      var isPartial = bitmask & PARTIAL_COMPARE_FLAG;//是否部分比较
      convert || (convert = setToArray);//将map或者set转换成数组的方法,mapToArray或者setToArray

      if (object.size != other.size && !isPartial) {//如果不是部分比较,且长度不一样,直接返回false
        return false;
      }
      // Assume cyclic values are equal.
      //假设循环值都相等,从中判断不相等的情况
      var stacked = stack.get(object);
      if (stacked) {
        return stacked == other;
      }
      bitmask |= UNORDERED_COMPARE_FLAG;

      // Recursively compare objects (susceptible to call stack limits).
      stack.set(object, other);
      var result = equalArrays(convert(object), convert(other), equalFunc, customizer, bitmask, stack);
      //把object和other都转换成数组,然后用equalArrays方法来比较是否相等
      stack['delete'](object);
      return result;

    case symbolTag://'[object Symbol]'
      if (symbolValueOf) {//Symbol.prototype.valueOf
        return symbolValueOf.call(object) == symbolValueOf.call(other);
      }
  }
  return false;
}

/**
 * A specialized version of `baseIsEqualDeep` for objects with support for
 * partial deep comparisons.
 *
 * @private
 * @param {Object} object The object to compare.
 * @param {Object} other The other object to compare.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Function} customizer The function to customize comparisons.
 * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
 *  for more details.
 * @param {Object} stack Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
//判断object类型深度比较是否相等,支持部分比较
function equalObjects(object, other, equalFunc, customizer, bitmask, stack) {
  var isPartial = bitmask & PARTIAL_COMPARE_FLAG,//是否部分比较
      objProps = keys(object),//Object.keys(),返回key组成的数组
      objLength = objProps.length,
      othProps = keys(other),//Object.keys(),返回key组成的数组
      othLength = othProps.length;

  if (objLength != othLength && !isPartial) {//如果不是部分比较,且object和other的key长度不一样,返回false
    return false;
  }
  var index = objLength;//循环索引是object key数组的长度
  while (index--) {
    var key = objProps[index];//object 的key
    if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
      //如果是部分比较,就用in来判断,否则用Object.prototype.hasOwnProperty
      //判断object的key是否other里也有
      //如果没有,返回false
      return false;
    }
  }
  // Assume cyclic values are equal.
  var stacked = stack.get(object);
  if (stacked && stack.get(other)) {
    return stacked == other;
  }
  var result = true;//开始循环之前假设循环的值都是相等的
  stack.set(object, other);
  stack.set(other, object);

  var skipCtor = isPartial;//部分比较,跳过constructor
  while (++index < objLength) {
    key = objProps[index];//object的key
    var objValue = object[key],//object key对应的value
        othValue = other[key];//other key对应的value

    if (customizer) {//如果有自定义比较方法,就用自定义的比较
      var compared = isPartial
        ? customizer(othValue, objValue, key, other, object, stack)
        : customizer(objValue, othValue, key, object, other, stack);
    }
    // Recursively compare objects (susceptible to call stack limits).
    if (!(compared === undefined
          ? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack))
          : compared
        )) {
          //如果自定义比较失败就跳出循环,result=false
          //如果不是自定义比较,就用===比较,或者继续递归调用baseIsEqual来深度比较
      result = false;
      break;
    }
    skipCtor || (skipCtor = key == 'constructor');
  }
  if (result && !skipCtor) {//不是部分比较且object中有constructor属性,不跳过constructor属性,判断constructor是否一样
    var objCtor = object.constructor,
        othCtor = other.constructor;

    // Non `Object` object instances with different constructors are not equal.
    if (objCtor != othCtor &&
        ('constructor' in object && 'constructor' in other) &&
        !(typeof objCtor == 'function' && objCtor instanceof objCtor &&
          typeof othCtor == 'function' && othCtor instanceof othCtor)) {
      result = false;
    }
  }
  stack['delete'](object);
  stack['delete'](other);
  return result;
}

/**
 * Creates an array of own enumerable property names and symbols of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names and symbols.
 */
//创建指定对象的所有可枚举属性(包括symbol属性)组成的数组
function getAllKeys(object) {
  return baseGetAllKeys(object, keys, getSymbols);
}

/**
 * Gets the data for `map`.
 *
 * @private
 * @param {Object} map The map to query.
 * @param {string} key The reference key.
 * @returns {*} Returns the map data.
 */
function getMapData(map, key) {
  var data = map.__data__;
  return isKeyable(key)
    ? data[typeof key == 'string' ? 'string' : 'hash']
    : data.map;
}

/**
 * Gets the property names, values, and compare flags of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the match data of `object`.
 */
//获取对象中的属性名,属性值,和用来匹配的标记
function getMatchData(object) {
  var result = keys(object),//object的key组成的数组
      length = result.length;//key数组的长度

  while (length--) {//循环key数组
    var key = result[length],//当前key
        value = object[key];//当前值

    result[length] = [key, value, isStrictComparable(value)];
    //重写result的当前值,变成[key, value, boolean]的形式,第三个布尔值表明当前值是否适合用===比较
  }
  return result;
}

/**
 * Gets the native function at `key` of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @param {string} key The key of the method to get.
 * @returns {*} Returns the function if it's native, else `undefined`.
 */
function getNative(object, key) {
  var value = getValue(object, key);
  return baseIsNative(value) ? value : undefined;
}

/**
 * Creates an array of the own enumerable symbol properties of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of symbols.
 */
//创建一个对象的自身symbol属性组成的数组
//如果支持原生的Object.getOwnPropertySymbols就直接用,否则使用stubArray,返回一个空数组
var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray;

/**
 * Gets the `toStringTag` of `value`.
 *
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
var getTag = baseGetTag;

// Fallback for data views, maps, sets, and weak maps in IE 11,
// for data views in Edge < 14, and promises in Node.js.
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
    (Map && getTag(new Map) != mapTag) ||
    (Promise && getTag(Promise.resolve()) != promiseTag) ||
    (Set && getTag(new Set) != setTag) ||
    (WeakMap && getTag(new WeakMap) != weakMapTag)) {
  getTag = function(value) {
    var result = objectToString.call(value),
        Ctor = result == objectTag ? value.constructor : undefined,
        ctorString = Ctor ? toSource(Ctor) : undefined;

    if (ctorString) {
      switch (ctorString) {
        case dataViewCtorString: return dataViewTag;
        case mapCtorString: return mapTag;
        case promiseCtorString: return promiseTag;
        case setCtorString: return setTag;
        case weakMapCtorString: return weakMapTag;
      }
    }
    return result;
  };
}

/**
 * Checks if `path` exists on `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @param {Array|string} path The path to check.
 * @param {Function} hasFunc The function to check properties.
 * @returns {boolean} Returns `true` if `path` exists, else `false`.
 */
//判断path路径是否在object中存在
function hasPath(object, path, hasFunc) {
  path = isKey(path, object) ? [path] : castPath(path);
  //将path变成路径数组

  var result,//结果,true或false
      index = -1,//循环路径数组的索引
      length = path.length;//路径数组的长度

  while (++index < length) {//循环路径数组
    var key = toKey(path[index]);//当前key
    if (!(result = object != null && hasFunc(object, key))) {
      //如果当前key在object中不存在,跳出循环,并且result赋值为false
      break;
    }
    object = object[key];//下一层object赋值
  }
  if (result) {//如果为真,返回true
    return result;
  }
  var length = object ? object.length : 0;//object的length
  return !!length && isLength(length) && isIndex(key, length) &&
    (isArray(object) || isArguments(object));
  //判断object是数组的情况
  //有length属性,length属性是有效数字,判断最后一个key值是否是一个数组有效索引,object是数组或者是一个arguments对象
}

/**
 * Initializes an array clone.
 *
 * @private
 * @param {Array} array The array to clone.
 * @returns {Array} Returns the initialized clone.
 */
//初始化数组的克隆
function initCloneArray(array) {
  var length = array.length,//数组长度
      result = array.constructor(length);//结果数组,和array长度相同

  // Add properties assigned by `RegExp#exec`.
  if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    //为结果数组上赋值index属性和input属性
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}

/**
 * Initializes an object clone.
 *
 * @private
 * @param {Object} object The object to clone.
 * @returns {Object} Returns the initialized clone.
 */
//初始化对象克隆
function initCloneObject(object) {
  return (typeof object.constructor == 'function' && !isPrototype(object))
    ? baseCreate(getPrototype(object))
    : {};
    //如果object有构造函数且object不是作为prototype存在,就调用baseCreate处理,先获取object的原型
    //否则返回空对象
}

/**
 * Initializes an object clone based on its `toStringTag`.
 *
 * **Note:** This function only supports cloning values with tags of
 * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
 *
 * @private
 * @param {Object} object The object to clone.
 * @param {string} tag The `toStringTag` of the object to clone.
 * @param {Function} cloneFunc The function to clone values.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the initialized clone.
 */
//基于对象的toStringTag初始化对象克隆
//这个方法只支持克隆`Boolean`, `Date`, `Error`, `Number`, `RegExp`, `String`这几种类型
function initCloneByTag(object, tag, cloneFunc, isDeep) {
  var Ctor = object.constructor;//要克隆的对象的构造函数
  switch (tag) {
    case arrayBufferTag://[object ArrayBuffer]
      return cloneArrayBuffer(object);

    case boolTag://[object Boolean]
    case dateTag://[object Date]
      return new Ctor(+object);//将布尔值和时间对象转换成数字然后调用对应构造函数新建克隆返回

    case dataViewTag://[object DataView]
      return cloneDataView(object, isDeep);

    case float32Tag: case float64Tag:
    case int8Tag: case int16Tag: case int32Tag:
    case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
    //[object Float32Array] [object Float64Array] [object Int8Array] [object Int16Array] [object Int32Array] [object Uint8Array] [object Uint8ClampedArray] [object Uint16Array] [object Uint32Array]
      return cloneTypedArray(object, isDeep);

    case mapTag://[object Map]
      return cloneMap(object, isDeep, cloneFunc);

    case numberTag://[object Number]
    case stringTag://[object String]
      return new Ctor(object);//调用数字和字符串的对应构造函数新建克隆

    case regexpTag://[object RegExp]
      return cloneRegExp(object);

    case setTag://[object Set]
      return cloneSet(object, isDeep, cloneFunc);

    case symbolTag://[object Symbol]
      return cloneSymbol(object);
  }
}

/**
 * Checks if `value` is a valid array-like index.
 *
 * @private
 * @param {*} value The value to check.
 * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
 * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
 */
//判断一个值是否是一个有效的array-like对象的索引
function isIndex(value, length) {
  length = length == null ? MAX_SAFE_INTEGER : length;
  return !!length &&
    (typeof value == 'number' || reIsUint.test(value)) &&
    (value > -1 && value % 1 == 0 && value < length);
}

/**
 * Checks if `value` is a property name and not a property path.
 *
 * @private
 * @param {*} value The value to check.
 * @param {Object} [object] The object to query keys on.
 * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
 */
//检查一个值是一个合法属性名而不是属性路径
//value需要判断的值,object需要查询键的对象
function isKey(value, object) {
  if (isArray(value)) {//如果value是数组,返回false
    return false;
  }
  var type = typeof value;//value的typeof值
  if (type == 'number' || type == 'symbol' || type == 'boolean' ||
      value == null || isSymbol(value)) {
        //如果value是number、sumbol、boolean、null,说明是合法key,返回true
    return true;
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object));
    //reIsPlainProp = /^\w*$/   匹配单词,说明是属性名
    //reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/ 如果含有点.或者含有数组说明不是属性名
    //如果value在object里能找到说明是属性名
}

/**
 * Checks if `value` is suitable for use as unique object key.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is suitable, else `false`.
 */
function isKeyable(value) {
  var type = typeof value;
  return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
    ? (value !== '__proto__')
    : (value === null);
}

/**
 * Checks if `func` has its source masked.
 *
 * @private
 * @param {Function} func The function to check.
 * @returns {boolean} Returns `true` if `func` is masked, else `false`.
 */
function isMasked(func) {
  return !!maskSrcKey && (maskSrcKey in func);
}

/**
 * Checks if `value` is likely a prototype object.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
 */
//判断一个值是否类似一个prototype对象
function isPrototype(value) {
  var Ctor = value && value.constructor,
      proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;

  return value === proto;
}

/**
 * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` if suitable for strict
 *  equality comparisons, else `false`.
 */
//判断一个值是否适合使用严格等于===来比较,NaN和对象引用类型不适合严格比较
function isStrictComparable(value) {
  return value === value && !isObject(value);
}

/**
 * A specialized version of `matchesProperty` for source values suitable
 * for strict equality comparisons, i.e. `===`.
 *
 * @private
 * @param {string} key The key of the property to get.
 * @param {*} srcValue The value to match.
 * @returns {Function} Returns the new spec function.
 */
//iteratee传递的是一个两个元素的数组,并且第一个元素是合法属性键,第二个元素是适合===判断的值
//返回一个方法,这个方法用于比较传入对象object key值对应的值是否和给定值srcValue相等
function matchesStrictComparable(key, srcValue) {
  return function(object) {//object是iteratee接收到的对象
    if (object == null) {//如果object为空,返回false
      return false;
    }
    return object[key] === srcValue &&
      (srcValue !== undefined || (key in Object(object)));
      //如果object上key对应的值和srcValue相等,就返回true
  };
}

/**
 * Converts `string` to a property path array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the property path array.
 */
//将路径字符串转换成路径数组
var stringToPath = memoize(function(string) {
  string = toString(string);//将string转换成字符串格式

  var result = [];//结果数组
  if (reLeadingDot.test(string)) {
    //reLeadingDot = /^\./
    //如果字符串的开头如果是一个点,结果数组第一个路径值就push一个空字符串
    result.push('');
  }
  //rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g
  string.replace(rePropName, function(match, number, quote, string) {
    result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
    //如果捕获到了quote,处理转义字符后赋值给key然后push进结果
    //否则插入捕获到的number
  });
  return result;
});

/**
 * Converts `value` to a string key if it's not a string or symbol.
 *
 * @private
 * @param {*} value The value to inspect.
 * @returns {string|symbol} Returns the key.
 */
//将一个值转换成字符串键,如果它不是字符串或者symbol对象
function toKey(value) {
  if (typeof value == 'string' || isSymbol(value)) {//如果value已经是字符串或者symbol,直接返回
    return value;
  }
  var result = (value + '');//转成字符串
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;//处理-0的情况
}

/**
 * Converts `func` to its source code.
 *
 * @private
 * @param {Function} func The function to process.
 * @returns {string} Returns the source code.
 */
function toSource(func) {
  if (func != null) {
    try {
      return funcToString.call(func);
    } catch (e) {}
    try {
      return (func + '');
    } catch (e) {}
  }
  return '';
}

/**
 * Creates a function that memoizes the result of `func`. If `resolver` is
 * provided, it determines the cache key for storing the result based on the
 * arguments provided to the memoized function. By default, the first argument
 * provided to the memoized function is used as the map cache key. The `func`
 * is invoked with the `this` binding of the memoized function.
 *
 * **Note:** The cache is exposed as the `cache` property on the memoized
 * function. Its creation may be customized by replacing the `_.memoize.Cache`
 * constructor with one whose instances implement the
 * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
 * method interface of `delete`, `get`, `has`, and `set`.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to have its output memoized.
 * @param {Function} [resolver] The function to resolve the cache key.
 * @returns {Function} Returns the new memoized function.
 * @example
 *
 * var object = { 'a': 1, 'b': 2 };
 * var other = { 'c': 3, 'd': 4 };
 *
 * var values = _.memoize(_.values);
 * values(object);
 * // => [1, 2]
 *
 * values(other);
 * // => [3, 4]
 *
 * object.a = 2;
 * values(object);
 * // => [1, 2]
 *
 * // Modify the result cache.
 * values.cache.set(object, ['a', 'b']);
 * values(object);
 * // => ['a', 'b']
 *
 * // Replace `_.memoize.Cache`.
 * _.memoize.Cache = WeakMap;
 */
//创建一个函数可以缓存func方法的结果。如果提供了resolver参数,它被用来计算缓存对象上的key,基于传递给已经memoize化的函数的参数。
//将计算结果缓存在闭包中的私有变量中,如果再次计算同样的值就直接获取
function memoize(func, resolver) {
  if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
    //func和resolver(如果提供的话)如果不是函数,抛错误
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  var memoized = function() {
    var args = arguments,
        key = resolver ? resolver.apply(this, args) : args[0],
        //如果提供了resolver,就用resolver来确定key,否则key就是memoized的第一个参数
        cache = memoized.cache;//cashe对象

    if (cache.has(key)) {//如果cache中已经有此key,就直接获取后返回
      return cache.get(key);
    }
    var result = func.apply(this, args);//否则调用func计算出key对应的value
    memoized.cache = cache.set(key, result);//将新value存入cache后返回
    return result;
  };
  memoized.cache = new (memoize.Cache || MapCache);
  //cache对象被定义到memoized方法上,cache使用自定义的MapCache类型
  return memoized;//返回被memoize化的方法
}

// Assign cache to `_.memoize`.
memoize.Cache = MapCache;

/**
 * Performs a
 * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
 * comparison between two values to determine if they are equivalent.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to compare.
 * @param {*} other The other value to compare.
 * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
 * @example
 *
 * var object = { 'a': 1 };
 * var other = { 'a': 1 };
 *
 * _.eq(object, object);
 * // => true
 *
 * _.eq(object, other);
 * // => false
 *
 * _.eq('a', 'a');
 * // => true
 *
 * _.eq('a', Object('a'));
 * // => false
 *
 * _.eq(NaN, NaN);
 * // => true
 */
//用SameValueZero规则判断两个值是否相等
function eq(value, other) {
  return value === other || (value !== value && other !== other);
}

/**
 * Checks if `value` is likely an `arguments` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an `arguments` object,
 *  else `false`.
 * @example
 *
 * _.isArguments(function() { return arguments; }());
 * // => true
 *
 * _.isArguments([1, 2, 3]);
 * // => false
 */
//判断一个值是否是一个arguments对象
function isArguments(value) {
  // Safari 8.1 makes `arguments.callee` enumerable in strict mode.
  return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') &&
    (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag);
}

/**
 * Checks if `value` is classified as an `Array` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an array, else `false`.
 * @example
 *
 * _.isArray([1, 2, 3]);
 * // => true
 *
 * _.isArray(document.body.children);
 * // => false
 *
 * _.isArray('abc');
 * // => false
 *
 * _.isArray(_.noop);
 * // => false
 */
//判断一个值是否是数组
var isArray = Array.isArray;

/**
 * Checks if `value` is array-like. A value is considered array-like if it's
 * not a function and has a `value.length` that's an integer greater than or
 * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
 * @example
 *
 * _.isArrayLike([1, 2, 3]);
 * // => true
 *
 * _.isArrayLike(document.body.children);
 * // => true
 *
 * _.isArrayLike('abc');
 * // => true
 *
 * _.isArrayLike(_.noop);
 * // => false
 */
//判断一个值是否是array-like对象
function isArrayLike(value) {
  return value != null && isLength(value.length) && !isFunction(value);
}

/**
 * This method is like `_.isArrayLike` except that it also checks if `value`
 * is an object.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an array-like object,
 *  else `false`.
 * @example
 *
 * _.isArrayLikeObject([1, 2, 3]);
 * // => true
 *
 * _.isArrayLikeObject(document.body.children);
 * // => true
 *
 * _.isArrayLikeObject('abc');
 * // => false
 *
 * _.isArrayLikeObject(_.noop);
 * // => false
 */
//判断一个值是否是一个array-like对象并且也是一个object-like对象
function isArrayLikeObject(value) {
  return isObjectLike(value) && isArrayLike(value);
}

/**
 * Checks if `value` is a buffer.
 *
 * @static
 * @memberOf _
 * @since 4.3.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
 * @example
 *
 * _.isBuffer(new Buffer(2));
 * // => true
 *
 * _.isBuffer(new Uint8Array(2));
 * // => false
 */
//判断一个值是否是一个buffer对象
var isBuffer = nativeIsBuffer || stubFalse;

/**
 * Checks if `value` is classified as a `Function` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a function, else `false`.
 * @example
 *
 * _.isFunction(_);
 * // => true
 *
 * _.isFunction(/abc/);
 * // => false
 */
//判断一个值是否是function对象
function isFunction(value) {
  // The use of `Object#toString` avoids issues with the `typeof` operator
  // in Safari 8-9 which returns 'object' for typed array and other constructors.
  var tag = isObject(value) ? objectToString.call(value) : '';
  return tag == funcTag || tag == genTag;
}

/**
 * Checks if `value` is a valid array-like length.
 *
 * **Note:** This method is loosely based on
 * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
 * @example
 *
 * _.isLength(3);
 * // => true
 *
 * _.isLength(Number.MIN_VALUE);
 * // => false
 *
 * _.isLength(Infinity);
 * // => false
 *
 * _.isLength('3');
 * // => false
 */
//判断一个值是否是一个有效的array-like对象的length属性
function isLength(value) {
  return typeof value == 'number' &&
    value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}

/**
 * Checks if `value` is the
 * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
 * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an object, else `false`.
 * @example
 *
 * _.isObject({});
 * // => true
 *
 * _.isObject([1, 2, 3]);
 * // => true
 *
 * _.isObject(_.noop);
 * // => true
 *
 * _.isObject(null);
 * // => false
 */
//判断一个值是否是一个语言层面的对象
function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}

/**
 * Checks if `value` is object-like. A value is object-like if it's not `null`
 * and has a `typeof` result of "object".
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
 * @example
 *
 * _.isObjectLike({});
 * // => true
 *
 * _.isObjectLike([1, 2, 3]);
 * // => true
 *
 * _.isObjectLike(_.noop);
 * // => false
 *
 * _.isObjectLike(null);
 * // => false
 */
//判断一个值是否是一个object-like对象
function isObjectLike(value) {
  return !!value && typeof value == 'object';
}

/**
 * Checks if `value` is classified as a `Symbol` primitive or object.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
 * @example
 *
 * _.isSymbol(Symbol.iterator);
 * // => true
 *
 * _.isSymbol('abc');
 * // => false
 */
//判断一个值是否是一个symbol对象
function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && objectToString.call(value) == symbolTag);
}

/**
 * Checks if `value` is classified as a typed array.
 *
 * @static
 * @memberOf _
 * @since 3.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
 * @example
 *
 * _.isTypedArray(new Uint8Array);
 * // => true
 *
 * _.isTypedArray([]);
 * // => false
 */
//判断一个值是否是一个typedArray对象
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;

/**
 * Converts `value` to a string. An empty string is returned for `null`
 * and `undefined` values. The sign of `-0` is preserved.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to process.
 * @returns {string} Returns the string.
 * @example
 *
 * _.toString(null);
 * // => ''
 *
 * _.toString(-0);
 * // => '-0'
 *
 * _.toString([1, 2, 3]);
 * // => '1,2,3'
 */
//将值转换为字符串,如果value是null或者undefined,返回空字符串。-0的符号会被保留
function toString(value) {
  return value == null ? '' : baseToString(value);
}

/**
 * Gets the value at `path` of `object`. If the resolved value is
 * `undefined`, the `defaultValue` is returned in its place.
 *
 * @static
 * @memberOf _
 * @since 3.7.0
 * @category Object
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @param {*} [defaultValue] The value returned for `undefined` resolved values.
 * @returns {*} Returns the resolved value.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 3 } }] };
 *
 * _.get(object, 'a[0].b.c');
 * // => 3
 *
 * _.get(object, ['a', '0', 'b', 'c']);
 * // => 3
 *
 * _.get(object, 'a.b.c', 'default');
 * // => 'default'
 */
//获取object上path路径对应的值
function get(object, path, defaultValue) {
  var result = object == null ? undefined : baseGet(object, path);
  //如果object为空,则结果值是undefined,否则调用baseGet获取
  return result === undefined ? defaultValue : result;
}

/**
 * Checks if `path` is a direct or inherited property of `object`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Object
 * @param {Object} object The object to query.
 * @param {Array|string} path The path to check.
 * @returns {boolean} Returns `true` if `path` exists, else `false`.
 * @example
 *
 * var object = _.create({ 'a': _.create({ 'b': 2 }) });
 *
 * _.hasIn(object, 'a');
 * // => true
 *
 * _.hasIn(object, 'a.b');
 * // => true
 *
 * _.hasIn(object, ['a', 'b']);
 * // => true
 *
 * _.hasIn(object, 'b');
 * // => false
 */
//判断path路径代表的属性是否是object的自身直接属性或者继承的属性
function hasIn(object, path) {
  return object != null && hasPath(object, path, baseHasIn);
}

/**
 * Creates an array of the own enumerable property names of `object`.
 *
 * **Note:** Non-object values are coerced to objects. See the
 * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
 * for more details.
 *
 * @static
 * @since 0.1.0
 * @memberOf _
 * @category Object
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 * @example
 *
 * function Foo() {
 *   this.a = 1;
 *   this.b = 2;
 * }
 *
 * Foo.prototype.c = 3;
 *
 * _.keys(new Foo);
 * // => ['a', 'b'] (iteration order is not guaranteed)
 *
 * _.keys('hi');
 * // => ['0', '1']
 */
//创建一个对象的自身可枚举属性名组成的数组,类似原生的Object.keys()
function keys(object) {
  return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
  //如果object是array-like对象,调用arrayLikeKeys处理,否则使用baseKeys处理
}

/**
 * This method returns the first argument it receives.
 *
 * @static
 * @since 0.1.0
 * @memberOf _
 * @category Util
 * @param {*} value Any value.
 * @returns {*} Returns `value`.
 * @example
 *
 * var object = { 'a': 1 };
 *
 * console.log(_.identity(object) === object);
 * // => true
 */
//返回第一个接收到的参数
function identity(value) {
  return value;
}

/**
 * Creates a function that invokes `func` with the arguments of the created
 * function. If `func` is a property name, the created function returns the
 * property value for a given element. If `func` is an array or object, the
 * created function returns `true` for elements that contain the equivalent
 * source properties, otherwise it returns `false`.
 *
 * @static
 * @since 4.0.0
 * @memberOf _
 * @category Util
 * @param {*} [func=_.identity] The value to convert to a callback.
 * @returns {Function} Returns the callback.
 * @example
 *
 * var users = [
 *   { 'user': 'barney', 'age': 36, 'active': true },
 *   { 'user': 'fred',   'age': 40, 'active': false }
 * ];
 *
 * // The `_.matches` iteratee shorthand.
 * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
 * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
 *
 * // The `_.matchesProperty` iteratee shorthand.
 * _.filter(users, _.iteratee(['user', 'fred']));
 * // => [{ 'user': 'fred', 'age': 40 }]
 *
 * // The `_.property` iteratee shorthand.
 * _.map(users, _.iteratee('user'));
 * // => ['barney', 'fred']
 *
 * // Create custom iteratee shorthands.
 * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
 *   return !_.isRegExp(func) ? iteratee(func) : function(string) {
 *     return func.test(string);
 *   };
 * });
 *
 * _.filter(['abc', 'def'], /ef/);
 * // => ['def']
 */
//创建一个迭代器方法,这个方法和参数func有关,还和这个被创建的方法接收的参数有关。如果func是一个属性名,那么这个方法就返回指定元素的对应属性值;如果func是一个数组或对象,这个方法就根据元素是否包含与这个数组或对象相等的数组或对象来返回true或false
function iteratee(func) {
  return baseIteratee(typeof func == 'function' ? func : baseClone(func, true));
  //如果func是function就不做变化,否则调用baseClone处理func
}

/**
 * Creates a function that returns the value at `path` of a given object.
 *
 * @static
 * @memberOf _
 * @since 2.4.0
 * @category Util
 * @param {Array|string} path The path of the property to get.
 * @returns {Function} Returns the new accessor function.
 * @example
 *
 * var objects = [
 *   { 'a': { 'b': 2 } },
 *   { 'a': { 'b': 1 } }
 * ];
 *
 * _.map(objects, _.property('a.b'));
 * // => [2, 1]
 *
 * _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b');
 * // => [1, 2]
 */
//创建一个方法,这个方法可以根据给定path属性路径返回给定对象的属性值
function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}

/**
 * This method returns a new empty array.
 *
 * @static
 * @memberOf _
 * @since 4.13.0
 * @category Util
 * @returns {Array} Returns the new empty array.
 * @example
 *
 * var arrays = _.times(2, _.stubArray);
 *
 * console.log(arrays);
 * // => [[], []]
 *
 * console.log(arrays[0] === arrays[1]);
 * // => false
 */
//返回一个新的空数组
function stubArray() {
  return [];
}

/**
 * This method returns `false`.
 *
 * @static
 * @memberOf _
 * @since 4.13.0
 * @category Util
 * @returns {boolean} Returns `false`.
 * @example
 *
 * _.times(2, _.stubFalse);
 * // => [false, false]
 */
function stubFalse() {
  return false;
}

module.exports = iteratee;

 

posted @ 2018-11-03 17:26  hahazexia  阅读(8480)  评论(1编辑  收藏  举报