1.underscore.js源码解读记录

1. 作用域包裹,通过立即执行函数来包裹自己的业务逻辑

作用:(不受外部影响,也不给外部添麻烦)

  • 避免全局污染:所有库的逻辑,库所定义和使用的变量全部被封装到了该函数的作用域中
  • 隐私保护:但凡在立即执行函数中声明的函数、变量等,除非是自己想暴露。否则不可能在外部获得
(function() {
  //  ...执行逻辑
})(this)
2. 对象 _,函数对象,之后所有的API会被挂载在这个对象上,例如: _.each _.map
  var _ = function (obj) {
    // 以下均针对 OOP 形式的调用
    // 如果是非 OOP 形式的调用,不会进入该函数内部
    // 如果 obj 已经是 `_` 函数的实例,则直接返回 obj
    if (obj instanceof _) { return obj; }
    // 如果不是 `_` 函数的实例
    // 则调用 new 运算符,返回实例化的对象
    if (!(this instanceof _)) { return new _(obj); }
    // 将 obj 赋值给 this._wrapped 属性
    this._wrapped = obj;
  };

3.执行环境的判断

如何使你的库既能够服务于浏览器,又能服务于诸如nodejs所搭建的服务器端,underscore对象_会将依托当前所处的环境,挂载到不同的全局空间中,浏览器的全局对象是windowsWindows,node的全局对象是global。

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

Tips:在这里写self是为了一些不具有窗口的上下文环境中,例如WebWorker
其次,如果处于node环境,_还将会被作为模块导出

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = _;
    }
    exports._ = _;
  } else {
    root._ = _;
  }
4.松弛绑定

默认情况下,underscore对象会覆盖对象同名的属性,但是他会保存之前存在的属性,因为想lodash这样的一些库也喜欢将自己的对象命名为

var previousUnderscore = root._;

当当前用户已经在全局对象上绑定了对象的时候,可以通过underscore提供的noConflict函数来重命名underscore对象,或者说是手动获得underscore对象,避免与之前的冲突

var underscore = _.noConflict();

noConflict源码,在其内部,将会恢复原来对象上的_:

/**
 * 返回一个 underscore 对象,把_所有权交还给原来的拥有者(比如 lodash)
 */
_.noConflict = function () {
  //  回复原来的_指代的对象
  root._ = previousUnderscore;
  //  返回 underscore 对象
  return this;
};
5.局部变量的妙用
  • underscore本身也一来了不少js原生方法,他会通过局部变量来保存一些他经常用到的方法或者属性,避免了冗长代码的书写
  • 减少了对象成员的访问深度,,(Array.prototype.push --> push), 这样做能带来一定的性能提升
  // 缓存变量便于压缩代码,值得是压缩到min.js的版本
  var ArrProto = Array.prototype;
  var ObjProto = Object.prototype;
  var FuncProto = Function.prototype;

  // 缓存变量便于压缩代码,值得是压缩到min.js的版本
  // 同时可以减少在原型链中的查找次数(提高代码效率)
  var push = ArrProto.push;
  var slice = ArrProto.slice;
  var toString = ObjProto.toString;
  var hasOwnProperty = ObjProto.hasOwnProperty;

  //ES5的原生的方法,如果浏览器支持,则 underscore 中会优先使用
  var nativeArray = Array.isArray;
  var nativeKeys = Object.keys;
  var nativeBind  = FuncProto.bind;
  var nativeCreate = Object.create;
6.undefined的处理

因为JS中undefined并不是可靠的,因为这个标识符可能会被改写
例如:

function test(a) {
  var undefined = 1;
  console.log(undefined); //  => 1
  if (a === undefined) {
    // ...
  }
}
// 在 ES5 之前,全局的 undefined 也是可以被修改的,而在 ES5 中,该标识符被设计为了只读标识符, 假如你现在的浏览器不是太老,你可以在控制台中输入以下语句测试一下:
undefined = 1;
console.log(undefined); // => undefined

可见,标识符undefined并不能真正的反应‘未定义’,所以我们需要通过其他手段。JS提供了void运算符,这个运算符会被指定的表达式求职,并返回受信任的undefined:

