Underscore.js 源码学习笔记(上)

版本 Underscore.js 1.9.1

一共 1693 行。注释我就删了,太长了…

 

整体是一个 (function() {...}());  这样的东西,我们应该知道这是一个 IIFE(立即执行函数)。

 

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

获取当前运行环境根对象。

在浏览器中为 self(=window)  在服务端中是 global 在一些虚拟机中是 this

 

var previousUnderscore = root._;

如果环境中已经定义了同名变量,防止对其造成覆盖,先把这个变量缓存起来。

 

var ArrayProto = Array.prototype, ObjProto = Object.prototype;
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;

var push = ArrayProto.push,
  slice = ArrayProto.slice,
  toString = ObjProto.toString,
  hasOwnProperty = ObjProto.hasOwnProperty;

var nativeIsArray = Array.isArray,
  nativeKeys = Object.keys,
  nativeCreate = Object.create;

定义一些变量来存储 JS 定义的对象原型和方法,以便后续使用。

 

var Ctor = function(){};

根据注释,这个裸函数是用来代理原型交换的?英文不好……后面应该可以看到用处,不急。

 

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

_ 是一个构造函数,传入的对象如果已经是 _ 实例就直接返回

我们知道当我们通过 new foo() 创建对象时会创建一个新的对象,然后将它的原型链绑定为 foo.propotype ,然后把这个对象作为 foo 调用的this,如果 foo 没有返回值的话,就返回这个对象。

所以通过   this instanceof _  可以判断是否是构造调用(是否加 new)如果不是的话 就手动加一个 new 调用一次。

通过foo生成一个对象,他有一个属性 _wrapped 的值是传入的obj。

 

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

因为 node 环境中会有 exports 变量,由此判断是在浏览器还是服务端。服务端的话就导出 _ ,否则在根元素上添加 _ 变量。

 

_.VERSION = '1.9.1';

版本号

 

var optimizeCb = function(func, context, argCount) {
  if (context === void 0) return func;
  switch (argCount == null ? 3 : argCount) {
    case 1: return function(value) {
      return func.call(context, value);
    };
    // The 2-argument case is omitted because we’re not using it.
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }
  return function() {
    return func.apply(context, arguments);
  };
};

其实这个函数可以简化成

var optimizeCb = function(func, context) {
  return function() {
    return func.apply(context, arguments);
  };
};

 

所以说这就相当于实现了一个 bind 。 optimizeCb(func, context) = func.bind(context) 

那为什么要分那么多情况呢?因为 apply  比 call 慢,而且某些情况下,还会慢很多

至于 void 0 是什么,void + 表达式会返回 undefined 这个算常识吧。而不使用 undefined 因为在某些老的浏览器中 undefined 可以被赋值,出于兼容性考虑。

 

var builtinIteratee;

var cb = function(value, context, argCount) {
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  if (value == null) return _.identity; // _.identity 函数: value => value
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
  return _.property(value);
};

_.iteratee = builtinIteratee = function(value, context) {
  return cb(value, context, Infinity);
};

这个看到有点懵比……

首先 builtinIteratee 就是一个用来判断  iteratee 是否被用户改变的临时变量,没有其他用处。

 _.iteratee() 是一个函数 默认返回 cb

cb 作为操作集合的函数的回调函数使用

如果  _.iteratee 被修改就调用修改后的函数

如果  value == null 就返回  _.identity 一个传入什么就返回什么的函数

如果 value 是函数 就返回  optimizeCb(value, context, argCount) 也就是 value.bind(context) 

如果 value 是对象 且不是数组 就返回  _.matcher(value) 

以上都不符合就返回 _.property(value); 

 

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

_.matcher = _.matches = function(attrs) {
  attrs = _.extendOwn({}, attrs);
  return function(obj) {
    return _.isMatch(obj, attrs);
  };
};
// e.g.
var isZhangsan = _.matcher({ firstname: 'san', lastname: 'zhang' });

console.log(isZhangsan({ firstname: 'san', lastname: 'zhang', age: 55 })); // true

console.log(isZhangsan({ firstname: 'si', lastname: 'zhang' })); // false

好了 现在知道不是正序写的了 哭唧唧 先不看  _.extendOwn 是什么鬼东西了 反正看名字肯定是一个扩展对象的函数

首先看 isMatch , keys 相当于 Object.keys , isMatch 就是判断 attrs 中的 key 是否在 object 中都存在且对应的值都相等。

那么  _.matcher 就是设定 attrs 返回函数。返回的函数传入 obj 看其是否符合 attrs。

 

var shallowProperty = function(key) {
  return function(obj) {
    return obj == null ? void 0 : obj[key];
  };
};

