Underscore.js 1.8.3 学习笔记

underscore.js源码加注释一共1500多行,它提供了一整套函数式编程实用的功能,一共一百多个函数,几乎每个函数均可以做为参考典范。初读的时候,真是一脸懵圈,各类函数闭包、迭代和嵌套的使用,让我一时很难消化。
在这里,我来记录一下我学习underscore.js的一些发现,以及几个我认为比较经典的函数使用。css

首先咱们能够看到,underscore.js中全部的函数和方法都在一个闭包里:(function() {...}.call(this));这么作的目的是为了不污染全局变量。正则表达式

为了压缩代码,underscore中用到将原型赋值给变量保存的方法:算法

// Save bytes in the minified (but not gzipped) version:
  // 原型赋值,便于压缩代码,这里的压缩指压缩到min.js而不是gzip压缩
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  // Create quick reference variables for speed access to core prototypes.
  // 将内置对象原型中的经常使用方法赋值给引用变量,减小在原型链中的查找次数,从而提升代码效率
  var
    push             = ArrayProto.push,
    slice            = ArrayProto.slice,
    toString         = ObjProto.toString,
    hasOwnProperty   = ObjProto.hasOwnProperty;

咱们在处理代码时,不能直接将Array.prototype等直接压缩,由于压缩事后,浏览器是没法识别这些压缩字段的。压缩事后,咱们在使用obj.prototype方法时,直接使用其相对应的变量就能够了。若是在咱们的代码中会屡次用到某个方法,用上面的方法就进行处理,使用起来就方便多了。express

接着建立了一个"_"对象,以后将underscore中的相关方法添加到"_"原型中,那么建立的"_"对象也就具有了underscore方法。编程

// 建立一个"_"对象
var _ = function(obj) {
    if (obj instanceof _) return obj;  //若是obj是"—"的实例,则直接返回obj
    if (!(this instanceof _)) return new _(obj);  //若是不是,则调用new运算符,返回实例化的对象
    this._wrapped = obj;    //将underscore对象存放在_.wrapped属性中
  };

上面用到了instanceof运算符,JavaScript中instanceof运算符是返回一个 Boolean 值,指出对象是不是特定类的一个实例。
使用方法:result = object instanceof class
其中,result是必选项,表任意变量;object是必选项,表任意对象表达式;class是必选项,表任意已定义的对象类。若是 object 是 class 的一个实例,则 instanceof 运算符返回 true。若是 object 不是指定类的一个实例,或者 object 是 null,则返回 false。api

接下来我就列举几个underscore中的函数。数组

一、_.each浏览器

_.each = _.forEach = function(obj, iteratee, context) {
    iteratee = optimizeCb(iteratee, context);  // 根据 context 肯定不一样的迭代函数
    var i, length;
    if (isArrayLike(obj)) { // 若是是类数组 (默认不会传入相似 {length: 10} 这样的数据)
      for (i = 0, length = obj.length; i < length; i++) {  //遍历
        iteratee(obj[i], i, obj);
      }
    } else {  // 若是 obj 是对象
      var keys = _.keys(obj);  // 获取对象的全部 key 值
      for (i = 0, length = keys.length; i < length; i++) {  //若是是对象,则遍历处理 values 值
        iteratee(obj[keys[i]], keys[i], obj);
      }
    }
    return obj; //返回 obj 参数,供链式调用(Returns the list for chaining)
  };

_.each = _.forEach = function(obj, iteratee, context)中,一共有三个参数:
第一个参数为数组(包括类数组)或者对象;第二个参数为迭代方法,对数组或者对象每一个元素都执行
该方法,该方法又能传入三个参数,分别为 (item, index, array)((value, key, obj) for object);
第三个参数(可省略)肯定第二个参数 iteratee 函数中的(可能有的)this 指向,
即 iteratee 中出现的(若是有)全部 this 都指向 context。

二、_.contains缓存

_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
    if (!isArrayLike(obj)) obj = _.values(obj); // // 若是是对象,返回 values 组成的数组
    //fromIndex 表示查询起始位置,若是没有指定该参数,则默认从头找起
    if (typeof fromIndex != 'number' || guard) fromIndex = 0;
    //_.indexOf 是数组的扩展方法(Array Functions)
    //数组中寻找某一元素
    return _.indexOf(obj, item, fromIndex) >= 0; 
  };

