基於 JavaScript function 和 prototype 的架構上,可以實現 JavaScript Mixin。為什麼需要 Mixin(維基百科)?有時候多個 class 之間存在部分 methods 是做一樣的事,這些相同的或相似的 method 可以藉由 mixin 做到分離並共用,達成「Don’t repeat yourself」。
JavaScript 目前已經有看似物件導向的 class 寫法,但背後的實作還是使用 function。因此,對 prototype 進行增刪 methods 是重點之一。
本篇將介紹如何對 function 和 object(已經 initialize 的 instance)做 mixin,來源與目標可以是 function 也可以是 object。
取得來源的 Property
取得 object 的 property 比較簡單,可以用 Object.getOwnPropertyNames(obj)
取得 object 的所有 properties 值,因此可以 iterate 他回傳的值來判斷該 property 是不是 function。
function 的 property 定義在 prototype 中,因此可以用 Object.getOwnPropertyNames(function.prototype)
取得該 function 擁有的所有 prototype。
基於上述的分析,可以寫出以下用來取得來源所有 property 的方法:
// Get the pointer of all properties.
let sourcePtr = source instanceof Function ? source.prototype : source;
let propList = Object.getOwnPropertyNames(sourcePtr);
首先,使用 sourcePtr
當作 pointer 指向來源 properties 的儲存位置,source instanceof Function
判斷傳來的來源是不是 Function,如果是 function 則使用 function.prototype
,若不是代表來源是 object,直接 reference source。接著使用 Object.getOwnPropertyNames(sourcePtr)
取得 properties 名稱陣列。
因為希望可以對 function & object 都適用,所以目標也可以用上面的方法取得 pointer:
let targetPtr = target instanceof Function ? target.prototype : target;
Iterate properties
基於上面取得的 properties array,可以開始逐一加入至目標內。這裡可以用 for loop 解決或 forEach 也可以。在迴圈中,需要做三件事:
- Property name 篩選,可以在這裡篩掉 private 的方法。
- 檢查有無 property name 衝突
- 將 source 的 function property 加到 target 內
for (let propName of propList) {
// Filtering Property name
// Conflicting Property name handling
// Add property to target
}
過濾 Property Name
一般預設 property 名稱帶有 _ 前綴的代表他是 private property,constructor 不可以覆蓋,這兩個可以用正規表示法處理。以及如果 property 不是 function 則需要跳過。對此可以寫出以下條件式:
propName.match(/^_+|(constructor)$/g) || typeof targetPtr[propName] !== "function"
處理 Property Name 衝突
如果發生 target 中已經有該 property 與 source property 衝突時,可以在這裡處理衝突的解決方式。而我簡單地報錯並跳過不將該 property 加入至 target 中。
if (targetPtr[propName]) {
console.error(`The "${propName}" was conflicting.`);
continue;
}
加入至目標
Function prototype 新增的方式與 object 不相同,所以要先判斷 target 是不是 function。對 function 可以使用 Object.defineProperty 將來源的 property 加入至目標的 prototype 中;對 object 可以簡單地用 assignment 的方式加入。
// Add property to target
if (target instanceof Function) {
// Function Mixin
Object.defineProperty(targetPtr, propName, {
value: sourcePtr[propName],
writable: false,
enumerable: false,
configurable: false
});
} else {
// Instance Mixin
targetPtr[propName] = sourcePtr[propName];
}

JavaScript Mixin Helper Function
統整上方的設計流程與結果,可以得到一個 mixin helper function。這是一個簡單的 JavaScript Mixin 實作方式,可以依照需求調整內部的處理方式。重點在於 assign property 的方式,object 的方式非常簡單,而 function prototype 看似複雜實際上非常直觀。
/**
* Mixin in JavaScript
* @param {Function | Object} target
* @param {Function | Object} source
*/
function mixin(target, source) {
let targetPtr = target instanceof Function ? target.prototype : target;
let sourcePtr = source instanceof Function ? source.prototype : source;
let propList = Object.getOwnPropertyNames(sourcePtr);
for (let propName of propList) {
// Filtering Property name
if (propName.match(/^_+|(constructor)$/g) || typeof sourcePtr[propName] !== "function") {
continue;
}
// Conflicting Property name handling
if (targetPtr[propName]) {
console.error(`The "${propName}" was conflicting.`);
continue;
}
// Add property to target
if (target instanceof Function) {
// Function Mixin
Object.defineProperty(targetPtr, propName, {
value: sourcePtr[propName],
writable: false,
enumerable: false,
configurable: false
});
} else {
// Instance Mixin
targetPtr[propName] = sourcePtr[propName];
}
}
};