var deepGet = function(obj, path) {
  var length = path.length;
  for (var i = 0; i < length; i++) {
    if (obj == null) return void 0;
    obj = obj[path[i]];
  }
  return length ? obj : void 0;
};

_.property = function(path) {
  if (!_.isArray(path)) {
    return shallowProperty(path);
  }
  return function(obj) {
    return deepGet(obj, path);
  };
};

如果传入的不是数组,就返回获取对象属性path的值的函数,如果传入一个数组,就返回获取对象属性[path]对应的值的函数。

 

var restArguments = function(func, startIndex) {
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  return function() {
    var length = Math.max(arguments.length - startIndex, 0),
        rest = Array(length),
        index = 0;
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    switch (startIndex) {
      case 0: return func.call(this, rest);
      case 1: return func.call(this, arguments[0], rest);
      case 2: return func.call(this, arguments[0], arguments[1], rest);
    }
    var args = Array(startIndex + 1);
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    args[startIndex] = rest;
    return func.apply(this, args);
  };
};

相当于 ES6 的剩余参数,从 startIndex 开始的所有参数当做一个数组传入。分情况使用 call 还是上面提到的效率问题。

使用举例:

function sum(arr) {
  return arr.reduce((previous, current) => {
    return previous + current;
  });
}

var restArgumentsWrapperSum = restArguments(sum);

console.log(restArgumentsWrapperSum(1, 2, 3));

 

// var nativeCreate = Object.create;

var baseCreate = function(prototype) {
  if (!_.isObject(prototype)) return {};
  if (nativeCreate) return nativeCreate(prototype);
  Ctor.prototype = prototype;
  var result = new Ctor;
  Ctor.prototype = null;
  return result;
};

相当于手动实现了一个  Object.create 利用了上面不知道什么用空函数 Ctor 。

 new Ctor 没有加括号,在构造调用的时候,如果不传入参数,可以不加括号。相当于  new Ctor() 。

 Object.create(foo) 就是创建一个对象 对象的 [[Prototype]] 为 foo.prototype,这里通过 new 实现。结束之后再将 Ctor 的 protottype 赋值为 null 。

 

var shallowProperty = function(key) {
  return function(obj) {
    return obj == null ? void 0 : obj[key];
  };
};
// e.g.
var getId = shallowProperty('id');

let obj = { id: 233, otherKey: 'who care' };

console.log(getId(obj)); // 233

传入一个 key 生成一个 获取对象属性 key 的值 的函数。

 

// var hasOwnProperty = ObjProto.hasOwnProperty;

var has = function(obj, path) {
  return obj != null && hasOwnProperty.call(obj, path);
}

就是使用了  Object.prototype.hasOwnProperty 判断对象 obj 是否存在属性 path

 

var deepGet = function(obj, path) {
  var length = path.length;
  for (var i = 0; i < length; i++) {
    if (obj == null) return void 0;
    obj = obj[path[i]];
  }
  return length ? obj : void 0;
};
// e.g.
var obj = {
  user: {
    name: {
      first: 'san',
      last: 'zhang',
    },
    id: 3
  }
}

console.log(deepGet(obj, ['user', 'name', 'last'])); // zhang

根据路径获取对象指定嵌套属性的值。

 

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = shallowProperty('length');
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

如果一个对象有属性  length ,属性值为数字且在 [0, 2^53-1] 之间,则判断这个对象为类数组。

类数组常见的有 arguments、HTML Collection,数组也是类数组。

 

到这里是 174 行,下面就是集合相关函数了,明天再看 =。=

 

_.each = _.forEach = function(obj, iteratee, context) {
  // 首先将 iteratee 的 this 绑定到 context (如果 context 存在的话
  iteratee = optimizeCb(iteratee, context);
  var i, length;
  if (isArrayLike(obj)) {
    // 如果 obj 是类数组 对 obj[0 ... obj.lenght-1] 执行 iteratee
    for (i = 0, length = obj.length; i < length; i++) {
      iteratee(obj[i], i, obj);
    }
  } else {
    // 否则获取 obj 属性的集合 然后进行操作
    var keys = _.keys(obj);
    for (i = 0, length = keys.length; i < length; i++) {
      iteratee(obj[keys[i]], keys[i], obj);
    }
  }
  return obj;
};

这个函数相当于实现了 ES 的 forEach,传入(类)数组就遍历对每一项执行传入的函数  iteratee(item, idx, arr) ,如果传入的是对象就对每一个键值对执行  iteratee(value, key, obj) 

举个使用例子

