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

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

  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 两个函数,主要针对的是为了将函数调用模式更改为构造器调用和方法调用。

  _.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 函数这个需要仔细讲一下了,先化简:

    _.bind = function(func, context, args) {        var length = arguments.length - 2;        args = Array(length);        for (var index = 0; index 这样看上去是不是直白很多,官网给它的定义是:绑定函数 function 到对象 object 上, 也就是无论何时调用函数, 函数里的 this 都指向这个 object.任意可选参数 arguments 可以传递给函数 function , 可以填充函数所需要的参数,这也被称为 partial application。对于没有结合上下文的partial application绑定,请使用partial。,怎么听怎么别扭,我们可以这样理解:_.bind 函数是为其传参中的 function 的 this 上绑定相应对象属性,并且同时进行 function 的参数传入,而其中最关键的就是在执行这一系列动作的同时将传入参数 context 绑定到了指向它的 Function 对象本身的 this 身上(可参考函数调用模式与方法调用模式的区别)。官网有个栗子:

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"]

分别代表四种情况:1. 函数调用模式:指向 Global,浏览器客户端即 window;1. 方法调用模式:指向对象本身;1. 构造器调用模式:指向为新构造的对象,继承自原 Function 对象;1. apply 或 call 调用模式:指向传入的参数。这里还有一些非常好的资料:this、Understanding 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
所以
.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 _.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
.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  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 进行判断然后分别执行相应的策略。

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

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

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

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

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

  1. {leading:true,trailing:true}:从实例化 .throttle 的时间开始到执行实例化的函数的时间为止,中间的差值定义为 now - previous,进而得出设定的时间 wait 与 now - previous 的差值 remaining,从而决定怎么执行函数。参考 世纪之光 的很有趣的说法,就是第一次可以立即执行,第二次开始将在每 wait 时间内只允许执行一次,为什么会第一次立即执行呢,因为大家设置的 wait 一般都不会太大,所以页面加载过程中一般已经执行了 .throttle 的实例化,也就是说其 remaining ,而后面如果一直执行函数,那么就开始 0 模式了,

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

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

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

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

  1. remaining :立即执行 _.throttle 函数的回调函数 func。

  2. 0 :通过 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 _.after 接受两个参数,Number 参数用来限定 _.after 实例化函数的执行次数,说白了就是只有当第 Number 次执行实例化函数的时候才会继续执行 func 回调,这个用来处理遍历 _.each 时某些情况很有用。

.before = function(times, func) {
var memo;
return function() {
if (--times > 0) {
memo = func.apply(this, arguments);
}
if (times
.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 函数绑定到 对象上。

关键字:JavaScript, web, WEB前端开发


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部