[譯] 為函式自定義屬性的八種實現方法

leto發表於2019-02-23

介紹:本文來自 Stack Overflow 上 Adding custom properties to a function - John Slegers 的答案。“給函式自定義屬性”並不是常規做法,但是答案中給出的思路涉及廣泛,很值得學習。

首先,你要認識到標準的函式屬性( arguments,name,caller 和 length )都不能被覆蓋。所以,打消自定義的這些屬性名的想法吧。

給函式新增自定義屬性可以使用很多不同方法,這些都是跨瀏覽器相容的。

方法一:執行函式時新增屬性

var doSomething = function() {
    doSomething.name = 'Tom';
    doSomething.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

方法一(替代語法)

function doSomething() {
    doSomething.name = 'Tom';
    doSomething.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : doSomething
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : doSomething
doSomething.name2 : John 
複製程式碼

方法一(第二種替代語法)

var doSomething = function f() {
    f.name = 'Tom';
    f.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : f
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : f
doSomething.name2 : John 
複製程式碼

這種方法的問題在於,你需要至少先執行一次函式,才能完成屬性賦值。對於多數函式來說,很顯然我們不想這麼做。所以考慮其他選擇。

方法二:定義函式之後新增屬性

function doSomething() {
    return 'Beep';
};

doSomething.name = 'Tom';
doSomething.name2 = 'John';

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : doSomething
doSomething.name2 : John
doSomething() : Beep
doSomething.name : doSomething
doSomething.name2 : John 
複製程式碼

現在,你不需要在取得屬性值之前執行一次函式了。然而,不足之處是感覺這些屬性脫離了函式。

方法三:使用匿名函式包裝函式體(並立即執行)

var doSomething = (function(args) {
    var f = function() {
        return 'Beep';
    };
    for (i in args) {
        f[i] = args[i];
    }
    return f;
}({
    'name': 'Tom',
    'name2': 'John'
}));

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

使用匿名函式包裝你的函式,就可以將屬性放入物件中,然後在匿名函式內遍歷這些屬性加以新增。用這種方式,屬性與函式聯絡密切了。當你想要從已存在的物件中拷貝屬性到函式中時,這種技術很有效。

然而缺點是,你只能在定義函式時一次性新增這些屬性。而且,這也違背了軟體工程的 DRY 原則(程式碼的抽象三原則),尤其是你需要經常給各種函式新增屬性的話。

方法四:為函式新增一個“extend”方法,用於從物件中逐個新增屬性

var doSomething = function() {
    return 'Beep';
};

doSomething.extend = function(args) {
    for (i in args) {
        this[i] = args[i];
    }
    return this;
}

doSomething.extend({
    'name': 'Tom',
    'name2': 'John'
});

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

可以一次性處理多個屬性,也可以隨時從別處新增了。

然而,程式碼同樣違反了 DRY 原則,如果你經常這麼做的話。

方法五:建立一個通用的“extend”函式

// 注:原文有錯誤,已改正
var extend = function(obj, args) {
    if (Array.isArray(args) || (args !== null && typeof args === 'object')) {
        for (i in args) {
            obj[i] = args[i];
        }
    }
    return obj;
}

var doSomething = extend(
    function() {
        return 'Beep';
    }, {
        'name': 'Tom',
        'name2': 'John'
    }
);

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

這是一個通用的繼承方法,更符合 DRY 原則,允許你任性新增屬性。

方法六:建立一個 extendableFunction 物件,用它為函式新增擴充套件函式

var extendableFunction = (function() {
    var extend = function(args) {
        if (Array.isArray(args) || (args !== null && typeof args === 'object')) {
            for (i in args) {
                this[i] = args[i];
            }
        }
        return this;
    };
    var ef = function(v, obj) {
        v.extend = extend;
        return v.extend(obj);
    };

    ef.create = function(v, args) {
        return new this(v, args);
    };
    return ef;
})();

var doSomething = extendableFunction.create(
    function() {
        return 'Beep';
    }, {
        'name': 'Tom',
        'name2': 'John'
    }
);

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

不再需要一個通用的“extend”函式了,這種方法生產出來的函式自帶一個“extend”函式方法。

注:extendableFunction 和上面的 extend 都是高階函式(high order function),但又都不是純函式(pure function)。相關知識可以瞭解一下函數語言程式設計,有能力的話最好閱讀英文原版 mostly-adequate-guide

方法七:向 Function 原形掛載“extend”函式

Function.prototype.extend = function(args) {
    if (Array.isArray(args) || (args !== null && typeof args === 'object')) {
        for (i in args) {
            this[i] = args[i];
        }
    }
    return this;
};

var doSomething = function() {
    return 'Beep';
}.extend({
    name : 'Tom',
    name2 : 'John'
});

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 
複製程式碼

這種做法的一個巨大優勢是,向函式新增新的屬性變得非常簡單,這種做法也符合 DRY 原則,並且很物件導向。另外,向 Function 原形鏈掛載方法也節省記憶體。

然而,這種做法的缺點是並不具有充分的前瞻性。一旦將來瀏覽器給 Funcion 的原形上掛載了原生的“extend”方法,你的程式碼就會出問題。

方法八:函式遞迴一次然後返回自身

var doSomething = (function f(arg1) {
    if(f.name2 === undefined) {
        f.name = 'Tom';
        f.name2 = 'John';
        // 注:此處原答案疑似錯誤,已改正
        f.extend = function(args) {
            if (Array.isArray(args) || (args !== null && typeof args === 'object')) {
                for (i in args) {
                    this[i] = args[i];
                }
            }
            return this;
        };
        return f;
    } else {
        return 'Beep';
    }
})();

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
複製程式碼

輸出:

doSomething.name : f
doSomething.name2 : John
doSomething() : Beep
doSomething.name : f
doSomething.name2 : John 
複製程式碼

執行該函式一次,檢查它所需要的屬性。如果沒有,設定該屬性並返回函式本體;如果設定過了,直接執行該函式。

如果你將“extend”方法設定為屬性之一,之後還可以為該函式新增別的屬性。


向物件中新增自定義屬性

我雖然給出了這麼多方法,還是建議你不要給函式設定屬性;設定物件的屬性好多了!

我個人喜歡使用下面這種語法來實現單例類:

var keyValueStore = (function() {
    return {
        'data' : {},
        'get' : function(key) { return keyValueStore.data[key]; },
        'set' : function(key, value) { keyValueStore.data[key] = value; },
        'delete' : function(key) { delete keyValueStore.data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in keyValueStore.data) l++;
            return l;
        }
    }
})();
複製程式碼

這種寫法的好處是相容公共和私有變數。例如,這樣你就可以讓“data”變為 private 的:

var keyValueStore = (function() {
    var data = {};

    return {
        'get' : function(key) { return data[key]; },
        'set' : function(key, value) { data[key] = value; },
        'delete' : function(key) { delete data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in data) l++;
            return l;
        }
    }
})();
複製程式碼

可是你想要多個 datastore 例項?沒問題!

var keyValueStore = (function() {
    var count = -1;
    
    return (function kvs() {
        count++;
        return {
            'data': {},
            'create': function() { return new kvs(); },
            'get' : function(key) { return this.data[key]; },
            'set' : function(key, value) { this.data[key] = value; },
            'delete' : function(key) { delete this.data[key]; },
            'getLength' : function() {
                var l = 0;
                for (p in this.data) l++;
                return l;
            }
        }
    })();
})();
複製程式碼

注:使用時可以按照以下語法

let s1 = keyValueStore;
let s2 = s1.create();
s1.set('a', 1);
s1.set('b', 2);
s2.set('c', 3);
s1.count() === s2.count(); // 輸出 true;count() 返回 kvs 的個數
s1.getLength(); // 輸出 2;getLength() 返回每個 kvs 自身的資料條數
s2.getLength(); // 輸出 1
s1.get('c'); // 輸出 undefined;因為“c”是 s2 所包含的
複製程式碼

最後,你可以隔離例項和單例類的屬性,並在原形上給例項定義 public 方法。

var keyValueStore = (function() {
    var count = 0; // Singleton private properties

    var kvs = function() {
        count++; // Instance private properties
        this.data = {};  // Instance public properties
    };

    kvs.prototype = { // Instance public properties
        'get' : function(key) { return this.data[key]; },
        'set' : function(key, value) { this.data[key] = value; },
        'delete' : function(key) { delete this.data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in this.data) l++;
            return l;
        }
    };

    return  { // Singleton public properties
        'create' : function() { return new kvs(); },
        'count' : function() { return count; }
    };
})();
複製程式碼

使用上面的方法,好處有:

  • 為物件宣告多個例項
  • 保有私有變數
  • 定義類的變數

使用方法如下:

kvs = keyValueStore.create();
kvs.set('Tom', "Baker");
kvs.set('Daisy', "Hostess");
var profession_of_daisy = kvs.get('Daisy');  // 輸出 Hostess
kvs.delete('Daisy');
console.log(keyValueStore.count());  // 輸出 1
複製程式碼

注:keyValueStore 是單例,create 與 count 是其自身方法,create 用於新建資料倉儲 kvs,count 為生產的 kvs 計數;kvs 僅有 get、set、delete、getLength 方法,無法像前一種實現一樣直接呼叫 create 和 count 方法了。

相關文章