1625行,解开 underscore.js 的面纱 - 第六章

北京的雨已经断断续续下了很久,昏昏欲睡的躲在家里不肯意出门,火影忍者快要结束了,一拳超人第二季听说还要等好多年,勇者大冒险貌似断更了,我又是在不喜欢海贼王的画风,因此,我该看什么好呢。javascript

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
    var self = baseCreate(sourceFunc.prototype);
    var result = sourceFunc.apply(self, args);
    if (_.isObject(result)) return result;
    return self;
  };

executeBound 用来构成 _.bind_.partial 两个函数,主要针对的是为了将函数调用模式更改成构造器调用和方法调用。html

_.bind = restArgs(function(func, context, args) {
    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
    var bound = restArgs(function(callArgs) {
      return executeBound(func, bound, context, this, args.concat(callArgs));
    });
    return bound;
  });

也许咱们能够参考下 Function.prototype.bind()_.bind 函数这个须要仔细讲一下了,先化简:java

_.bind = function(func, context, args) {
        var length = arguments.length - 2;
        args = Array(length);
        for (var index = 0; index < length; index++) {
            args[index] = arguments[index + startIndex];
        }
        if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
        var bound = function(args_2){
            args_2 = Array(arguments.length);
            for (var index = 0; index < arguments.length; index++) {
                args_2[index] = arguments[index];
            }
            (function(sourceFunc, boundFunc, context, callingContext, args) {
                if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
                var self = baseCreate(sourceFunc.prototype);
                var result = sourceFunc.apply(self, args);
                if (_.isObject(result)) return result;
                return self;
          })(func, bound, context, this, args.concat(args_2));
        };
        return bound;
    };

这样看上去是否是直白不少,官网给它的定义是:绑定函数 function 到对象 object 上, 也就是不管什么时候调用函数, 函数里的 this 都指向这个 object.任意可选参数 arguments 能够传递给函数 function , 能够填充函数所须要的参数,这也被称为 partial application。对于没有结合上下文的partial application绑定,请使用partial。,怎么听怎么别扭,咱们能够这样理解:_.bind 函数是为其传参中的 function 的 this 上绑定相应对象属性,而且同时进行 function 的参数传入,而其中最关键的就是在执行这一系列动做的同时将传入参数 context 绑定到了指向它的 Function 对象自己的 this 身上(可参考函数调用模式与方法调用模式的区别)。官网有个栗子:chrome

var func = function(greeting){ return greeting + ': ' + this.name };
   func = _.bind(func, {name: 'moe'}, 'hi');
   func();
   {'hi: moe'}

实际上呢它等同于:数组

var func = _.bind(function(greeting){
           return greeting + ': ' + this.name;
       },
       {name: 'moe'},
       'hi'
   );
   func();
   {'hi: moe'}

结合前面简化的 _.bind 代码示例可知这个函数的核心思想就是先经过 _.bind 初始化的时候优化第3+个参数 args,为何叫 3+ 呢,由于从第三个参数开始,多是不限定的参数数量,因此从第三个开始到最后一个参数同一处理为一个数组 args。
紧接着就是执行刚才初始化事后的函数了,当 func(); 的时候也就是开始执行 _.bind 中的 bound 函数。bound 容许传递参数而且其参数会被 push 到 args 中,具体实现参看上面的简化代码 args.concat(args_2)。这里咱们有几个须要注意的点,其一是 callingContext instanceof boundFunc,以前咱们讲过 instanceof 的神奇用法,在这里它用与判断 bound 中的 this 的指向是否继承于 bound。咱们必定知道 this 指向的四个状况,以下:浏览器

var obj = {};
var func = function (){console.log(this);};
func();
new func();
obj.func = func;
obj.func();
func.apply(['this is parameter']);
func.call(['this is parameter']);

输出结果为:缓存

Window {external: Object, chrome: Object, document: document, alogObjectConfig: Object, alogObjectName: "alog"…}
func {}
Object {}
["this is parameter"]
["this is parameter"]

分别表明四种状况:app

  • 函数调用模式:指向 Global,浏览器客户端即 window;函数

  • 方法调用模式:指向对象自己;优化

  • 构造器调用模式:指向为新构造的对象,继承自原 Function 对象;

  • apply 或 call 调用模式:指向传入的参数。