void expression
// 最常见的用法是通过以下运算来获得 undefined,表达式为 0 时的运算开销最小:
void 0;   or   void(0);
// 所以在underscore 中,所有需要获得 undefined 地方,都通过void 0进行了替代

当然,曲线救国的方式不只一种,我们看到包裹jquery的立即执行函数:

// 在这个函数中,我们没有向其传递第二参数(形参名叫 undefined),那么第二个参数的值就会被传递上 “未定义”,因此,通过这种方式,在该函数的作用域中所有的 undefined 都为受信的 undefined。
(function(window, undefined) {
  //  ...
})(window)
7.使用迭代,而不是循环
// 迭代
var results = _.map([1,2,3], function(elem) {
  return elem * 2;
}); //  => [2, 4, 6]
// 循环
var results = [];
var elems = [1, 2, 3];
for (var i = 0, length = elems.length; i < length; i++) {
  result.push(elems[i]*2);
} //  => [2, 4, 6]
迭代 iteratee

对于一个迭代来说,他至少由2部分组成,

  1. 被迭代的集合
  2. 当前迭代过程:在 underscore 中,当前迭代过程是一个函数,他被称为 iteratee(直译为被迭代者),他将对当前的迭代元素进行处理。
  3. 我们看到 _.map 的实现
  // Math.pow(2, 53) - 1 是 JavaScript 中能精确表示的最大数字
  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

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

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

  // 返回一个对象的 keys 组成的数组
  // 仅返回 own enumerable properties 组成的数组
  _.keys = function (obj) {
    // 容错
    // 如果传入的参数不是对象,则返回空数组
    if (!_.isObject(obj)) return [];

    // 如果浏览器支持 ES5 Object.key() 方法
    // 则优先使用该方法
    if (nativeKeys) return nativeKeys(obj);

    var keys = [];

    // own enumerable properties
    for (var key in obj)
    // hasOwnProperty
    { if (_.has(obj, key)) keys.push(key); }

    // Ahem, IE < 9.
    // IE < 9 下不能用 for in 来枚举某些 key 值
    // 传入 keys 数组为参数
    // 因为 JavaScript 下函数参数按值传递
    // 所以 keys 当做参数传入后会在 `collectNonEnumProps` 方法中改变值
    if (hasEnumBug) collectNonEnumProps(obj, keys);

    return keys;
  };

  // 遍历数组(每个元素)或者对象的每个元素(value)
  // 对每个元素执行 iteratee 迭代方法
  // 将结果保存到新的数组中,并返回
  _.map = _.collect = function (obj, iteratee, context) {
    // 根据 context 确定不同的迭代函数
    iteratee = cb(iteratee, context);

    // 如果传参是对象,则获取它的 keys 值数组(短路表达式)
    var keys = !isArrayLike(obj) && _.keys(obj);
    // 如果 obj 为对象,则 length 为 key.length
    // 如果 obj 为数组,则 length 为 obj.length
    var length = (keys || obj).length;
    var results = Array(length); // 结果数组, 定长初始化数组

    // 遍历
    for (var index = 0; index < length; index++) {
      // 如果 obj 为对象,则 currentKey 为对象键值 key
      // 如果 obj 为数组,则 currentKey 为 index 值
      var currentKey = keys ? keys[index] : index;
      results[index] = iteratee(obj[currentKey], currentKey, obj);
    }

    // 返回新的结果数组
    return results;
  };

我们传递给的 _.map 的第二个参数就是一个 iteratee,他可能是函数,对象,甚至是字符串,underscore 会将其统一处理为一个函数。这个处理由 underscore 的内置函数 cb 来完成。
cb 的实现如下:

var cb = function(value, context, argCount) {
  //  是否用自定义的iteratee;重置默认的.iteratee改变迭代过程中的行为只在underscore最新的master分支支持, 发布版的1.8.3并不支持
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  //  针对不同的情况
  if (value == null)  return _.identity;
  if (_.isFunction(value))  return optimizeCb(value, context, argCount);
  if (_.isObject(value)) return _.matcher(value);

  return _.property(value);
};
  
  //返回传入的参数,统一将value做函数处理, _.identity 在 undescore 内大量作为迭代函数出现
  // 能简化很多迭代函数的书写
  _.identity = function (value) {
    return value;
  };
  // 判断一个给定的对象是否有某些键值对
  _.matcher = _.matches = function (attrs) {
    attrs = _.extendOwn({}, attrs);
    return function (obj) {
      return _.isMatch(obj, attrs);
    };
  };
 // 闭包
  var property = function (key) {
    return function (obj) {
      return obj == null ? void 0 : obj[key];
    };
  };
  _.property = property;

