博客 / 詳情

返回

學習 underscore 源碼整體架構,打造屬於自己的函數式編程類庫

前言

你好,我是若川。這是學習源碼整體架構系列第二篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。

要是有人説到怎麼讀源碼,正在讀文章的你能推薦我的源碼系列文章,那真是太好了

學習源碼整體架構系列文章如下:

1.學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫
2.學習 underscore 源碼整體架構,打造屬於自己的函數式編程類庫
3.學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫
4.學習 sentry 源碼整體架構,打造屬於自己的前端異常監控SDK
5.學習 vuex 源碼整體架構,打造屬於自己的狀態管理庫
6.學習 axios 源碼整體架構,打造屬於自己的請求庫
7.學習 koa 源碼的整體架構,淺析koa洋葱模型原理和co原理
8.學習 redux 源碼整體架構,深入理解 redux 及其中間件原理
感興趣的讀者可以點擊閲讀。

其他源碼計劃中的有:expressvue-rotuerreact-redux 等源碼,不知何時能寫完(哭泣),歡迎持續關注我(若川)。

源碼類文章,一般閲讀量不高。已經有能力看懂的,自己就看了。不想看,不敢看的就不會去看源碼。

所以我的文章,儘量寫得讓想看源碼又不知道怎麼看的讀者能看懂。

雖然看過挺多underscore.js分析類的文章,但總感覺少點什麼。這也許就是紙上得來終覺淺,絕知此事要躬行吧。於是決定自己寫一篇學習underscore.js整體架構的文章。

本文章學習的版本是v1.9.1
unpkg.com源碼地址:https://unpkg.com/underscore@...

雖然很多人都沒用過underscore.js,但看下官方文檔都應該知道如何使用。

從一個官方文檔_.chain簡單例子看起:

_.chain([1, 2, 3]).reverse().value();
// => [3, 2, 1]

看例子中可以看出,這是支持鏈式調用。

讀者也可以順着文章思路,自行打開下載源碼進行調試,這樣印象更加深刻。

鏈式調用

_.chain 函數源碼:

_.chain = function(obj) {
    var instance = _(obj);
    instance._chain = true;
    return instance;
};

這個函數比較簡單,就是傳遞obj調用_()。但返回值變量竟然是instance實例對象。添加屬性_chain賦值為true,並返回intance對象。但再看例子,實例對象竟然可以調用reverse方法,再調用value方法。猜測支持OOP(面向對象)調用。

帶着問題,筆者看了下定義 _ 函數對象的代碼。

_ 函數對象 支持OOP

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

如果參數obj已經是_的實例了,則返回obj
如果this不是_的實例,則手動 new _(obj);
再次new調用時,把obj對象賦值給_wrapped這個屬性。
也就是説最後得到的實例對象是這樣的結構
`{

_wrapped: '參數obj',

}`
它的原型_(obj).__proto___.prototype;

如果對這塊不熟悉的讀者,可以看下以下這張圖(之前寫面試官問:JS的繼承畫的圖)。
構造函數、原型對象和實例關係圖
繼續分析官方的_.chain例子。這個例子拆開,寫成三步。

var part1 = _.chain([1, 2, 3]);
var part2 = part1.reverse();
var part3 = part2.value();

// 沒有後續part1.reverse()操作的情況下
console.log(part1); // {__wrapped: [1, 2, 3], _chain: true}

console.log(part2); // {__wrapped: [3, 2, 1], _chain: true}

console.log(part3); // [3, 2, 1]

思考問題:reverse本是Array.prototype上的方法呀。為啥支持鏈式調用呢。
搜索reverse,可以看到如下這段代碼:

並將例子代入這段代碼可得(怎麼有種高中做數學題的既視感^_^):

_.chain([1,2,3]).reverse().value()
var ArrayProto = Array.prototype;
// 遍歷 數組 Array.prototype 的這些方法,賦值到 _.prototype 上
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
    // 這裏的`method`是 reverse 函數
    var method = ArrayProto[name];
    _.prototype[name] = function() {
    // 這裏的obj 就是數組 [1, 2, 3]
    var obj = this._wrapped;
    // arguments  是參數集合,指定reverse 的this指向為obj,參數為arguments, 並執行這個函數函數。執行後 obj 則是 [3, 2, 1]
    method.apply(obj, arguments);
    if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
    // 重點在於這裏 chainResult 函數。
    return chainResult(this, obj);
    };
});
// Helper function to continue chaining intermediate results.
var chainResult = function(instance, obj) {
    // 如果實例中有_chain 為 true 這個屬性,則返回實例 支持鏈式調用的實例對象  { _chain: true, this._wrapped: [3, 2, 1] },否則直接返回這個對象[3, 2, 1]。
    return instance._chain ? _(obj).chain() : obj;
};