这里还有一些很是好的资料:thisUnderstanding JavaScript Function Invocation and "this",在这里我要说一下我在推库上看到一篇关于 this 的介绍文章说:“比较系统的分类是《JavaScript语言精粹》中的,分为函数调用模式(this绑定全局对象window)和方法调用模式(this绑定调用方法的主体)”,我把《JavaScript语言精粹》这本书从头至尾翻看了好几遍,实际上它原文是这样说的:“在 JAVASCRIPT 中一共有4种调用模式:方法调用模式、函数调用模式、构造器调用模式和 apply 调用模式。”,具体叙述在原书的P27~P30页,感兴趣的朋友能够看下,在给你们看一个彩蛋,严格模式下的 this。紧接上文,当 bound 中的 this 的指向是否继承于 bound 函数的时候说明是使用了 new 关键字的构造器调用模式调用了 _.bind 函数,则继续执行 executeBound 函数中的 baseCreate 建立基本函数而后进行一系列的操做,其实说到底 baseCreate 的目的就是为了保证传入参数 Function 的 this 的干净。
另一个须要注意的地方是官网示例的暗示(特蛋疼的暗示),我扩展了一下:

var func = function(){ return JSON.stringify(arguments) + ': ' + this.name };
   func = _.bind(func, {name: 'moe'}, 'hi');
   func();
   func = _.bind(func, {name: 'moe2'}, 'hi2');
   func();

输出结果:

"{"0":"hi"}: moe"
   "{"0":"hi","1":"hi2"}: moe"

可能有些不明就里的同窗会问这是为何啊,怎么 this.name 的值没有变化呢。实际上咱们第一个 _.bind 是正常的函数绑定,而第二个 func = _.bind(func, {name: 'moe2'}, 'hi2'); 是将上一个 _.bind 做为了 Function 参数传入到了新的 _.bind 中,而原本的函数 func 做为第一个 _.bind 的 func 参数一直传递到第二个 _.bind 中,可是中间的 this.name 却被绑定到了第一个 _.bind 上面而不是第一个 _.bind 中的 func 上。有一点绕口。用个代码介绍下,第二个 _.bind 的状况是这样子的:

func = _.bind(function(
       function(greeting){
           return greeting + ': ' + this.name;
      },
       context,
       args
   ) {
        var length = arguments.length - 2;
        args = Array(length);
        for (var index = 0; index < length; index++) {
            args[index] = arguments[index + startIndex];
        }
        if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
        var bound = function(args_2){
            args_2 = Array(arguments.length);
            for (var index = 0; index < arguments.length; index++) {
                args_2[index] = arguments[index];
            }
            (function(sourceFunc, boundFunc, context, callingContext, args) {
                if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
                var self = baseCreate(sourceFunc.prototype);
                var result = sourceFunc.apply(self, args);
                if (_.isObject(result)) return result;
                return self;
          })(func, bound, context, this, args.concat(args_2));
        };
        return bound;
    },
       {name: 'moe2'},
       'hi2'
   );

因此 _.bind 必定要遵循正确的用法,否则真的出错了可能调试都很差发现问题,多层回调嵌套的时候一层套一层,很麻烦。

_.partial = restArgs(function(func, boundArgs) {
    var placeholder = _.partial.placeholder;
    var bound = function() {
      var position = 0, length = boundArgs.length;
      var args = Array(length);
      for (var i = 0; i < length; i++) {
        args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
      }
      while (position < arguments.length) args.push(arguments[position++]);
      return executeBound(func, bound, this, this, args);
    };
    return bound;
  });

_.partial 函数的核心思想与 _.bind 相同,都是为了解决 this 指向的问题,区别在于 _.partial 不须要对 this 上的值作什么处理。用法上我以为 _.partial 看上去更怪异一些,也许用来作一些特定的计算可能更合适些。

_.partial.placeholder = _;

设置 _.partial.placeholder_

_.bindAll = restArgs(function(obj, keys) {
    keys = flatten(keys, false, false);
    var index = keys.length;
    if (index < 1) throw new Error('bindAll must be passed function names');
    while (index--) {
      var key = keys[index];
      obj[key] = _.bind(obj[key], obj);
    }
  });

这里咱们看到 _.bindAll 函数官网的示例就有点糊涂了:

var buttonView = {
     label  : 'underscore',
     onClick: function(){ console.log('clicked: ' + this.label); },
     onHover: function(){ console.log('hovering: ' + this.label); }
   };
   _.bindAll(buttonView, 'onClick', 'onHover');
   buttonView.onClick();
   clicked: underscore