判断数组或者对象中(value 值)是否有指定元素,若是是 object,则忽略 key 值,只须要查找 value 值便可,若是obj 中是否有指定的 value 值,则返回布尔值。闭包

三、 _.uniq

_.uniq = _.unique = function(array, isSorted, iteratee, context) {
    if (!_.isBoolean(isSorted)) { // 没有传入 isSorted 参数
      context = iteratee;
      iteratee = isSorted;
      isSorted = false;     // 转为 _.unique(array, false, undefined, iteratee)
    }
    // 若是有迭代函数,则根据 this 指向二次返回新的迭代函数
    if (iteratee != null) iteratee = cb(iteratee, context); 
    var result = [];  // 结果数组,是 array 的子集
    var seen = [];    //// 已经出现过的元素(或者通过迭代过的值),用来过滤重复值
    for (var i = 0, length = getLength(array); i < length; i++) {
      var value = array[i],
      //若是指定了迭代函数,则对数组每个元素进行迭代
      //迭代函数传入的三个参数一般是 value, index, array 形式
          computed = iteratee ? iteratee(value, i, array) : value; 
          //若是是有序数组,则当前元素只需跟上一个元素对比便可,并用 seen 变量保存上一个元素
      if (isSorted) {
          //若是 i === 0,是第一个元素,则直接 push,不然比较当前元素是否和前一个元素相等
        if (!i || seen !== computed) result.push(value);
        seen = computed; // seen 保存当前元素,供下一次对比
      } else if (iteratee) {
        if (!_.contains(seen, computed)) { //// 若是 seen[] 中没有 computed 这个元素值
          seen.push(computed);
          result.push(value);
        }
      } else if (!_.contains(result, value)) {
          // 若是不用通过迭代函数计算,也就不用 seen[] 变量了
        result.push(value);
      }
    }
    return result;
  };

这是一个数组去重函数,若是函数参数中第二个参数 isSorted 为 true,则说明事先已经知道数组有序,程序会跑一个更快的算法;若是有第三个参数 iteratee,则对数组每一个元素迭代,对迭代以后的结果进行去重,而后返回去重后的数组(array 的子数组);另外,暴露的 API 中没 context 参数。

四、_.range

// 返回某一个范围内的数组成的数组
  _.range = function(start, stop, step) {
    if (stop == null) {
      stop = start || 0;
      start = 0;
    }
    step = step || 1;
    // 返回数组的长度
    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;
  };