if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
提一下上面源碼中的這一句,看到這句是百思不得其解。於是趕緊在github中搜索這句加上""雙引號。表示全部搜索。

搜索到兩個在官方庫中的ISSUE,大概意思就是兼容IE低版本的寫法。有興趣的可以點擊去看看。

I don't understand the meaning of this sentence.

[why delete obj[0]](https://github.com/jashkenas/...

基於流的編程

至此就算是分析完了鏈式調用_.chain()_ 函數對象。這種把數據存儲在實例對象{_wrapped: '', _chain: true} 中,_chain判斷是否支持鏈式調用,來傳遞給下一個函數處理。這種做法叫做 基於流的編程

最後數據處理完,要返回這個數據怎麼辦呢。underscore提供了一個value的方法。

_.prototype.value = function(){
    return this._wrapped;
}

順便提供了幾個別名。toJSONvalueOf
_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

還提供了 toString的方法。

_.prototype.toString = function() {
    return String(this._wrapped);
};

這裏的String()new String() 效果是一樣的。
可以猜測內部實現和 _函數對象類似。

var String = function(){
    if(!(this instanceOf String)) return new String(obj);
}
var chainResult = function(instance, obj) {
    return instance._chain ? _(obj).chain() : obj;
};

細心的讀者會發現chainResult函數中的_(obj).chain(),是怎麼實現實現鏈式調用的呢。

_(obj) 是返回的實例對象{_wrapped: obj}呀。怎麼會有chain()方法,肯定有地方掛載了這個方法到_.prototype上或者其他操作,這就是_.mixin()

_.mixin 掛載所有的靜態方法到 _.prototype, 也可以掛載自定義的方法

_.mixin 混入。但侵入性太強,經常容易出現覆蓋之類的問題。記得之前Reactmixin功能,Vue也有mixin功能。但版本迭代更新後基本都是慢慢的都不推薦或者不支持mixin

_.mixin = function(obj) {
    // 遍歷對象上的所有方法
    _.each(_.functions(obj), function(name) {
        // 比如 chain, obj['chain'] 函數,自定義的,則賦值到_[name] 上,func 就是該函數。也就是説自定義的方法,不僅_函數對象上有,而且`_.prototype`上也有
    var func = _[name] = obj[name];
    _.prototype[name] = function() {
        // 處理的數據對象
        var args = [this._wrapped];
        // 處理的數據對象 和 arguments 結合
        push.apply(args, arguments);
        // 鏈式調用  chain.apply(_, args) 參數又被加上了 _chain屬性,支持鏈式調用。
        // _.chain = function(obj) {
        //    var instance = _(obj);
        //    instance._chain = true;
        //    return instance;
        };
        return chainResult(this, func.apply(_, args));
    };
    });
    // 最終返回 _ 函數對象。
    return _;
};

_.mixin(_);

_mixin(_) 把靜態方法掛載到了_.prototype上,也就是_.prototype.chain方法 也就是 _.chain方法。

所以_.chain(obj)_(obj).chain()效果一樣,都能實現鏈式調用。

關於上述的鏈式調用,筆者畫了一張圖,所謂一圖勝千言。

underscore.js 鏈式調用圖解

_.mixin 掛載自定義方法

掛載自定義方法:
舉個例子:

_.mixin({
    log: function(){
        console.log('哎呀,我被調用了');
    }
})
_.log() // 哎呀,我被調用了
_().log() // 哎呀,我被調用了

_.functions(obj)

_.functions = _.methods = function(obj) {
    var names = [];
    for (var key in obj) {
    if (_.isFunction(obj[key])) names.push(key);
    }
    return names.sort();
};

_.functions_.methods 兩個方法,遍歷對象上的方法,放入一個數組,並且排序。返回排序後的數組。

underscore.js 究竟在__.prototype掛載了多少方法和屬性

再來看下underscore.js究竟掛載在_函數對象上有多少靜態方法和屬性,和掛載_.prototype上有多少方法和屬性。

使用for in循環一試便知。看如下代碼:

var staticMethods = [];
var staticProperty = [];
for(var name in _){
    if(typeof _[name] === 'function'){
        staticMethods.push(name);
    }
    else{
        staticProperty.push(name);
    }
}
console.log(staticProperty); // ["VERSION", "templateSettings"] 兩個
console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138個
var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
    if(typeof _.prototype[name] === 'function'){
        prototypeMethods.push(name);
    }
    else{
        prototypeProperty.push(name);
    }
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152個

