美文网首页
1.underscore.js源码解读记录

1.underscore.js源码解读记录

作者: yaoyao妖妖 | 来源:发表于2019-07-10 14:30 被阅读0次
    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源码解读记录

          本文链接:https://www.haomeiwen.com/subject/icxhkctx.html