原文: https://zhehuaxuan.github.io/...
做者:zhehuaxuan
Underscore 是一個 JavaScript 工具庫,它提供了一整套函數式編程的實用功能,可是沒有擴展任何 JavaScript 內置對象。javascript
本文主要梳理underscore內部的函數組織與調用邏輯的方式和思想。前端
經過這篇文章,咱們能夠:java
瞭解underscore在函數組織方面的巧妙構思;爲本身書寫函數庫提供必定思路;node
咱們開始!git
前端的小夥伴必定不會對jQuery陌生,常用$.xxxx
的形式進行調用,underscore使用_.xxxx
,若是本身在ES5語法中寫過自定義模塊的話,就能夠寫出下面一段代碼:github
//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瀏覽器中打開以後,打印出以下結果:編程
咱們看到在全局對象下有一個_
屬性,屬性下面掛載了自定義函數。
咱們不妨使用_.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));
輸出結果以下:瀏覽器
沒問題,咱們的函數庫製做完成了,咱們通常直接這麼用,也不會有太大問題。app
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中咱們可使用如下兩種方式調用:
console.log(_.first([1,2,3,4]));
console.log(_([1,2,3,4])).first();
在underscore中,它們返回的結果都是相同的。
第一種方式咱們如今就沒有問題,難點就是第二種方式的實現。
解決這個問題要達到兩個目的:
_
是一個函數,而且調用返回一個對象;_
對象上聲明的方法。咱們來看看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]))
時,this
爲window
,那麼(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 }
最後獲得的對象爲:
咱們執行以下代碼:
console.log(_([1,2,3,4])); console.log(_.prototype); console.log(_([1,2,3,4]).__proto__ == _.prototype);
看一下打印的信息:
這代表經過_(obj)
構建出來的對象確實具備兩個特徵:
_proto_
屬性指向_
的prototype
到此咱們已經完成了第一個問題。
接着解決第二個問題:
這個對象依然可以調用掛載在_
對象上聲明的方法
咱們先來執行以下代碼:
_([1,2,3,4]).first();
此時JavaScript執行器會先去找_([1,2,3,4])
返回的對象上是否有first
屬性,若是沒有就會順着對象的原型鏈上去找first
屬性,直到找到並執行它。
咱們發現_([1,2,3,4])
返回的對象屬性和原型鏈上都沒有first
!
那咱們本身先在_.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]));
效果以下:
原型鏈上找到了first
函數,咱們能夠調用first
函數了。以下:
console.log(_([1,2,3,4]).first());
惋惜報錯了:
因而調試一下:
咱們發現arr
是undefined
,可是咱們但願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());
效果以下:
咱們的效果彷佛已經達到了!
如今咱們執行下面的代碼:
console.log(_([1,2,3,4]).first(2));
調試一下:
涼涼了。
其實咱們但願的是:
將[1,2,3,4]
和2
以arguments
的形式傳入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);
看一下打印的效果:
參數都已經拿到了。
咱們調用函數一下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));
如今好像咱們全部的問題都解決了。
可是彷佛仍是怪怪的。
咱們每聲明一個函數都得在原型鏈上也聲明一個同名函數。形以下面:
_.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)
返回的打印信息:
確實是_
中自定義函數的屬性值。
咱們再來分析一下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));
結果以下:
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._ = _; } })();
歡迎各位大佬拍磚!同時您的點贊是我寫做的動力~謝謝。