underscore作為開發中比較常用的一個javascript工具庫,提供了一套豐富的函數語言程式設計功能,該庫並沒有擴充原有的javascript原生物件,而是在自定義的
_
物件上,提供了100多個方法函式。在這個系列中,將從uderscore原始碼角度, 打造一個自己的underscore框架
一,框架設計
1.1 自執行函式
現代js 庫的框架設計,一般都是以自執行函式的形式,自執行函式一般有兩種形式
(function(){
// 形式一
}())
複製程式碼
(function(){
// 形式二
})()
複製程式碼
我們知道,函式宣告的形式會掛載到window物件作為方法存在,而函式表示式的形式則會掛載在window物件作為屬性存在,這都會造成變數汙染,而自執行函式的好處在於可以防止變數的汙染,函式執行完後便立刻銷燬掉。
1.2 使用風格
underscore有兩種風格形式可以使用,一種是物件導向型別,另一種是函式型別。
// 例子
_.map([1, 2, 3], function(n){ return n * 2; });
_([1, 2, 3]).map(function(n){ return n * 2; });
複製程式碼
因此,在定義underscore類的時候需要考慮物件和函式兩種場景。當以函式的形式呼叫時需要把 _ 當作一個建構函式並返回他的例項化。程式碼如下
(function(root){
var _ = function (obj) {
if (!(this instanceof _)) {
return new _(obj)
}
}
root._ = _
}(this))
複製程式碼
1.3 使用環境
現在前端開發重視模組化,以node服務端而論, 我們有commonjs規範,以客戶端而論,我們有AMD 和 CMD規範,對應的模組載入器為 requirejs 和 seajs。目前通行的javascript模組規範主要集中在commonjs 和 AMD,因此為了讓定義的underscore庫能夠適用於各種規範。在框架的定義時需檢測使用環境併相容各種規範。
- 服務端:commonjs規範,檢測module.exports 是否存在,滿足時通過 module.exports = {} 將 underscore暴露出來,不滿足則 通過window物件暴露出來。
- 客戶端: AMD 規範, 檢測 define.amd 是否存在,滿足時通過
define('**', [], function(){ return '***' })
暴露模組
(function (root) {
var _ = function (obj) {
if (!(this instanceof _)) {
return new _(obj)
}
}
// commonjs 規範 檢測 module.exports 是否存在
if ((typeof module !== 'undefined' && module.exports)) {
module.exports = {
_: _
}
} else {
root._ = _;// window 物件暴露方法
}
// amd 規範,檢測 define.amd 是否存在
if (typeof define == 'function' && define.amd) {
define('underscore', [], function () {
return _;
});
}
}(this))
複製程式碼
1.3.1 服務端使用
// commonjs
const _ = require('./underscore.js')
console.log(_)
複製程式碼
1.3.2 客戶端使用
// AMD
require(['underscore'], function (underscore) {
console.log(underscore)
})
複製程式碼
1.4 方法定義
underscore的呼叫,既可以通過_.unique()
,也可以通過 _().unique()
,兩種方法效果相同卻需要在框架設計時定義兩套方法,一套是定義 _ 物件的靜態方法,另一套是擴充套件 _物件原型鏈上的方法。
_.uniqe = function() {}
_.prototype.unique = function() {}
複製程式碼
為了避免冗餘程式碼,可以將定義好的靜態方法複製一份成為原型鏈上的方法
(function(root){
···
_.mixins = function() {
// 複製靜態方法到原型上
}
_.mixins() // 執行方法
}(this))
複製程式碼
mixins 方法的實現,需要遍歷 underscore 物件上所有的靜態方法,因此需要先完成對 遍歷方法 _.each 的實現
1.41 _.each
_.each(list, iteratee, [context]) Alias: forEach 遍歷list中的所有元素,按順序用每個元素當做引數呼叫 iteratee 函式。如果傳遞了context引數,則把iteratee繫結到context物件上。每次呼叫iteratee都會傳遞三個引數:(element, index, list)。如果list是個JavaScript物件,iteratee的引數是 (value, key, list))。返回list以方便鏈式呼叫。
each 的第一個引數按照文件可以支援 陣列,類陣列,物件三種型別,陣列類陣列和物件在遍歷時的處理方式不同。前者回撥函式處理的是 值和下標,後者處理的是 值和屬性。
// 判斷陣列,類陣列方法
(function(root) {
···
_.each = function (list, callback, context) {
// context 存在會改變callback 中this 的指向
var i = 0;
var key;
if (isArrayLikeLike(list)) { // 陣列,類陣列
for (var i = 0; i < list.length; i++) {
context ? callback.call(context, list[i], i, list) : callback(list[i], i, list)
}
} else { // 物件
for (key in list) {
context ? callback.call(context, list[key], key) : callback(list[key], key)
}
}
}
var isArrayLike = function (collection) {
// 返回引數 collection 的 length 屬性值
var length = collection.length;
// length是數值,非負,且小於等於MAX_ARRAY_INDEX
// MAX_ARRAY_INDEX = Math.pow(2, 53) - 1
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
}
}(this))
複製程式碼
1.4.2 _.mixin
mixin方法的設計,目的是為了在underscore原型物件上擴充套件更多的方法,它既可以用來擴充套件使用者自定義的方法,比如
_.mixin({
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
}
});
_("fabio").capitalize();
=> "Fabio"
複製程式碼
當然也可以用來內部拷貝靜態方法到原型鏈的方法上。
(function(root){
···
var push = Array.prototype.push
var _ = function (obj) {
if (!(this instanceof _)) {
return new _(obj)
}
this.wrap = obj // 儲存例項物件傳過來的引數
}
_.mixins = function (obj) {
_.each(obj, function (value, key) {
_.prototype[key] = function () {
var args = [this.wrap]
push.apply(args, arguments)
return value.apply(this, args)
}
})
}
_.mixins(_)
}(this))
複製程式碼
其中關注點在arguments 的處理上,靜態方法需要傳遞目標源作為方法的引數 即_.unique(目標源, 回撥函式)
,而例項方法的目標源儲存在構造物件的屬性中 ,即_(目標源).unique(回撥函式)
,因此定義例項方法時需要合併屬性和回撥函式。即Array.prorotype.push.apply([this.wrap], arguments)
,之後將他作為引數傳遞給靜態方法並返回處理結果。
將類陣列轉成陣列的方法
Array.prototype.slice.call(類陣列)
var a = []; Array.prototype.push.apply(a, 類陣列); console.log(a);
var a = []; Array.prototype.concat.apply(a, 類陣列); console.log(a);
- ES6方法
Array.from(類陣列)
- ES6擴充套件運算子
var args = [...類陣列]
1.5 鏈式呼叫
1.5.1 _.chain()
返回一個封裝的物件. 在封裝的物件上呼叫方法會返回封裝的物件本身, 直道 value 方法呼叫為止。
underscore中方法的呼叫返回的是處理後的值,因此無法支援方法的鏈式呼叫。如果需要鏈式呼叫,需要使用chain()方法,chain的使用會使每次呼叫方法後返回underscore的例項物件,直到 呼叫value方法才停止返回。
(function(root){
···
// chain方法會返回 _ 例項,並且標註該例項是否允許鏈式呼叫的
_.chain = function(obj) {
var instance = _(obj);
instance.chain = true;
return instance
}
// 增加是否支援鏈式呼叫的判斷,如果支援,則返回該例項,不支援則直接返回結果,
var chainResult = function (instance, obj) {
return instance.chain ? _(obj).chain() : obj
}
_.mixins = function (obj) {
_.each(obj, function (value, key) {
_.prototype[key] = function () {
var args = [this.wrap]
push.apply(args, arguments)
return chainResult(this, value.apply(this, args)) // 修改例項方法的返回值,返回值通過chainResult 包裝,根據chainResult的判斷結果改變返回值
}
})
}
}(this))
複製程式碼
1.5.2 value()
因為鏈式呼叫會使underscore的方法返回他的例項物件,所以當需要結束這一呼叫行為時,需要使用value()。 value()方法會返回撥用的結果。
(function(root){
···
_.value = function(instance) {
return instance.wrap
}
}(this))
複製程式碼