咱们固然知道结果是 clicked: underscore,那么执行 _.bindAll(buttonView, 'onClick', 'onHover'); 的意义在哪呢,因此说这又是官网坑人的地方了,_.bindAll 的本意是将其传入的第二个及之后的参数放到一个共同的上下文环境里面执行,从而达到 this 指向其第一个参数的自己的目的,而官网的示例为方法调用模式,this 指向已是 Object 自己了因此看不到变化,可是咱们在浏览器控制台查看的话应该能知道 this 上多了 [[TargetFunction]]: function ()[[BoundThis]]: Object[[BoundArgs]]: Array[0] 三个参数而且 [[BoundThis]] 刚好是 Object。闲来无事这好看到有人也写了这个问题并举证了一个示例,详见 Understanding bind and bindAll in Backbone.js。我 cope 一下:

function Developer(skill) {
     this.skill = skill;
     this.says = function(){
       console.log(this.skill + ' rocks!');
     }
   }
   var john = new Developer('Ruby');
   _.bindAll(john, 'says');
   var func = john.says;
   func(); //Ruby rocks!

这个函数调用模式的示例正好答疑了 this 指向已经被改变的这个问题。

_.memoize = function(func, hasher) {
    var memoize = function(key) {
      var cache = memoize.cache;
      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
      return cache[address];
    };
    memoize.cache = {};
    return memoize;
  };

_.memoize 函数更像是一个能够缓存第一次执行结果的递归函数,咱们从源码中能够看到 memoize.cache = {}; 就是用来存储计算结果的容器,这里面比较有意思的是 hasher 这个参数,官网释义: hashFunction,实际上就是经过 hashFunction 对传入的 key 值进行处理而后放到 memoize.cache = {}; 中,至于怎么处理 hash 也好、md5 也好、或者什么其余的计算加密真值判断增长对象等等均可以经过 hasher 这个传入的回调进行扩展。

————————— 疲惫的分割线 ———————————
这几天北京总在下雨,身体特别的疲惫,状态也不怎么好,因此今天才开始继续更新。
————————— END ———————————

_.delay = restArgs(function(func, wait, args) {
    return setTimeout(function() {
      return func.apply(null, args);
    }, wait);
  });

_.delay 函数用于处理定时器相关函数,原理是经过 setTimeout 进行二次封装,比较关键的就是 args 参数经过 restArgs 函数处理为一个数组,方便了下一步的 func.apply(null, args); 传值。

_.defer = _.partial(_.delay, _, 1);

_.defer 这个函数咱们首先能够看到内部应用了 _.partial 而且中间传入参数 _,这意味着当 _.defer 执行的时候传入的参数会被补全到 _.partial 内部 bound 中的 args[0] 位置,而此时 args 的值为 [func, 1]并将它传给 _.delay 函数,即 _.delay.apply(null, args);,用着这种方式曲线的设置 setTimeout 函数的 wait = 1,目的就是处理代码复用问题,否则的话彻底能够改装一下 _.delay 函数能够更简单的实现这一功能。

_.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;
      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) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };
    return throttled;
  };

_.throttle 函数能够限制和控制其参数 func 的执行次数和执行时间,思想就是经过 wait、now、previous 和 remaining 进行判断而后分别执行相应的策略。

  • wait:使用 _.throttle 函数时传入的时间标识,在每一个 wait 毫秒时间段内最多且必定调用一次该函数。

  • now:使用 _.now() 函数获取当前时间戳。

  • previous:用来缓存函数执行时的时间戳,用于后面与下一次执行时的时间戳进行相关判断。

  • remaining:缓存 wait - (now - previous) 的差值。

