跟underscore一起學如何寫函式庫

EdwardXuan發表於2019-03-06

原文:zhehuaxuan.github.io/2019/03/07/…
作者: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._ = _;
    }
})();
複製程式碼

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

相關文章