_.range([start], stop, [step]) 是一个用来建立整数灵活编号的列表的函数,便于each 和 map循环。若是省略start则默认为 0;step 默认为 1.返回一个从start 到stop的整数的列表,用step来增长 (或减小)独占。值得注意的是,若是stop值在start前面(也就是stop值小于start值),那么值域会被认为是零长度,而不是负增加。-若是要一个负数的值域 ,则使用负数step. (参考http://www.css88.com/doc/unde...

五、_.bind

_.bind = function(func, context) {
    if (nativeBind && func.bind === nativeBind) 
        // 若是浏览器支持 ES5 bind 方法,而且 func 上的 bind 方法没有被重写,则优先使用原生的 bind 方法
        return nativeBind.apply(func, slice.call(arguments, 1));

    if (!_.isFunction(func)) 
        //若是传入的参数 func 不是方法,则抛出错误
        throw new TypeError('Bind must be called on a function');

    //经典闭包,函数返回函数
    var args = slice.call(arguments, 2); // args 获取优先使用的参数
    var bound = function() {
        //最终函数的实际调用参数由两部分组成
        //一部分是传入 _.bind 的参数(会被优先调用),另外一部分是传入 bound(_.bind 所返回方法)的参数
      return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
    };
    return bound;
  };

该方法是ES5 bind 方法的扩展, 将 func 中的 this 指向 context(对象),用法为_.bind(function, object, *arguments),其中arguments 参数可选,它会被看成 func 的参数传入,func 在调用时,会优先用 arguments 参数,而后使用 _.bind 返回方法所传入的参数。

六、 _.memoize

_.memoize = function(func, hasher) {
    var memoize = function(key) { // 储存变量,方便使用
      var cache = memoize.cache;
      //求 key
      //若是传入了 hasher,则用 hasher 函数来计算 key,不然用 参数 key(即 memoize 方法传入的第一个参数)当 key
      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
      //若是这个 key 还没被 hash 过(还没求过值)
      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
      return cache[address];
    };
    memoize.cache = {}; // cache 对象被当作 key-value 键值对缓存中间运算结果
    return memoize; // 返回一个函数(经典闭包)
  };

Memoizes方法能够缓存某函数的计算结果,用法:_.memoize(function, [hashFunction])
若是传递了 hashFunction 参数,就用 hashFunction 的返回值做为key存储函数的计算结果。hashFunction 默认使用function的第一个参数做为key。memoized值的缓存可做为返回函数的cache属性。

七、_.throttle

_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;  //标记时间戳,上一次执行回调的时间戳
    var previous = 0;
    if (!options)  //若是没有传入 options 参数
        options = {};  // 则将 options 参数置为空对象
    var later = function() {
    //若是 options.leading === false,则每次触发回调后将 previous 置为 0,不然置为当前时间戳
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    // _.throttle 方法返回的函数
    return function() {
      var now = _.now();  // 记录当前时间戳
      //第一次执行回调(此时 previous 为 0,以后 previous 值为上一次时间戳)
      //而且若是程序设定第一个回调不是当即执行的(options.leading === false),则将 previous 值(表示上次执行的时间戳)设为 now 的时间戳(第一次触发时),表示刚执行过,此次就不用执行了
      if (!previous && options.leading === false) previous = now;

       // 距离下次触发 func 还须要等待的时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout); // 解除引用,防止内存泄露
          timeout = null;
        }
        // 重置前一次触发的时间戳
        previous = now;
        // 触发方法,result 为该方法返回值
        result = func.apply(context, args);

        if (!timeout) 
            context = args = null; //引用置为空,防止内存泄露
      } else if (!timeout && options.trailing !== false) {// 最后一次须要触发的状况
        //若是已经存在一个定时器,则不会进入该 if 分支
        // 若是 {trailing: false},即最后一次不须要触发了,也不会进入这个分支
        timeout = setTimeout(later, remaining); // 间隔 remaining milliseconds 后触发 later 方法
      }
      return result;  // 回调返回值
    };
  };

函数节流(若是有连续事件响应,则每间隔必定时间段触发),每间隔 wait(Number) milliseconds 触发一次 func 方法,若是 options 参数传入 {leading: false},不会立刻触发(等待 wait milliseconds 后第一次触发 func),若是 options 参数传入 {trailing: false},那么最后一次回调不会被触发。options 不能同时设置 leading 和 trailing 为 false。