根據這些,筆者又畫了一張圖underscore.js 原型關係圖,畢竟一圖勝千言。

codeunderscore.js/code 原型關係圖

整體架構概覽

匿名函數自執行

(function(){

}());

這樣保證不污染外界環境,同時隔離外界環境,不是外界影響內部環境。

外界訪問不到裏面的變量和函數,裏面可以訪問到外界的變量,但裏面定義了自己的變量,則不會訪問外界的變量。
匿名函數將代碼包裹在裏面,防止與其他代碼衝突和污染全局環境。
關於自執行函數不是很瞭解的讀者可以參看這篇文章。
[[譯] JavaScript:立即執行函數表達式(IIFE)](https://segmentfault.com/a/11...

root 處理

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

支持瀏覽器環境nodeWeb Workernode vm微信小程序

導出

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

關於root處理導出的這兩段代碼的解釋,推薦看這篇文章冴羽:underscore 系列之如何寫自己的 underscore,講得真的太好了。筆者在此就不贅述了。
總之,underscore.js作者對這些處理也不是一蹴而就的,也是慢慢積累,和其他人提ISSUE之後不斷改進的。

支持 amd 模塊化規範

if (typeof define == 'function' && define.amd) {
    define('underscore', [], function() {
        return _;
    });
}

_.noConflict 防衝突函數

源碼:

// 暫存在 root 上, 執行noConflict時再賦值回來
var previousUnderscore = root._;
_.noConflict = function() {
    root._ = previousUnderscore;
    return this;
};

使用:

<script>
var _ = '我就是我,不一樣的煙火,其他可不要覆蓋我呀';
</script>
<script src="https://unpkg.com/underscore@1.9.1/underscore.js">
</script>
<script>
var underscore = _.noConflict();
console.log(_); // '我就是我,不一樣的煙火,其他可不要覆蓋我呀'
underscore.isArray([]) // true
</script>

總結

全文根據官網提供的鏈式調用的例子, _.chain([1, 2, 3]).reverse().value();較為深入的調試和追蹤代碼,分析鏈式調用(_.chain()_(obj).chain())、OOP、基於流式編程、和_.mixin(_)_.prototype掛載方法,最後整體架構分析。學習Underscore.js整體架構,利於打造屬於自己的函數式編程類庫。

文章分析的源碼整體結構。

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

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

    if (typeof exports != 'undefined' && !exports.nodeType) {
      if (typeof module != 'undefined' && !module.nodeType && module.exports) {
        exports = module.exports = _;
      }
      exports._ = _;
    } else {
      root._ = _;
    }
    _.VERSION = '1.9.1';

    _.chain = function(obj) {
      var instance = _(obj);
      instance._chain = true;
      return instance;
    };

    var chainResult = function(instance, obj) {
      return instance._chain ? _(obj).chain() : obj;
    };

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

    _.mixin(_);

    _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
      var method = ArrayProto[name];
      _.prototype[name] = function() {
        var obj = this._wrapped;
        method.apply(obj, arguments);
        if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
        return chainResult(this, obj);
      };
    });

    _.each(['concat', 'join', 'slice'], function(name) {
      var method = ArrayProto[name];
      _.prototype[name] = function() {
        return chainResult(this, method.apply(this._wrapped, arguments));
      };
    });

    _.prototype.value = function() {
      return this._wrapped;
    };

    _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

    _.prototype.toString = function() {
      return String(this._wrapped);
    };

    if (typeof define == 'function' && define.amd) {
      define('underscore', [], function() {
        return _;
      });
    }
}());

下一篇文章可是學習lodash的源碼整體架構。學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫

讀者發現有不妥或可改善之處,歡迎評論指出。另外覺得寫得不錯,可以點贊、評論、轉發,也是對筆者的一種支持。

推薦閲讀

underscorejs.org 官網
undersercore-analysis
underscore 系列之如何寫自己的 underscore

筆者往期文章

學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:能否模擬實現JS的call和apply方法
面試官問:能否模擬實現JS的bind方法
面試官問:能否模擬實現JS的new操作符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

關於

作者:常以若川為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
若川的博客,使用vuepress重構了,閲讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

歡迎加微信交流 微信公眾號

可能比較有趣的微信公眾號,長按掃碼關注。也可以加微信 ruochuan12,註明來源,拉您進【前端視野交流羣】。

若川視野

user avatar jm365 頭像 frontoldman 頭像 shellingfordly 頭像 yxaw 頭像 codinger 頭像 jackysummer 頭像 anetin 頭像 yookoo 頭像 jiasm 頭像 tongouba 頭像 lijing0302 頭像 chamson_5eb23538190b0 頭像
25 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.