JavaScript Mixin:可以對 Function & Object 做 Mixin 的方法

基於 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 也可以。在迴圈中,需要做三件事:

  1. Property name 篩選,可以在這裡篩掉 private 的方法。
  2. 檢查有無 property name 衝突
  3. 將 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 Idea

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];
        }
    }
};

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端