跟underscore一塊兒學如何寫函數庫

原文:zhehuaxuan.github.io/2019/03/07/…
做者:zhehuaxuanjavascript

目的

Underscore 是一個 JavaScript 工具庫,它提供了一整套函數式編程的實用功能,可是沒有擴展任何 JavaScript 內置對象。前端

本文主要梳理underscore內部的函數組織與調用邏輯的方式和思想。java

經過這篇文章,咱們能夠:node

瞭解underscore在函數組織方面的巧妙構思;git

爲本身書寫函數庫提供必定思路;github

咱們開始!編程

本身寫個函數庫

前端的小夥伴必定不會對jQuery陌生,常用$.xxxx的形式進行調用,underscore使用_.xxxx,若是本身在ES5語法中寫過自定義模塊的話,就能夠寫出下面一段代碼:數組

//IIFE函數
(function(){
    //獲取全局對象
    var root = this;
    //定義對象
    var _ = {};
    //定義和實現函數
    _.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    //綁定在全局變量上面
    root._ = _;
})();
console.log(this);
複製代碼

在Chrome瀏覽器中打開以後,打印出以下結果:瀏覽器

image-20190305235718888

咱們看到在全局對象下有一個_屬性,屬性下面掛載了自定義函數。
咱們不妨使用_.first(xxxx)在全局環境下直接調用。bash

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
複製代碼

輸出結果以下:

image-20190306000334727

沒問題,咱們的函數庫製做完成了,咱們通常直接這麼用,也不會有太大問題。

underscore是怎麼作的?

underscore正是基於上述代碼進行完善,那麼underscore是如何接着往下作的呢?容我娓娓道來!

對兼容性的考慮

首先是對兼容性的考慮,工具庫固然須要考慮各類運行環境。

// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
              typeof global == 'object' && global.global === global && global ||
              this ||
              {};
複製代碼

上面是underscore1.9.1在IIFE函數中的源碼,對應於咱們上面本身寫的var root = this;

在源碼中做者也做了解釋:
建立root對象,而且給root賦值。怎麼賦值呢?

瀏覽器端:window也能夠是window.self或者直接self

服務端(node):global

WebWorker:self

虛擬機:this

underscore充分考慮了兼容性,使得root指向對局對象。

支持兩種不一樣風格的函數調用

在underscore中咱們可使用如下兩種方式調用:

  1. 函數式的調用:console.log(_.first([1,2,3,4]));
  2. 對象式調用:console.log(_([1,2,3,4])).first();

在underscore中,它們返回的結果都是相同的。

第一種方式咱們如今就沒有問題,難點就是第二種方式的實現。

對象式調用的實現

解決這個問題要達到兩個目的:

  1. _是一個函數,而且調用返回一個對象;
  2. 這個對象依然可以調用掛載在_對象上聲明的方法。

咱們來看看underscore對於_的實現:

var _ = function(obj) {
      if (obj instanceof _) return obj;
      if (!(this instanceof _)) return new _(obj);
      this._wrapped = obj;
};
複製代碼

相關圖片

不怕,咱們不妨調用_([1,2,3,4]))看看他是怎麼執行的!

第一步if (obj instanceof _) return obj;傳入的對象及其原型鏈上有_類型的對象,則返回自身。咱們這裏的[1,2,3,4]顯然不是,跳過。

第二步if (!(this instanceof _)) return new _(obj);,若是當前的this對象及其原型鏈上沒有_類型的對象,那麼執行new操做。調用_([1,2,3,4]))時,thiswindow,那麼(this instanceof _)false,因此咱們執行new _([1,2,3,4])

第三步:執行new _([1,2,3,4]),繼續調用_函數,這時

obj[1,2,3,4]

this爲一個新對象,而且這個對象的__proto__指向_.prototype(對於new對象執行有疑問,請猛戳此處)

此時

(obj instanceof _)爲false

(this instanceof _)爲true

因此此處會執行this._wrapped = obj;,在新對象中,添加_wrapped屬性,將[1,2,3,4]掛載進去。

綜合上述函數實現的效果就是:

_([1,2,3,4]))<=====>new _([1,2,3,4])

而後執行以下構造函數:

var _ = function(obj){
    this._wrapped = obj
}
複製代碼

最後獲得的對象爲:

image-20190306201849178

image-20190306235445836

咱們執行以下代碼:

console.log(_([1,2,3,4]));
console.log(_.prototype);
console.log(_([1,2,3,4]).__proto__ == _.prototype);
複製代碼

看一下打印的信息:

image-20190306214133549

這代表經過_(obj)構建出來的對象確實具備兩個特徵:

  1. 下面掛載了咱們傳入的對象/數組
  2. 對象的_proto_屬性指向_prototype

到此咱們已經完成了第一個問題。

「我正是个天才 表æƒ
åŒ
」的圖片搜尋結果

接着解決第二個問題:

這個對象依然可以調用掛載在_對象上聲明的方法

咱們先來執行以下代碼:

_([1,2,3,4]).first();
複製代碼