function foo() {
  console.log('类数组 --->');
  // arguments 是类数组
  _.each(arguments, function iteratee(item, idx, arr) {
    // 传入 arguments[i], i, arguments
    console.log(`item=${item}, idx=${idx}, arr=${JSON.stringify(arr)}, this=${this}`);
  }, '上下文');
  console.log('对象 --->');
  var obj = { k: 'v', kk: 'vv' };
  // _.keys(obj) => ['k', 'kk']
  _.each(obj, function iteratee(value, key, obj) {
    console.log(`value=${value}, key=${key}, obj=${JSON.stringify(obj)}, this=${this}`);
  }, '上下文');
}

foo('one', [2], { three: false });

// 类数组 --->
// item=one, idx=0, arr={"0":"one","1":[2],"2":{"three":false}}, this=上下文
// item=2, idx=1, arr={"0":"one","1":[2],"2":{"three":false}}, this=上下文
// item=[object Object], idx=2, arr={"0":"one","1":[2],"2":{"three":false}}, this=上下文
// 对象 --->
// value=v, key=k, obj={"k":"v","kk":"vv"}, this=上下文
// value=vv, key=kk, obj={"k":"v","kk":"vv"}, this=上下文

 

_.map = _.collect = function(obj, iteratee, context) {
  iteratee = cb(iteratee, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
    length = (keys || obj).length,
    results = Array(length);
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    results[index] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results;
};

这个和上面实现和功能差不多,无非两个循环放到一起写了,且对每一项执行传入函数后都有了返回值,并返回这些返回值组成的数组。

 

var createReduce = function(dir) {
  var reducer = function(obj, iteratee, memo, initial) {
    var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length,
      index = dir > 0 ? 0 : length - 1;
    if (!initial) {
      // 如果没有传入初始值就把第一项当做初始值 并从第二项开始执行函数 (反向 reduce 就是倒数第一项咯
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    for (; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      memo = iteratee(memo, obj[currentKey], currentKey, obj);
    }
    return memo;
  };

  return function(obj, iteratee, memo, context) {
    var initial = arguments.length >= 3; // 根据参数个数 判断有没有初始值
    return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
  };
};

_.reduce = _.foldl = _.inject = createReduce(1);

_.reduceRight = _.foldr = createReduce(-1);

reduce的实现, createReduce 通过传入 +1/-1 可以返回 正向/反向 reduce ,返回函数的函数称作高阶函数 createReduce 就是。

源码倒是不难理解。不过 reduce 的实现可以参考一下。手写代码什么的。。。

 

var cb = function(value, context, argCount) {
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  if (value == null) return _.identity; // _.identity 函数: value => value
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
  return _.property(value);
};

var createPredicateIndexFinder = function(dir) {
  return function(array, predicate, context) {
    predicate = cb(predicate, context);
    var length = getLength(array);
    var index = dir > 0 ? 0 : length - 1;
    for (; index >= 0 && index < length; index += dir) {
      if (predicate(array[index], index, array)) return index;
    }
    return -1;
  };
};

_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);


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

_.find = _.detect = function(obj, predicate, context) {
  var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey;
  var key = keyFinder(obj, predicate, context);
  if (key !== void 0 && key !== -1) return obj[key];
};

这一段可能比较复杂,可以顺便好好理解一下cb函数。

首先 _.find(obj, predicate, context)
    如果 obj 不是类数组, keyFinder 就是 _.findKey
        进入 _.findKey 先将 predicate 赋值为 cb(predicate, context)
            如果 predicate 是函数 就获得 predicate.bind(context)
            如果 predicate 是 null 就获得 _.identity
            如果 predicate 是 对象且不是数组 就获得 _.matcher(value) 这个上面看过 就是一个判断传入对象是否符合 value 的函数
            以上都不符合 获得 _.property(value) 就是获取一个对象属性为 value 的值的函数 (obj) => obj[value] 如果 value 是数组 就是获得嵌套属性
        遍历 obj 每一个属性 如果属性值符合 predicate 返回对用属性名
    如果 obj 是类数组, keyFinder 就是 _.findIndex
        进入 _.findIndex 先将 predicate 赋值为 cb(predicate, context)
            过程同上
        遍历 obj 每一项 如果该项符合 predicate 就返回下标
获取了 第一个 符合条件的属性名或下标,然后获取对应属性值。

// =========以下几个例子便于理解;=========

var obj = {
  a: '2333',
  b: 666,
  c: 10086,
  d: { propertyName: 'mdzz' }
};
var arr = ['2333', 666, 10086, { propertyName: 'mdzz' }];

/* 获取 number 类型的值 */
function predicate(item, index, arr) { return typeof item === 'number'; }

console.log( _.find(obj, predicate) ) // predicate 是函数, 获取 obj 中第一个属性值类型为数字的值 > 666
console.log( _.find(obj) ) // 没有传入 predicate 就是 _.identity 对于每一个 truly 值都符合 所以返回第一个值 > '2333'
console.log( _.find(obj, { propertyName: 'mdzz' }) ) // predicate 是对象,查找符合对象的值 > { propertyName: 'mdzz' }
console.log( _.find(obj, 'propertyName') ) // predicate 字符串,获取含有对应属性的值 > { propertyName: 'mdzz' }

console.log( _.find(arr, predicate) ) // 666
console.log( _.find(arr) ) // '2333'
console.log( _.find(arr, { propertyName: 'mdzz' }) ) // { propertyName: 'mdzz' }
console.log( _.find(arr, 'propertyName') ) // { propertyName: 'mdzz' }

 

_.filter = _.select = function(obj, predicate, context) {
  var results = [];
  predicate = cb(predicate, context);
  _.each(obj, function(value, index, list) {
    if (predicate(value, index, list)) results.push(value);
  });
  return results;
};

_.each = _.forEach = function(obj, iteratee, context) {
  iteratee = optimizeCb(iteratee, context);
  var i, length;
  if (isArrayLike(obj)) {
    for (i = 0, length = obj.length; i < length; i++) {
      iteratee(obj[i], i, obj);
    }
  } else {
    var keys = _.keys(obj);
    for (i = 0, length = keys.length; i < length; i++) {
      iteratee(obj[keys[i]], keys[i], obj);
    }
  }
  return obj;
};

看到这里已经能理解 underscore 的这个逻辑了,如果是对象就 _.keys(obj) 遍历key, 如果是 isLikeArray 就遍历下标。

_.each 就是对遍历的每一项执行传入函数。_.filter 就是执行传入函数后返回 true 项的放入返回对象。

 

_.reject = function(obj, predicate, context) {
  return _.filter(obj, _.negate(cb(predicate)), context);
};

_.negate = function(predicate) {
  return function() {
    return !predicate.apply(this, arguments);
  };
};

_.negate 将传入函数返回值取反,_reject 就是反向 _filter 所有不符合 predicate 的项放入返回数组返回。

 

_.every = _.all = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    if (!predicate(obj[currentKey], currentKey, obj)) return false;
  }
  return true;
};

