原文: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瀏覽器中打開以後,打印出以下結果:瀏覽器
咱們看到在全局對象下有一個_
屬性,屬性下面掛載了自定義函數。
咱們不妨使用_.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));
複製代碼
輸出結果以下:
沒問題,咱們的函數庫製做完成了,咱們通常直接這麼用,也不會有太大問題。
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._ = _;
}
})();
複製代碼
歡迎各位大佬拍磚!同時您的點贊是我寫做的動力~謝謝。