此時JavaScript執行器會先去找_([1,2,3,4])返回的對象上是否有first屬性,若是沒有就會順着對象的原型鏈上去找first屬性,直到找到並執行它。

咱們發現_([1,2,3,4])返回的對象屬性和原型鏈上都沒有first

image-20190307000429320

那咱們本身先在_.prototype上面加一個first屬性上去試試:

(function(){
    //定義
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};
    
    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };

    _.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    _.prototype.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    root._ = _;
})();
複製代碼

咱們在執行打印一下:

console.log(_([1,2,3,4]));
複製代碼

效果以下:

image-20190306214554433

原型鏈上找到了first函數,咱們能夠調用first函數了。以下:

console.log(_([1,2,3,4]).first());
複製代碼

惋惜報錯了:

image-20190306214848922

因而調試一下:

image-20190306214932983

咱們發現arrundefined,可是咱們但願arr[1,2,3,4]

「不æ
Œ 表æƒ
åŒ
」的圖片搜尋結果

咱們立刻改一下_.prototype.first的實現

(function(){
    
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };

    _.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    _.prototype.first = function(arr,n=0){
        arr = this._wrapped;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    root._ = _;
})();
複製代碼

咱們在執行一下代碼:

console.log(_([1,2,3,4]).first());
複製代碼

效果以下:

image-20190306215555025

咱們的效果彷佛已經達到了!

「赞 表æƒ
åŒ
」的圖片搜尋結果

如今咱們執行下面的代碼:

console.log(_([1,2,3,4]).first(2));
複製代碼

調試一下:

image-20190306215729756

涼涼了。

「凉凉 表æƒ
åŒ
」的圖片搜尋結果

其實咱們但願的是:

[1,2,3,4]2arguments的形式傳入first函數

咱們再來改一下:

//_.prototype.first = function(arr,n=0){
        // arr = this._wrapped;
        // if(n==0) return arr[0];
        // return arr.slice(0,n);
    //}
    _.prototype.first=function(){
        /** * 蒐集待傳入的參數 */
        var that = this._wrapped;
        var args = [that].concat(Array.from(arguments));
        console.log(args); 
    }
複製代碼

咱們再執行下面代碼:

_([1,2,3,4]).first(2);
複製代碼

看一下打印的效果:

image-20190306220704637

參數都已經拿到了。

咱們調用函數一下first函數,咱們繼續改代碼:

_.prototype.first=function(){
     /** * 蒐集待傳入的參數 */
     var that = this._wrapped;
     var args = [that].concat(Array.from(arguments));
     /** * 調用在_屬性上的first函數 */
     return _.first(...args);
}
複製代碼

這樣一來_.prototype上面的函數的實現都省掉了,至關於作一層代理;並且咱們不用再維護兩套代碼,一旦修改實現,兩邊都要改。

一箭雙鵰!

執行一下最初咱們的代碼:

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
複製代碼

image-20190306221231484

如今好像咱們全部的問題都解決了。

「赞 表æƒ
åŒ
」的圖片搜尋結果

可是彷佛仍是怪怪的。
咱們每聲明一個函數都得在原型鏈上也聲明一個同名函數。形以下面:

_.a = function(args){
    //a的實現
}
_.prototype.a = function(){
    //調用_.a(args)
}
_.b = function(args){
    //b的實現
}
_.prototype.b = function(){
    //調用_.b(args)
}
_.c = function(args){
    //c的實現
}
_.prototype.c = function(){
    //調用_.c(args)
}
.
.
.
1000個函數以後...
複製代碼

會不會以爲太恐怖了!

「害怕 表æƒ
åŒ
」的圖片搜尋結果

咱們能不能改爲以下這樣呢?

_.a = function(args){
    //a的實現
}
_.b = function(args){
    //b的實現
}
_.c = function(args){
    //c的實現
}
1000個函數以後...
_.mixin = function(){
    //將_屬性中聲明的函數都掛載在_prototype上面
}
_.mixin(_);
複製代碼

上面這麼作好處大大的:

咱們能夠專一於函數庫的實現,不用機械式的複寫prototype上的函數

underscore也正是這麼作的!

咱們看看mixin函數在underscore中的源碼實現:

// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
   _.each(_.functions(obj), function(name) {
   var func = _[name] = obj[name];
      _.prototype[name] = function() {
          var args = [this._wrapped];
          push.apply(args, arguments);
          return chainResult(this, func.apply(_, args));
      };
   });
    return _;
};
  
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
複製代碼

有了上面的鋪墊,這個代碼一點都不難看懂,首先調用_.each函數,形式以下:

_.each(arrs, function(item) {
     //遍歷arrs數組中的每個元素
 }
複製代碼

咱們一想就明白,咱們在_對象屬性上實現了自定義函數,那麼如今要把它們掛載到—_.prototype屬性上面,固然先要遍歷它們了。

咱們能夠猜到_.functions(obj)確定返回的是一個數組,並且這個數組確定是存儲_對象屬性上面關於咱們實現的各個函數的信息。

咱們看一下_.function(obj)的實現:

_.functions = _.methods = function(obj) {
  var names = [];
  /** ** 遍歷對象中的屬性 **/
  for (var key in obj) {
      //若是屬性值是函數,那麼存入names數組中
     if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
};
複製代碼

確實是這樣的!

「拉菲 表æƒ
åŒ
」的圖片搜尋結果

咱們把上述實現的代碼整合起來:

(function(){
    /** * 保證兼容性 */
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

    /** * 在調用_(obj)時,讓其執行new _(obj),並將obj掛載在_wrapped屬性之下 */
    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };
    
    //本身實現的first函數
    _.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }

    //判斷是不是函數
    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

    //遍歷生成數組存儲_對象的函數值屬性
    _.functions = _.methods = function(obj) {
        var names = [];
        for (var key in obj) {
          if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
      };
  
    //本身實現的遍歷數組的函數
    _.each = function(arrs,callback){
        for(let i=0;i<arrs.length;i++){
            callback(arrs[i]);
        }
    }
    
    var ArrayProto = Array.prototype;
    var push = ArrayProto.push;
 
    //underscore實現的mixin函數
    _.mixin = function(obj) {
        console.log(_.functions(obj)); //咱們打印一下_.functions(_)到底存儲了什麼?
        _.each(_.functions(obj), function(name) {
          var func = _[name] = obj[name];
           _.prototype[name] = function() {
               var args = [this._wrapped];
               push.apply(args, arguments);
               return func.apply(_, args);
           };
        });
         return _;
    };

    //執行minxin函數
    _.mixin(_);
    root._ = _;
})();
複製代碼

咱們看一下_.functions(obj)返回的打印信息:

image-20190306224747300

確實是_中自定義函數的屬性值。

咱們再來分析一下each中callback遍歷各個屬性的實現邏輯。

var func = _[name] = obj[name];
 _.prototype[name] = function() {
     var args = [this._wrapped];
      push.apply(args, arguments);
     return func.apply(_, args);
};
複製代碼

第一句:func變量存儲每一個自定義函數

第二句: _.prototype[name]=function();_.prototype上面聲明相同屬性的函數

第三句:args變量存儲_wrapped下面掛載的值

第四句:跟var args = [that].concat(Array.from(arguments));做用類似,將兩邊的參數結合起來

第五句:執行func變量指向的函數,執行apply函數,將上下文對象_和待傳入的參數`args``傳入便可。

咱們再執行如下代碼:

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
複製代碼

結果以下:

image-20190306230712917

Perfect!

這個函數在咱們的瀏覽器中使用已經沒有問題。

可是在Node中呢?又引出新的問題。

再回歸兼容性問題

咱們知道在Node中,咱們是這樣的:

//a.js
let a = 1;
module.exports = a;
//index.js
let b = require('./a.js');
console.log(b) //打印1
複製代碼

那麼:

let _ = require('./underscore.js')
_([1,2,3,4]).first(2);
複製代碼

咱們也但願上述的代碼可以在Node中執行。

因此root._ = _是不夠的。

underscore是怎麼作的呢?

以下:

if (typeof exports != 'undefined' && !exports.nodeType) {
    if (typeof module != 'undefined' && !module.nodeType && module.exports) {
        exports = module.exports = _;
    }
    exports._ = _;
} else {
    root._ = _;
}
複製代碼

咱們看到當全局屬性exports不存在或者不是DOM節點時,說明它在瀏覽器中,因此:

root._ = _;

若是exports存在,那麼就是在Node環境下,咱們再來進行判斷:

若是module存在,而且不是DOM節點,而且module.exports也存在的話,那麼執行:

exports = module.exports = _;

在統一執行:

exports._ = _;

附錄

下面是最後整合的閹割版underscore代碼:

(function(){
    /** * 保證兼容性 */
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

    /** * 在調用_(obj)時,讓其執行new _(obj),並將obj掛載在_wrapped屬性之下 */
    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };
    
    //本身實現的first函數
    _.first = function(arr,n){
        var n = n || 0;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }

    //判斷是不是函數
    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

    //遍歷生成數組存儲_對象的函數值屬性
    _.functions = _.methods = function(obj) {
        var names = [];
        for (var key in obj) {
          if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
      };
  
    //本身實現的遍歷數組的函數
    _.each = function(arrs,callback){
        for(let i=0;i<arrs.length;i++){
            callback(arrs[i]);
        }
    }

    var ArrayProto = Array.prototype;
    var push = ArrayProto.push;
 
    //underscore實現的mixin函數
    _.mixin = function(obj) {
        _.each(_.functions(obj), function(name) {
        var func = _[name] = obj[name];
           _.prototype[name] = function() {
               var args = [this._wrapped];
               push.apply(args, arguments);
               return func.apply(_, args);
           };
        });
        return _;
    };


    //執行minxin函數
    _.mixin(_);
    if (typeof exports != 'undefined' && !exports.nodeType) {
        if (typeof module != 'undefined' && !module.nodeType && module.exports) {
            exports = module.exports = _;
        }
        exports._ = _;
    } else {
        root._ = _;
    }
})();
複製代碼

歡迎各位大佬拍磚!同時您的點贊是我寫做的動力~謝謝。

相關文章
相關標籤/搜索