如果数组的每一项都符合 predicate 就返回 true 否则返回 false

 

_.some = _.any = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    if (predicate(obj[currentKey], currentKey, obj)) return true;
  }
  return false;
};

数组中存在符合 predicate 的项就返回 true 否则返回 false

 

_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
  if (!isArrayLike(obj)) obj = _.values(obj);
  if (typeof fromIndex != 'number' || guard) fromIndex = 0;
  return _.indexOf(obj, item, fromIndex) >= 0;
};

var createIndexFinder = function(dir, predicateFind, sortedIndex) {
  return function(array, item, idx) {
    var i = 0, length = getLength(array);
    if (typeof idx == 'number') {
      // 如果 idx 是数字类型 代表开始查找的位置
      if (dir > 0) {
        i = idx >= 0 ? idx : Math.max(idx + length, i);
      } else {
        length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
      }
    } else if (sortedIndex && idx && length) {
      // 如果 idx 不是数字类型 则代表 isSorted
      idx = sortedIndex(array, item);
      return array[idx] === item ? idx : -1;
    }
    if (item !== item) { // 判断 item 是否为 NaN
      idx = predicateFind(slice.call(array, i, length), _.isNaN);
      return idx >= 0 ? idx + i : -1;
    }
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      if (array[idx] === item) return idx;
    }
    return -1;
  };
};

_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);


_.sortedIndex = function(array, obj, iteratee, context) {
  iteratee = cb(iteratee, context, 1);
  var value = iteratee(obj);
  var low = 0, high = getLength(array);
  while (low < high) {
    var mid = Math.floor((low + high) / 2);
    if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
  }
  return low;
};

又是比较长的的几个函数。

首先 sortedIndex 二分查找符合 iteratee 的下标。是对有序的数组进行查找。注意返回的是 iteratee(obj) <= iteratee(idx) 的最小下标 idx 查找不到的时候不返回 -1

indexOf 正向查找符合 iteratee 的下标 不存在返回 -1

lastIndexOf 反向向查找符合 iteratee 的下标 不存在返回 -1

对于 _.contains 判断 obj 中是否存在 item 从 fromIndex 开始查找