cb 将根据不同情况来为我们的迭代创建一个迭代过程 iteratee,服务于每轮迭代:

  • value 为 null
    如果传入的 value 为 null,亦即没有传入 iteratee,则 iteratee 的行为只是返回当前迭代元素自身,比如:
var results = _.map([1, 2, 3]); //  => results: [1, 2, 3]
  • value 为一个函数
    如果传入 value 是一个函数,那么通过内置函数 optimizeCb 对其进行优化,optimizeCb 的作用放到之后讲,先来看个传入函数的例子
// => results:  [
//  "[1,2,3]'s 0 position is 1",
//  "[1,2,3]'s 1 position is 2",
//  "[1,2,3]'s 2 position is 3"
// ]
  • value 为一个对象
    如果 value 传入的是一个对象,那么返回的 iteratee(_.matcher)的目的是想要知道当前被迭代元素是否匹配给定的这个对象:
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
//  => results: [false, true]
  • value 是字面量,如数字,字符串等
    var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
    // => results: [false, true]
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj'}], 'name');
//  results: ['yoyoyohamapi', 'wxj'];

#######自定义的 iteratee
在 cb 函数的代码中,我们也发现了 underscore 支持通过覆盖其提供的 _.iteratee 函数来自定义 iteratee,更确切的说,来自己决定如何产生一个 iteratee:

var cb = function (value, context, argCount) {
  // ...
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  // ...
}

我们看一下 iteratee 函数的实现:

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

默认的 _.iteratee 函数仍然是把生产 iteratee 的工作交给 cb 完成,并且通过变量 buildIteratee 保存了默认产生器的引用,方便之后我们覆盖了 _.iteratee 后,underscore 能够通过比较 _.iteratee 与 buildIteratee 来知悉这次覆盖(也就知悉了用户想要自定义 iteratee 的生产过程)。
比如当传入的 value 是对象时,我们不想返回一个 _.matcher 来判断当前对象是否满足条件,而是返回当前元素自身(虽然这么做很无聊),就可以这么做:

_.iteratee = function(value, context) {
  //  现在,value为对象时,也是返回自身
  if (value == null || _.isObject(value)) return _.identity;
  if (_.isFunction(value))  return optimizeCb(value, context, argCount);
  return _.property(value);
};

现在运行之前的例子,看一下有什么不同:

var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
//  => results: [{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}];
8.optimizeCb(优化回调)

当传入的 value 是一个函数时,value 还要经过一个叫 optimizeCb 的内置函数才能获得最终的 iteratee

/** 优化回调(特指函数中传入的回调)
 *
 * @param func 待优化回调函数
 * @param context 执行上下文
 * @param argCount 参数个数
 * @returns {function}
 */
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);
    };
    case 2: return function(value, other) {
      return func.call(context, value, other);
    };
    case 3: return function() {
      return func.call(context, value, index, collection);
    };
    case 4: return function() {
      return func.call(context, accumlator, value, index, collection);
    };
  }
  return function() {
    return func.apply(context, arguments);
  };
};

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

  • argCount == 1,即 iteratee 只需要 1 个参数
    在 underscore 的 .times 函数的实现中,.times 的作用是执行一个传入的 iteratee 函数 n 次,并返回由每次执行结果组成的数组。它的迭代过程 iteratee 只需要 1 个参数 -- 当前迭代的索引:
