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