guard 存在的时候就一定是从 0 开始查找(不知道加这个参数是出于什么考虑

 

_.invoke = restArguments(function(obj, path, args) {
  var contextPath, func;
  if (_.isFunction(path)) {
    func = path;
  } else if (_.isArray(path)) {
    contextPath = path.slice(0, -1);
    path = path[path.length - 1];
  }
  return _.map(obj, function(context) {
    var method = func;
    if (!method) {
      if (contextPath && contextPath.length) {
        context = deepGet(context, contextPath);
      }
      if (context == null) return void 0;
      method = context[path];
    }
    return method == null ? method : method.apply(context, args);
  });
});


// e.g.
_.invoke({a: 1, b: '2'}, function (...args) {
  console.log(this, args);
}, '参数1', '参数2');
// [Number: 1] [ '参数1', '参数2' ]
// [String: '2'] [ '参数1', '参数2' ]

let obj = {
  one: {
    a: {
      b: function () {
        console.log(this, arguments);
      }
    }
  },
  two: {
    a: {
      b: function () {
        console.log('哈哈哈哈哈');
      }
    }
  }
};

_.invoke(obj, ['a', 'b']);
// { b: [Function: b] } {}
// 哈哈哈哈哈

_.invoke 首先通过 restArguments 将函数包裹起来 会将多余的参数作为一个数组 args 统一传入

判断 path 是否是一个函数 如果是数组的话 就获取数组的除最后一项的所有项作为 contextPath 然后获得最后一项为 path 重新赋值

如果 path 是函数的话 就分别以 obj 的每一项作为 this 调用 path 参数为 ...args

否则 获取到 obj 每一项中对应 path 的属性值 然后每一项作为 this 调用该属性值 参数为 ...args

 

320行 =。= 看不下去了 明天继续……

 

_.pluck = function(obj, key) {
  return _.map(obj, _.property(key));
};

// e.g.
let arr = [{id: 1}, {name: 'zs', id: 2}, {}];
console.log(_.pluck(arr, 'id')); // [ 1, 2, undefined ]

这个好理解,就是获取数组每一项指定 key 的值。

 

_.where = function(obj, attrs) {
  return _.filter(obj, _.matcher(attrs));
};
// e.g.
var attrs = { name: 'glory' };
var arr = [{name:'glory'}, {name:'saber'}, {name:'glory', id: 1}];

console.log(_.where(arr, attrs));

筛选出数组 obj (或者 _.keys(obj)) 中符合 attr 的项。

 

_.findWhere = function(obj, attrs) {
  return _.find(obj, _.matcher(attrs));
};
// e.g.
var attrs = { name: 'glory' };
var arr = [{name:'glory'}, {name:'saber'}, {name:'glory', id: 1}];
console.log(_.findWhere(arr, attrs)); // { name: 'glory' }

_.find 查找的是第一个 所以区别就是上面查找所有符合的项组成的数组,这个返回的是符合 attr 的第一项。没有就是 undefined

 

_.max = function(obj, iteratee, context) {
  var result = -Infinity, lastComputed = -Infinity,
      value, computed;
  if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
    obj = isArrayLike(obj) ? obj : _.values(obj);
    for (var i = 0, length = obj.length; i < length; i++) {
      value = obj[i];
      if (value != null && value > result) {
        result = value;
      }
    }
  } else {
    iteratee = cb(iteratee, context);
    _.each(obj, function(v, index, list) {
      computed = iteratee(v, index, list);
      if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
        result = v;
        lastComputed = computed;
      }
    });
  }
  return result;
};
// e.g.
var arr = [{x:3,y:4}, {x:4,y:4}, {x:4,y:5}];
var comp = function (obj) {
  return obj.x * 3 + obj.y * 4;
};
console.log(_.max(arr, comp)); // { x: 4, y: 5 }

如果传入比较函数(iteratee)就用该函数对每一项进行处理,返回处理值最大那一项。(注意返回的原元素而不是经过 iteratee 处理后的)

如果不传就直接比较。注意他判断了 value != null 所以 null 和 undefined 是不参与比较的。(null > -1 值为 true)

如果是传入比较函数的时候,初始值和计算值设为  -Infinity ,通过判断  computed === -Infinity && result === -Infinity 来确定是不是初始状态,但是感觉这样会有 bug 看了下讨论区确实有人提了 bug,还没有被解决。

但是鬼知道他判断 number 什么的是什么意思……(突然发现这个代码都七个月没更新了……

 

_.min = function(obj, iteratee, context) {
  var result = Infinity, lastComputed = Infinity,
      value, computed;
  if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
    obj = isArrayLike(obj) ? obj : _.values(obj);
    for (var i = 0, length = obj.length; i < length; i++) {
      value = obj[i];
      if (value != null && value < result) {
        result = value;
      }
    }
  } else {
    iteratee = cb(iteratee, context);
    _.each(obj, function(v, index, list) {
      computed = iteratee(v, index, list);
      if (computed < lastComputed || computed === Infinity && result === Infinity) {
        result = v;
        lastComputed = computed;
      }
    });
  }
  return result;
};

和最大值的逻辑相同。

 

_.shuffle = function(obj) {
  return _.sample(obj, Infinity);
};

