跟underscore一起學如何寫函式庫
原文:https://zhehuaxuan.github.io/2019/03/07/%E8%B7%9Funderscore%E4%B8%80%E8%B5%B7%E5%AD%A6%E5%A6%82%E4%BD%95%E5%86%99%E5%87%BD%E6%95%B0%E5%BA%93/ 作者:zhehuaxuan
目的
Underscore 是一個 JavaScript 工具庫,它提供了一整套函數語言程式設計的實用功能,但是沒有擴充套件任何 JavaScript 內建物件。
本文主要用於梳理和研究underscore內部是如何組織和處理函式的。
通過這篇文章,我們可以:
瞭解underscore在函式組織方面的巧妙構思;
為自己書寫函式庫提供一定思路;
我們開始!
自己寫個函式庫
前端的小夥伴一定不會對jQuery陌生,經常使用 $.xxxx
的形式進行進行呼叫,underscore也使用 _.xxxx
,如果自己在ES5語法中寫過自定義模組的話,就可以擼出下面一段程式碼:
(function(){ //定義 var root = this; var _ = {}; _.first = function(arr,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.1IIFE函式中的原始碼,對應於我們上面自己寫的 var root = this;
。
在原始碼中作者也解釋了:建立root物件,並且給root賦值:
瀏覽器端:window也可以是window.self或者直接self
服務端(node):global
WebWorker:self
虛擬機器:this
underscore充分考慮了相容性。
支援兩種不同風格的函式呼叫
在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
(obj 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
上面加一個上去試一下:
(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=0){ if(n==0) return arr[0]; return arr.slice(0,n); } _.prototype.first = function(arr,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=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=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=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._ = _; } })(); 複製程式碼
歡迎各位大佬拍磚!同時您的點贊是我寫作的動力~謝謝。