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 函數綁定到 _ 對象上。

相關文章
相關標籤/搜索