_.sample = function(obj, n, guard) {
  if (n == null || guard) {
    if (!isArrayLike(obj)) obj = _.values(obj);
    return obj[_.random(obj.length - 1)];
  }
  var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj);
  var length = getLength(sample);
  n = Math.max(Math.min(n, length), 0);
  var last = length - 1;
  for (var index = 0; index < n; index++) {
    var rand = _.random(index, last);
    var temp = sample[index];
    sample[index] = sample[rand];
    sample[rand] = temp;
  }
  return sample.slice(0, n);
};

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

_.random = function(min, max) { // max ? [min, max] : [0, min]
  if (max == null) {
    max = min;
    min = 0;
  }
  // min + floor( [0, (max - min + 1)) ) -> min + [0, (max - min)] -> [min, max]
  return min + Math.floor(Math.random() * (max - min + 1));
};

_.clone 浅复制一个数组或对象。

 _.random(min, max) 随机获得 [min, max] 之间的整数。

_.sample(obj, n, guard) 获得数组或对象所有值中随机的 n 个值,如果 n 等于 obj.length 就相当于打乱数组了。

guard 存在的话就忽略 n 使得 sample 可以直接应用到 map 中。

 

_.sortBy = function(obj, iteratee, context) {
  var index = 0;
  iteratee = cb(iteratee, context);
  return _.pluck(_.map(obj, function(value, key, list) {
    return {
      value: value,
      index: index++,
      criteria: iteratee(value, key, list)
    };
  }).sort(function(left, right) {
    var a = left.criteria;
    var b = right.criteria;
    if (a !== b) {
      if (a > b || a === void 0) return 1;
      if (a < b || b === void 0) return -1;
    }
    return left.index - right.index;
  }), 'value');
};

虽然不难理解 但是觉得挺巧妙的。

首先映射成 { value, index, comp(value, ...)(比较函数的计算值) } 对象,然后通过 sort 比较大小,如果计算值相同,会根据 index 保持原顺序。然后取 'value' 的值来获取数组原值。

 

// 返回一个函数 使用 iteratee 处理 obj 每一项后 将计算值和原值按照 behavior 进行分类
var group = function(behavior, partition) {
  return function(obj, iteratee, context) {
    var result = partition ? [[], []] : {};
    iteratee = cb(iteratee, context);
    _.each(obj, function(value, index) {
      var key = iteratee(value, index, obj);
      behavior(result, value, key);
    });
    return result;
  };
};

_.groupBy = group(function(result, value, key) {
  if (has(result, key)) result[key].push(value); else result[key] = [value];
});

_.indexBy = group(function(result, value, key) {
  result[key] = value;
});

_.countBy = group(function(result, value, key) {
  if (has(result, key)) result[key]++; else result[key] = 1;
});

_.partition = group(function(result, value, pass) {
  result[pass ? 0 : 1].push(value);
}, true);
// e.g.
function getScoreLevel(score) {
  if (score > 90) return 'A';
  if (score > 60) return 'B';
  return 'C';
}
console.log(_.groupBy([30, 40, 50, 60, 70, 80, 90, 100], getScoreLevel));
// { C: [ 30, 40, 50, 60 ], B: [ 70, 80, 90 ], A: [ 100 ] }

function isPass(score) {
  return score >= 60;
}
console.log(_.partition([30, 40, 50, 60, 70, 80, 90, 100], isPass));
// [ [ 60, 70, 80, 90, 100 ], [ 30, 40, 50 ] ]

groupBy 按照传入函数的计算值进行分类。indexBy 后面的值会覆盖前面的,所以应该是计算值唯一的时候使用。countBy 按照计算值统计个数。

partition 根据计算值分为两类。

 

var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
_.toArray = function(obj) {
  if (!obj) return [];
  if (_.isArray(obj)) return slice.call(obj);
  if (_.isString(obj)) {
    return obj.match(reStrSymbol);
  }
  if (isArrayLike(obj)) return _.map(obj, _.identity);
  return _.values(obj);
};

把一个对象转为数组。如果是 falsely 返回空数组。如果是数组返回数组副本,如果字符串就返回字符串分割成每个字符的数组,类数组转成数组返回,对象返回值的集合。

reStrSymbol正则,可以处理各种字符。

[^\ud800-\udfff] 是普通的 BMP 字符

[\ud800-\udbff][\udc00-\udfff] 是成对的代理项对

[\ud800-\udfff] 是未成对的代理项字

 

_.size = function(obj) {
  if (obj == null) return 0;
  return isArrayLike(obj) ? obj.length : _.keys(obj).length;
};

获取(类)数组的长度或者对象属性的个数。

 

492行,我终于把集合部分看完了!!!明天继续。

 