咱们在看官网介绍能够知道 _.throttle 传递的 options 分四种状况(默认是 {leading:false,trailing:false}):

  • {leading:true,trailing:true}:从实例化 _.throttle 的时间开始到执行实例化的函数的时间为止,中间的差值定义为 now - previous,进而得出设定的时间 wait 与 now - previous 的差值 remaining,从而决定怎么执行函数。参考 世纪之光 的颇有趣的说法,就是第一次能够当即执行,第二次开始将在每 wait 时间内只容许执行一次,为何会第一次当即执行呢,由于你们设置的 wait 通常都不会太大,因此页面加载过程当中通常已经执行了 _.throttle 的实例化,也就是说其 remaining <= 0,然后面若是一直执行函数,那么就开始 0 < remaining <= wait 模式了,

  • {leading:false,trailing:false}:这种状况下比较有意思的是 previous 这个参数,在实例化 _.throttle 的时候,previous = 0,利用了 !0 === true 的特性使 _.throttle 内部并无执行回调函数 func,因此第一次函数调用失败,在第二次开始 previous = now (now 为第一次调用的时间戳),因此它也分为两种状况:

  • {leading:true,trailing:false}:这种状况下是没有 setTimeout 函数的,由于 leading:true,因此 previous 初始化为 0,意味着第一次执行函数会当即执行,儿后面就要遵循 remaining <= 0 || remaining > wait 才能执行,也就是说只有第一执行完毕后的时间超过了 wait 才能继续调用函数才能执行(调用是重点),以此类推。

  • {leading:false,trailing:true}:这种状况因为 leading:false,因此每次 previous 都等于当前调用函数时的时间戳,因此完美的不存在 remaining <= 0 || remaining > wait 的状况,由此只能经过 setTimeout 执行回调,因此遵循经过 setTimeout 函数设定时间为 remaining 毫秒后执行 _.throttle 函数的回调函数 func,用以达到在规定时间 wait 毫秒时执行函数的目的,而且规定 wait 时间内只执行一次函数。

其实总结一下就是大概一下两种都存在或者只存在其一的状况:

  • remaining <= 0:当即执行 _.throttle 函数的回调函数 func。

  • 0 < remaining <= wait:经过 setTimeout 函数设定时间为 remaining 毫秒后执行 _.throttle 函数的回调函数 func,用以达到在规定时间 wait 毫秒时执行函数的目的,而且规定 wait 时间内只执行一次函数。

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

_.debounce 更像是 _.delay 的方言版,当 immediate = true 的时候经过 var callNow = !timeout = false 达到当即执行回调函数 func 的目的,并用 later 函数限制 规定 wait 时间内不容许在调用函数(later 函数内部 context = args = underfind,其实咱们知道 var later = function(context, args) 这个条件是为 _.delay(later, wait, this, args) 准备的)。

_.wrap = function(func, wrapper) {
    return _.partial(wrapper, func);
  };

_.wrap 的两个参数理论上都要求是 Function,咱们已经知道 _.partial 是用来在 this 上下功夫的,虽然这里和 this 也没什么太大关系,之因此这里应用了 _.partial 是为了让 func 做为 wrapper 的第一个参数执行,而且经过 executeBound 函数对函数调用模式方法调用模式作处理。

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

_.negate 用来作真值判断。

_.compose = function() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
      var i = start;
      var result = args[start].apply(this, arguments);
      while (i--) result = args[i].call(this, result);
      return result;
    };
  };

_.compose 用于将函数执行结果进行传递,须要注意的是 var args = arguments; 中的 arguments 和 args[start].apply(this, arguments); 中的 arguments 并不相同就能够了。这个涉及到函数的执行,当每个函数执行的时候都会造成一个内部的上下文执行环境(传说叫 ExecutionContext,这个我尚未考证过),在构建环境的同时生成 arguments 变量和做用域链表等等,这里不像叙述了。

_.after = function(times, func) {
    return function() {
      if (--times < 1) {
        return func.apply(this, arguments);
      }
    };
  };

_.after 接受两个参数,Number 参数用来限定 _.after 实例化函数的执行次数,说白了就是只有当第 Number 次执行实例化函数的时候才会继续执行 func 回调,这个用来处理遍历 _.each 时某些状况颇有用。

_.before = function(times, func) {
    var memo;
    return function() {
      if (--times > 0) {
        memo = func.apply(this, arguments);
      }
      if (times <= 1) func = null;
      return memo;
    };
  };

_.before,与 _.after 相反,只在规定 Number 参数的次数内以此执行 _.before,超过以后结束。

_.once = _.partial(_.before, 2);

_.once 建立一个只能调用一次的函数。到这里关于函数相关的源码就结束了,说内心话不少地方看得懂不必定说的懂,说的懂也不必定用的懂,就拿这个 _.once 来说,它只用了 _.partial_.before 来作文章,用 _.before 限定只能执行一次还好理解,那么为何必定要用 _.partial 坐下处理呢,其目的真的只是为了让 2 做为 _.before 的第一个参数进行传递过去并将 _.once 的传参做为 arguments[1+] 传入么,更深一层考虑,_.partial 函数是否是有处理过 _.once 传递过来的函数的做用域链和 this 相关的状况呢。

_.restArgs = restArgs;

_.restArgs 将 restArgs 函数绑定到 _ 对象上。

相关文章
相关标签/搜索