Underscore 源码学习笔记

本文详细解析了Underscore.js源码中的关键部分,包括内部函数optimizeCb和restArgs的功能与实现,函数节流与防抖的区别及实现方式,eq相等判断的深入分析,以及isObject对象判断的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Underscore 源码学习笔记

看完了 zepto 的源码,但是却觉得看完之后没有什么收获,在看源码的过程中也有过一些思考与分析,但是由于没有养成记笔记的习惯,导致看完之后脑子里已经剩不下什么东西了。现在我也只能说自己看完过zepto源码,但是没能从中得到多少具体的提升,十分遗憾。
正巧最近又开始学习 underscore 的源码,趁着国庆节后有空闲的时间顺便把阅读笔记写下,能帮助自己记录记录从中的收获。

void 0

void 0 在underscore中的使用非常普遍,用来替代 undefined,如:

if (context === void 0) return func;

根据MDN中对void的解释,void修饰符会对其后的表达式进行求值,并且返回undefined。
由于undefined在js中只是一个全局变量,可能会被修改导致不必要的麻烦,因此使用void来返回undefined,同时,void后的表达式的值无关紧要,所以选用最短的0。
就是一个简单的小trick吧,但是如果没有听说过void修饰符的话,阅读源码的时候可能会一头雾水。

内部函数 optimizeCb

var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    switch (argCount) {
      case 1: return function(value) {
        return func.call(context, value);
      };
      // The 2-parameter case has been omitted only because no current consumers
      // made use of it.
      case null:
      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);
    };
  };

optimizeCb函数功能如其名所言,通过函数柯里化做上下文绑定(context binding)。通过call与apply为指定函数绑定作用域,并且根据参数数量返回不同结果。其中,由于两个参数的版本没有使用到,因此在代码中已经删去。

内部函数restArgs

var restArgs = function(func, startIndex) {
    startIndex = startIndex == null ? func.length - 1 : +startIndex; //注意func.length - 1
    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);
    };
  };

函数功能参考ECMAScript 6 and Rest Parameter
restArgs函数基本功能就是将指定数量参数以外的数值以数组的形式作为参数传入。其中 func.length 的值为该函数定义中参数的个数。具体定义参照MDN
其中,需要注意的是:func.length - 1。一开始看可能会不理解,觉得-1多此一举,误算了参数的个数。但是实际使用过程中,函数声明中参数都会带上一个args,其对应着rest参数。因此在实际计算中需要将func参数列表中减去args。

函数节流和函数防抖

函数节流:

_.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // remaining > wait 考虑如果客户端时间受到了更改,则立刻执行
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 如果 option.trailing 为 true 则延迟执行
        // 如果 option.trailing 为 false 则忽略该次调用
        timeout = setTimeout(later, remaining);
      }
      return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };

函数防抖:

_.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArgs(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        // 如果 immediate 为 true 同时没有定时器工作时,立即执行
        if (callNow) result = func.apply(this, args);
      } else {
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

函数节流将以某时间间隔为限制控制回调函数的执行,保证在该间隔时间里重复的函数调用被覆盖或者推迟到间隔结束;函数防抖将延后函数执行,如果等待过程中函数又被调用则会重置时钟。
两者实现并不复杂,但是在实际使用过程中接触还是蛮多的,mark一下,以后也许还能用着。

eq 相等判断

两个对象的等价在js中还是蛮麻烦的一件事情,包含了许多的规则与技巧,在undersocre中的做法考虑情况比较周密,可以很好的作为参考。(其实是因为面试的时候常会碰到类似的问题,所以就仅仅从功利的角度来看也是挺重要的。)
eq函数:

eq = function(a, b, aStack, bStack) {
    // 0 和 -0 不相等,用 1 / a 可以得到 Infinity 和 -Infinity
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    // null 和 undefined 仅与自身等价
    if (a == null || b == null) return false;
    // NaN 与 NaN 等价,但是其与自身不等价
    if (a !== a) return b !== b;
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
    return deepEq(a, b, aStack, bStack);
  };

deepEq函数:

  deepEq = function(a, b, aStack, bStack) {
    // 原型和其封装类等价:'a' 和 new String('a') 等价
    if (a instanceof _) a = a._wrapped;
    if (b instanceof _) b = b._wrapped;
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;
    switch (className) {
      case '[object RegExp]':
      case '[object String]':
        // RexExp和String类型都可以转化为字符串进行比较
        return '' + a === '' + b;
      case '[object Number]':
        // 同之前eq函数中,NaN 与 NaN 相等,但与其自身不相等
        if (+a !== +a) return +b !== +b;
        // 判断数字值的同时,排除 0 和 -0 的情况
        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
        // 布尔与时间类型可以转换为数值进行比较
        // 转换结果分别为:false -> 0,true -> 1,date -> 毫秒数
        return +a === +b;
      case '[object Symbol]':
        return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
    }

    var areArrays = className === '[object Array]';
    if (!areArrays) {
      if (typeof a != 'object' || typeof b != 'object') return false;

      // constructor不同的object不等价,但是要求该object的确是由该constructor构造出的实例
      var aCtor = a.constructor, bCtor = b.constructor;
      if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
                               _.isFunction(bCtor) && bCtor instanceof bCtor)
                          && ('constructor' in a && 'constructor' in b)) {
        return false;
      }
    }
    // Assume equality for cyclic structures. The algorithm for detecting cyclic
    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

    // aStack 和 bStack 存储比较过的元素,用于防止陷入无限循环
    aStack = aStack || [];
    bStack = bStack || [];
    var length = aStack.length;
    while (length--) {
      // 判断当前元素是否已比较过
      if (aStack[length] === a) return bStack[length] === b;
    }

    aStack.push(a);
    bStack.push(b);

    // 递归比较每一个元素
    if (areArrays) {
      length = a.length;
      // length 不相等则直接返回 false
      if (length !== b.length) return false;
      while (length--) {
        if (!eq(a[length], b[length], aStack, bStack)) return false;
      }
    } else {
      var keys = _.keys(a), key;
      length = keys.length;
      // 与数组相同,比较前先计算key的数量,从而优化比较效率
      if (_.keys(b).length !== length) return false;
      while (length--) {
        key = keys[length];
        if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
      }
    }

    aStack.pop();
    bStack.pop();
    return true;
  };

判断是否为对象 isObject

isObject:

_.isObject = function(obj) {
    var type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
  };

函数本身很简单,只需要用typeof判断就行了。但是需要注意的是,Function 类型的对象 typeof 的返回值是 ‘function’。
这一个小小的特例点,勾起了我面试的回忆……虽然心里清楚这个知识点,但是面试的时候一紧张就给忘了,心理素质还是有待加强啊。
此外,更普遍的判断类型的方法还是使用 toString 比较好,可以返回 ‘[object 类型]’,更具有普适性。

总结思考

结合之前阅读其他源码的经验,我觉得自己也慢慢摸到了阅读源码的大致方法。从一开始阅读 JQuery 时毫无头绪,想到哪看到哪,到现在也慢慢有了一些章法。
大致上,阅读源码的思路是从 整体—>局部 。首先要了解整个项目的架构,理解文件的结构,对整体的项目有一个大致的掌握。然后阅读具体的代码,理解其实现过程。如果你手上拿到的是一份完全陌生的代码,那么先阅读API文档会给你带来极大的帮助。预先了解代码功能,猜测其可能的实现方式,不仅能够帮助你更好的理解源码,也能够给你与他人比较的机会,并从中得到提高。

// 关于 underscore 的源码学习也就告一段落了,等到什么时候对函数式编程有了强烈的兴趣之后再回头来看第二遍吧。滚去学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值