_.first = _.head = _.take = function(array, n, guard) {
  if (array == null || array.length < 1) return n == null ? void 0 : [];
  if (n == null || guard) return array[0];
  return _.initial(array, array.length - n);
};

_.initial = function(array, n, guard) {
  return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
};

initial 是获取数组除去最后 n 个元素的剩余部分,n 默认 1,如果传入 guard 也相当于 n 为 1。

first 是获取数组前 n 个元素。不传 n 或者传入 guard 则获取第一个元素。

guard 为了能够直接在 map 函数中使用。

 

// 获得数组最后 n 个元素
_.last = function(array, n, guard) {
  if (array == null || array.length < 1) return n == null ? void 0 : [];
  if (n == null || guard) return array[array.length - 1];
  return _.rest(array, Math.max(0, array.length - n));
};

// 获得数组除了前 n 个元素的剩余部分
_.rest = _.tail = _.drop = function(array, n, guard) {
  return slice.call(array, n == null || guard ? 1 : n);
};

见注释,没什么好说的。

 

_.compact = function(array) {
  return _.filter(array, Boolean);
};

返回数组中除了假值的元素。

 

var flatten = function(input, shallow, strict, output) {
  output = output || [];
  var idx = output.length;
  for (var i = 0, length = getLength(input); i < length; i++) {
    var value = input[i];
    if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
      if (shallow) {
        var j = 0, len = value.length;
        while (j < len) output[idx++] = value[j++];
      } else {
        flatten(value, shallow, strict, output);
        idx = output.length;
      }
    } else if (!strict) {
      output[idx++] = value;
    }
  }
  return output;
};

_.flatten = function(array, shallow) {
  return flatten(array, shallow, false);
};
// e.g.
let a = [ [1, 2, 3], [5, 6], 7, [ [8], [9] ] ];
console.log(_.flatten(a)); // [ 1, 2, 3, 5, 6, 7, 8, 9 ]
console.log(_.flatten(a, true)) // [ 1, 2, 3, 5, 6, 7, [ 8 ], [ 9 ] ]

从名字来看 flatten 要做的就是把一个多维数组拍平

如果传了 shallow 只会把二维数组拍平成一维数组 否则就递归全部拍平

shallow = false 的话 strict 应该不为 true,strict 表示扩展二维数组时如果一项不是数组就不加入最后扩展的结果。

output 不需要传 是在递归的时候用到的参数

 

_.without = restArguments(function(array, otherArrays) {
  return _.difference(array, otherArrays);
});

_.difference = restArguments(function(array, rest) {
  rest = flatten(rest, true, true);
  return _.filter(array, function(value){
    return !_.contains(rest, value);
  });
});
// e.g.
console.log(_.difference([1, 5, 8], [1, 2, 3], 5, [[8], [9]])); // [1, 5, 8] 中不在 [ 1, 2, 3, [ 8 ], [ 9 ] ] 的元素 => [ 5, 8 ]
console.log(_.without([1, 5, 8], 1, 2, 3)); // [1, 5, 8] 中不在 [ 1, 2, 3 ] 的元素 => [ 5, 8 ]
let obj = [{ m: 'd' }, { z: 'z' }];
console.log(_.without(obj, obj[0], { z: 'z' })); // [ { z: 'z' } ]

difference 将多余的参数作为数组 [rest] 传入 然后在拍平为一维数组,也就是说 difference 的参数是一堆参数

然后求的是第一个数组中删除后面所有数组都不包含的元素 剩下的部分

without 把多余的参数作为数组 [otherArrays] 传入 然后在传入 difference

也就是说 without 传入一个数组和一些值 判断数组中不包含在后面那些值的元素

 

_.uniq = _.unique = function(array, isSorted, iteratee, context) {
  if (!_.isBoolean(isSorted)) { // 如果 isSorted 不是布尔类型就认为没传 isSorted 默认为false
    context = iteratee;
    iteratee = isSorted;
    isSorted = false;
  }
  if (iteratee != null) iteratee = cb(iteratee, context);
  var result = [];
  var seen = [];
  for (var i = 0, length = getLength(array); i < length; i++) {
    var value = array[i],
        computed = iteratee ? iteratee(value, i, array) : value;
        // 没有传入 iteratee 就直接比较 value
    if (isSorted && !iteratee) { 
      // 如果是有序的 直接比较和前一个不相同就可以
      // 如果传入 iteratee 即使是有序的计算值也不一定有序所以忽略 isSorted 信息
      if (!i || seen !== computed) result.push(value);
      seen = computed;
    } else if (iteratee) { // 否则要判断之前没有存过该元素 传入计算函数比较计算值
      if (!_.contains(seen, computed)) {
        seen.push(computed);
        result.push(value);
      }
    } else if (!_.contains(result, value)) { // 没有传入计算函数就直接比较原值
      result.push(value);
    }
  }
  return result;
};

