JavaScript模块模式:使用Prototype创建多个实例

6
我想实现的目标是:使用JavaScript中的原型创建模块,以便用户可以多次实例化模块,并为每个实例设置不同的选项。
问题在于:当使用var my_module3 = new module();然后尝试使用my_module3.init({ option: "value" });设置选项时,它只会改变对象一次,而不是每次都改变。
测试:使用console.log可以看到它打印出具有相同选项的两个对象,即使它们被不同地设置。
Object {first: "Barry", second: "Larry", third: "Sam"} 
Object {first: "Barry", second: "Larry", third: "Sam"} 

这是我的 jsFiddle 完整代码: http://jsfiddle.net/11bLouc8/2/

        var module = (function () {
        // default options
        var options = {
            first: "test",
            second: "test2",
            third: "test3"
        };
        // take in useroptions and replace default options
        var module = function(userOptions) {
            if (userOptions != null && userOptions != undefined
                && userOptions != 'undefined') {
                for (var opt in options) {
                    if (userOptions.hasOwnProperty(opt)) {
                        options[ opt ] = userOptions[ opt ];
                    }
                }
            }
        };

        //prototype
        module.prototype = {
            init: module,
            options: options
        };

        return module;

    })();

    // create a new instance
    var my_module3 = new module();
    my_module3.init({
        first: "Mike",
        second: "Lisa",
        third: "Mary"
    });

    // another instance
    var my_module2 = new module();
    my_module2.init({
        first: "Barry",
        second: "Larry",
        third: "Sam"
    });

1
为什么你的代码修改默认选项?你需要为每个实例创建一个新对象! - Bergi
https://dev59.com/F1jUa4cB1Zd3GeqPTrLK - AntonB
好的,你特别要求覆盖在实例之间共享的默认值。这不是你想要的吗? - Bergi
你能提供一个解决方案吗?我已经提供了我的全部代码,显然我做错了什么... 我希望每个对象都携带自己的选项。 - AntonB
如果您选择原型方式,请注意在原型上指定实例成员,特别是如果它们是可变的(示例使用字符串,因此是安全的)。您可以在此处阅读更多信息:https://dev59.com/J2Qo5IYBdhLWcg3wbe5K#16063711 - HMR
2个回答

6

函数本身的属性就像静态类成员(是函数的属性,不是它的实例的属性)
函数原型的属性在实例之间不同:

function test(){};
test.prototype = {
    constructor : test,
    first : '',
    second : '',
    third : '',
    init : function(f, s, t){
        this.first = f;
        this.second = s;
        this.third = t;
        return this;
    },
    formatToString : function(){
        return 'first : ' + this.first + ', second : ' + this.second + ', third : ' + this.third;
    }
}
var t1 = new test().init(1, 2, 3);
var t2 = new test().init(4, 5, 6);
console.log(t1.formatToString());//outputs "first : 1, second : 2, third : 3"
console.log(t2.formatToString());//outputs "first : 4, second : 5, third : 6" 

我不确定你所说的“函数属性”是什么意思(OP在哪里使用它们?);以及“函数原型的属性在不同实例之间如何不同”(这与它们应该有的相反)。请您详细说明一下,好吗? - Bergi
在JS中,OOP意义上并不存在"类"这样的概念,所以我使用"函数"来描述通过调用new test()创建的"对象"。也许更好的说法是"类"和"类实例"。在我的示例中,直接访问原型中定义的属性(类成员)(例如t1.first而不是t1.prototype.first),就像访问"类实例"属性一样,而不是像访问"静态" "类"字段那样。 - www0z0k
@www0z0k 是的,用类和实例来描述它们会更清晰,尽管在JavaScript中它并不是真正的类。 - Ruan Mendes

5
你正在使用立即调用函数表达式 (IIFE),就像模块模式所说的那样,但是对于这种情况,你需要多次调用你的 IIFE。这意味着给它一个名称,以便你可以再次调用它,因此从技术上讲,它不再是 IIFE,但出于与 IIFE 相同的原因,它仍然有效。我将继续称之为。 当你调用一个函数时,JavaScript 会为其中的变量和闭包创建一个上下文。只要 IIFE 外部有任何东西引用内部的任何东西,该上下文就会存在。这就是传统模块模式使用 IIFE 的原因:你可以在 IIFE 的上下文中隐藏私有数据和函数。 但由于你只调用了该函数一次,所有模块实例共享相同的上下文。你将模块选项存储在 options 变量中,该变量是共享上下文的一部分,而不是模块的一部分,因此当你在其中一个模块中更新选项时,它会更新所有模块的选项。有时候是你想要的,但在你的情况下不是。 你想做的是为每个模块创建一个新的上下文。这意味着你需要保留 IIFE 的引用,以便可以多次调用它:换句话说,它将不再是匿名函数(甚至不一定是 IIFE)。但这是可行的。下面是一种可能的解决方案:
var moduleContext = function () {
    // default options
    var options = {
        first: "test",
        second: "test2",
        third: "test3"
    };
    // take in useroptions and replace default options
    var module = function(userOptions) {
        if (userOptions != null && userOptions != undefined
            && userOptions != 'undefined') {
            for (var opt in options) {
                if (userOptions.hasOwnProperty(opt)) {
                    options[ opt ] = userOptions[ opt ];
                }
            }
        }
    };

    //prototype
    module.prototype = {
        init: module,
        options: options
    };

    return module;

};

var my_module3 = new (moduleContext())();
my_module3.init({
    first: "Mike",
    second: "Lisa",
    third: "Mary"
});
var my_module2 = new (moduleContext())();
my_module2.init({
    first: "Barry",
    second: "Larry",
    third: "Sam"
});
console.log(my_module2.options, my_module3.options);

这两行代码中的神奇操作发生在new (moduleContext())()。像IIFE一样,moduleContext()函数为模块构造函数设置上下文,然后返回它。new运算符则针对返回的函数进行操作,并以无参数方式(最后一组圆括号)调用它。对于调用moduleContext()的额外圆括号之所以需要,原因与IIFE相同:它们解决了JavaScript解析器中的一些歧义问题。
现在,你的两个模块在两个不同的上下文中被创建。因此,您可以在任何一个模块中设置“common”选项对象(就像您目前正在做的那样),但只会影响该模块上下文中的选项对象。另一个对象则不会受到影响,因此您可以分别设置选项。

如果每次创建实例时都要更改原型,使用原型的意义是什么? - HMR
我承认我所发布的解决方案并不完全符合惯用语或实际需求。从这些角度来看,被接受的答案更好。我发布这个答案有两个原因:一是因为它更接近问题中给出的代码,二是因为它更清楚地说明了那段代码为什么不能实现提问者想要达到的目的。 - The Spooniest
这是有道理的,IIFE 在模块内部被调用一次,因此任何“新”的模块实例都已经使用最后一个模块选项调用,覆盖了任何“新”实例中用户选项。 - AntonB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接