//  执行iteratee函数n次,返回每次执行结果构成的数组
_.times = function(n, iteratee, context) {
  var accum = Array(Math.max(0, n));
  iteratee = optimizeCb(iteratee, context, 1);
  for (var i = 0; i < n; i++) accum[i] = iteratee(i);
  return accum;
};
//  看一个 _.times 的使用例子:
function getIndex(index) {
  return index;
}
var results = _.times(3, getIndex); // => [0, 1, 2]
  • argCount == 2,即 iteratee 需要 2 个参数
    该情况在 underscore 没用使用,所以最新的 master 分支已经不再考虑这个参数个数为 2 的情况。
  • argCount == 3(默认),即 iteratee 需要 3 个参数
    这 3 个参数是:
    value:当前迭代元素的值
    index:迭代索引
    collection:被迭代集合
    在 _.map, _.each, _.filter 等函数中,都是给 argCount 赋值了 3:
_.each([1, 2, 3], function() {
  console.log("被迭代的集合:"+collection+"; 迭代索引:"+index+"; 当前迭代的元素值"+value);
});
// =>
// 被迭代的集合:1,2,3; 迭代索引:0; 当前迭代的元素值:1
// 被迭代的集合:1,2,3; 迭代索引:1; 当前迭代的元素值:2
// 被迭代的集合:1,2,3; 迭代索引:2; 当前迭代的元素值:3
  • argCount == 4,即 iteratee 需要 4 个参数
    这 4 个参数分别是:
    accumulator:累加器
    value:迭代元素
    index:迭代索引
    collection:当前迭代集合
    那么这个累加器是什么意思呢?在 underscore 中的内部函数 createReducer 中,就涉及到了 4 个参数的情况。该函数用来生成 reduce 函数的工厂,underscore 中的 _.reduce 及 _.reduceRight 都是由它创建的:
/**
 * reduce 函数的工厂函数,用于生成一个reducer,通过参数决定reduce的方向
 * @param dir 方向 left or right
 * @returns {function}
 */
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;
    //  memo用来记录最新的reduce结果
    //  如果reduce没有初始化 memo, 则默认为首个元素 (从左开始则为第一个元素, 从右则为最后一个元素)
    if (!initial) {
      memo = obj[keys ? keys[index] : index]; 
      index += dir;
    }
    for (; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      //  执行 reduce 回调, 刷新当前值
      memo = iteratee(memo, obj[currentKey], currentKey, obj);
    } 
    return memo;        
  };

  return function () {
    // 如果参数正常, 则代表已经初始化了 memo
    var initial = arguments.length >= 3;
    //  reducer 因为引入了累加器, 所以优化函数的第三个参数传入了 4,
    //  这样, 新的迭代回调第一个参数就是当前的累加结果
    return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
  };
};

我们可以看到,createReduce 最终创建的 reducer 就是需要一个累加器,该累加器需要被初始化,看一个利用 _.reduce 函数求和的例子:

var sum = _.reduce([1,2,3,4,5], function(accumlator, value, index, collection) {
  return accumlator + value;
}, 0); //  => 15;
9.ubderscore是如何创建对象的
/**
 * 创建一个对象,该对象继承自prototype
 * 并且保证该对象在其原型上挂载属性不会影响所继承的prototype
 * @param {object} prototype
 */
var baseCreate = function (prototype) {
    if (!_.isObject(prototype)) return {};
    // 如果存在原生的创建方法(Object.create),则用原生的进行创建
    if (nativeCreate) return nativeCreate(prototype);
    // 利用Ctor这个空函数,临时设置对象原型
    Ctor.prototype = prototype;
    // 创建对象,result.__proto__ === prototype
    var result = new Ctor;
    // 还原Ctor原型
    Ctor.prototype = null;
    return result;
};

我们可以看到,underscore 利用 baseCreate 创建对象的时候会先检查当前环境是否已经支持了 Object.create,如果不支持,会创建一个简易的 polyfill:

// 利用Ctor这个空函数,临时设置对象原型
Ctor.prototype = prototype;
// 创建对象,result.__proto__ === prototype
var result = new Ctor;
// 防止内存泄漏,因为闭包的原因,Ctor常驻内存
Ctor.prototype = null;

而之所以叫 baseCreate,也是因为其只做了原型继承,而不像 Object.create 那样还支持传递属性列表。

转自:https://www.jianshu.com/p/91c329e902a5

你可能感兴趣的:(1.underscore.js源码解读记录)