八、_.debounce

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

    var later = function() {
        //定时器设置的回调 later 方法的触发时间,和连续事件触发的最后一次时间戳的间隔
      var last = _.now() - timestamp;  //若是间隔为 wait(或者恰好大于 wait),则触发事件

      if (last < wait && last >= 0) { //时间间隔 last 在 [0, wait) 中,还没到触发的点,则继续设置定时器
        timeout = setTimeout(later, wait - last);
      } else {  //到了能够触发的时间点
        timeout = null;
         // 若是不是当即执行,随即执行 func 方法
        if (!immediate) {
          result = func.apply(context, args); // 执行 func 函数
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      //每次触发函数,更新时间戳
      timestamp = _.now(); 
      // 当即触发须要知足两个条件
      // immediate 参数为 true,而且 timeout 还没设置
      var callNow = immediate && !timeout; // 设置 wait seconds 后触发 later 方法
      // 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
      if (!timeout) // 设置了 timeout,因此之后不会进入这个 if 分支了
          timeout = setTimeout(later, wait);
      // 若是是当即触发
      if (callNow) {
        result = func.apply(context, args);
        context = args = null; // 解除引用
      }

      return result;
    };
  };

函数去抖(连续事件触发结束后只触发一次),如_.debounce(function(){}, 1000)表示连续事件结束后的 1000ms 后触发。

九、 _.pick

_.pick = function(object, oiteratee, context) {
    var result = {}, // result 为返回的对象副本
    obj = object, iteratee, keys;
    if (obj == null) return result;
    // 若是第二个参数是函数
    if (_.isFunction(oiteratee)) {
      keys = _.allKeys(obj);
      iteratee = optimizeCb(oiteratee, context);
    } else {
    // 若是第二个参数不是函数
    // 则后面的 keys 多是数组
    // 也多是连续的几个并列的参数
    // 用 flatten 将它们展开
      keys = flatten(arguments, false, false, 1);
      //也转为 predicate 函数判断形式,将指定 key 转化为 predicate 函数
      iteratee = function(value, key, obj) { return key in obj; };
      obj = Object(obj);
    }
    for (var i = 0, length = keys.length; i < length; i++) {
      var key = keys[i];
      var value = obj[key];
      if (iteratee(value, key, obj)) result[key] = value;
    }
    return result;
  };

_.pick(object, *keys)根据必定的需求(key 值,或者经过 predicate 函数返回真假),返回拥有必定键值对的对象副本。第二个参数能够是一个 predicate 函数,也能够是0个或多个key。

十、_.noConflict

_.noConflict = function() {
    root._ = previousUnderscore;
    return this;
  };

若是全局环境中已经使用了 _ 变量,能够用该方法返回其余变量,继续使用 underscore 中的方法。

十一、_.template

_.template = function(text, settings, oldSettings) {
      // 兼容旧版本
    if (!settings && oldSettings) settings = oldSettings;
    settings = _.defaults({}, settings, _.templateSettings);

    // Combine delimiters into one regular expression via alternation.
    // // 正则表达式 pattern,用于正则匹配 text 字符串中的模板字符串
    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

    // Compile the template source, escaping string literals appropriately.
    // 编译模板字符串,将原始的模板字符串替换成函数字符串
    // 用拼接成的函数字符串生成函数(new Function(...))
    var index = 0;
    // source 变量拼接的字符串用来生成函数,用于当作 new Function 生成函数时的函数字符串变量
    var source = "__p+='";
    // replace 函数不须要为返回值赋值,主要是为了在函数内对 source 变量赋值
    // // 将 text 变量中的模板提取出来
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
        // escape/interpolate/evaluate 为匹配的子表达式(若是没有匹配成功则为 undefined)
       // offset 为字符匹配(match)的起始位置(偏移量)
      source += text.slice(index, offset).replace(escaper, escapeChar);

      index = offset + match.length;   // 改变 index 值,为了下次的 slice

      if (escape) {
          // 须要对变量进行编码(=> HTML 实体编码)
          // 避免 XSS 攻击
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      } else if (interpolate) {  // 单纯的插入变量
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) {
        source += "';\n" + evaluate + "\n__p+='";
      }

      // Adobe VMs need the match returned to produce the correct offest.
      // return 的做用是将匹配到的内容原样返回(Adobe VMs 须要返回 match 来使得 offset 值正常)
      return match;
    });
    source += "';\n";

    // If a variable is not specified, place data values in local scope.
    // 若是设置了 settings.variable,能显著提高模板的渲染速度,不然,默认用 with 语句指定做用域
    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

    // 增长 print 功能
    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + 'return __p;\n';   // __p 为返回的字符串

    try {
        // render 方法,前两个参数为 render 方法的参数
        // obj 为传入的 JSON 对象,传入 _ 参数使得函数内部能用 Underscore 的函数
      var render = new Function(settings.variable || 'obj', '_', source);
    } catch (e) {
      e.source = source;
      throw e;
    }

    // 返回的函数
    //data 通常是 JSON 数据,用来渲染模板
    var template = function(data) {
        // render 为模板渲染函数
      return render.call(this, data, _);  // 传入参数 _ ,使得模板里 <%  %> 里的代码能用 underscore 的方法
    };

    // Provide the compiled source as a convenience for precompilation.
    var argument = settings.variable || 'obj';
    template.source = 'function(' + argument + '){\n' + source + '}';

    return template;
  };
_.template(templateString, [settings])  // 将 JavaScript 模板编译为能够用于页面呈现的函数,setting 参数能够用来自定义字符串模板,是一个哈希表包含任何能够覆盖的设置。
相关文章
相关标签/搜索