跟underscore一起學如何寫函數庫

原文:https://zhehuaxuan.github.io/...
作者:zhehuaxuan

目的

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

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

通過這篇文章,我們可以:

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

爲自己書寫函數庫提供一定思路;

我們開始!

自己寫個函數庫

前端的小夥伴一定不會對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)在全局環境下直接調用。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章