_.union = restArguments(function(arrays) {
  return _.uniq(flatten(arrays, true, true));
});
// e.g.
var getName = _.property('name'); // 获取一个对象的 name 属性
var staff = [ { name: 'joy', age: 19 }, { name: 'john', age: 19 }, { name: 'joy', age: 88 } ];
_.uniq(staff, getName); // [ { name: 'joy', age: 19 }, { name: 'john', age: 19 } ]
_.union([1,2], [2, 3], [3, 4]); // [ 1, 2, 3, 4 ]

_.uniq 将数组去重,如果传入计算函数就按照计算值去重 留第一个元素 否则直接比较元素

_.union 将传入的多个数组合并成一个数组然后去重

 

_.intersection = function(array) {
  var result = [];
  var argsLength = arguments.length;
  for (var i = 0, length = getLength(array); i < length; i++) {
    var item = array[i];
    if (_.contains(result, item)) continue;
    var j;
    for (j = 1; j < argsLength; j++) {
      if (!_.contains(arguments[j], item)) break;
    }
    if (j === argsLength) result.push(item);
  }
  return result;
};
// e.g.
_.intersection([1,2,3], [2, 3], [3, 4]); // [ 3 ]

取多个数组的交集。在遍历第一个数组的元素,如果后面每个数组都包含,就加入结果数组。

 

_.unzip = function(array) {
  // array 是一个二维数组
  var length = array && _.max(array, getLength).length || 0; // 获取 array[0..len-1]中最长的数组的长度
  var result = Array(length);
  // result[i] 是一个数 由 array[0..len-1][i] 组成
  for (var index = 0; index < length; index++) {
    result[index] = _.pluck(array, index);
  }
  return result;
};

// 传入多个数组 然后通过 restArguments 合成一个二维数组传入 unzip
_.zip = restArguments(_.unzip);
// e.g.
var obj = [ ['张三', 18, '男'], ['李四', 16, '女'], ['王五', 23, '男'] ];
_.unzip(obj); // [ [ '张三', '李四', '王五' ], [ 18, 16, 23 ], [ '男', '女', '男' ] ]
_.zip([ '张三', '李四', '王五' ], [ 18, 16, 23 ], [ '男', '女', '男' ]); // [ [ '张三', 18, '男' ], [ '李四', 16, '女' ], [ '王五', 23, '男' ] ]

zip 和 unzip 功能差不多啊 就是传入多个数组,然后把下标相同的元素放到同一个数组,不过 zip 传入多个数组 unzip 传入二维数组

目前不太理解有什么用

 

_.object = function(list, values) {
  var result = {};
  for (var i = 0, length = getLength(list); i < length; i++) {
    if (values) {
      result[list[i]] = values[i];
    } else {
      result[list[i][0]] = list[i][1];
    }
  }
  return result;
};
// e.g.
_.object([ ['LENGTH', 34], ['WIDTH', 43] ]); // { LENGTH: 34, WIDTH: 43 }
_.object([ '叶修', '苏沐橙' ], ['君莫笑', '沐雨橙风']); // { '叶修': '君莫笑', '苏沐橙': '沐雨橙风' }

数组转键值对,一种是二维数组,一种是两个对应的数组。

 

_.range = function(start, stop, step) {
  if (stop == null) { // 只有一个参数作为 stop 默认 start 为 0
    stop = start || 0;
    start = 0;
  }
  if (!step) { // 默认 step 为 ±1
    step = stop < start ? -1 : 1;
  }
  // 计算 range 长度
  var length = Math.max(Math.ceil((stop - start) / step), 0);
  var range = Array(length);

  for (var idx = 0; idx < length; idx++, start += step) {
    range[idx] = start;
  }

  return range;
};
// e.g.
_.range(10); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
_.range(13, 100, 17); // [ 13, 30, 47, 64, 81, 98 ]

设定初始值,结束值和步长,生成一个数组。

 

_.chunk = function(array, count) {
  if (count == null || count < 1) return [];
  var result = [];
  var i = 0, length = array.length;
  while (i < length) {
    result.push(slice.call(array, i, i += count));
  }
  return result;
};
//e.g.
var _1To10 = _.range(1, 11); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
_.chunk(_1To10, 3); // [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ], [ 10 ] ]

将数组分块,按顺序,每 count 个分成一块。

 

755 行,下面是函数部分了。再开一篇博客写,太长了。

下接 《Underscore.js 源码学习笔记(下)

posted @ 2018-12-06 20:45  我不吃饼干呀  阅读(582)  评论(2编辑  收藏  举报