原文: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瀏覽器中開啟之後,列印出如下結果:
我們看到在全域性物件下有一個_
屬性,屬性下面掛載了自定義函式。
我們不妨使用_.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._ = _;
}
})();
複製程式碼
歡迎各位大佬拍磚!同時您的點贊是我寫作的動力~謝謝。