这三种JavaScript模块模式实现有什么区别?

15
我曾看过以下三个代码块作为JavaScript模块模式的示例。它们有哪些区别,我该选择哪种模式呢?

模式1

function Person(firstName, lastName) {
    var firstName = firstName;
    var lastName = lastName;

    this.fullName = function () {
        return firstName + ' ' + lastName;
    };

    this.changeFirstName = function (name) {
        firstName = name;
    };
};

var jordan = new Person('Jordan', 'Parmer');

模式2

function person (firstName, lastName) { 
    return {
        fullName: function () {
            return firstName + ' ' + lastName;
        },

        changeFirstName: function (name) {
            firstName = name;
        }
    };
};

var jordan = person('Jordan', 'Parmer');

模式3

var person_factory = (function () {
    var firstName = '';
    var lastName = '';

    var module = function() {
        return {
            set_firstName: function (name) {
                               firstName = name;
                           },
            set_lastName: function (name) {
                              lastName = name;
                          },
            fullName: function () {
                          return firstName + ' ' + lastName;
                      }

        };
    };

    return module;
})();

var jordan = person_factory();

据我所知,JavaScript社区普遍认为模式3是最佳选择。它与前两种模式有何不同?在我看来,这三种模式都可以用来封装变量和函数。

注意:这篇文章实际上并没有回答问题,我认为它不是重复的。


你不知道这三个部分会产生三种根本不同的行为,对吗? - Bergi
没有所谓的“最好”。如果JavaScript社区认为模式3是最好的,那么JavaScript社区是错误的。它们是三种不同的方法,每一种都有它们的用途。 - I Hate Lazy
@Bergi - 这就是问题的本质。有什么区别? - Jordan Parmer
可能是重复的问题:Javascript:模块模式 vs 构造函数/原型模式? - Peter O.
3个回答

13

我认为它们更像是对象实例化模式,而不是模块模式。就个人而言,我不会推荐你提供的任何示例。主要因为我认为重新分配函数参数除了方法重载外没有其他好处。让我们回到看一下在JavaScript中创建对象的两种方式:

原型和new操作符

这是在JavaScript中创建对象最常见的方式。它与模式1密切相关,但将函数附加到对象原型上,而不是每次创建一个新函数:

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
};

Person.prototype.fullName = function () {
    return this.firstName + ' ' + this.lastName;
};

Person.prototype.changeFirstName = function (name) {
    this.firstName = name;
};

var jordan = new Person('Jordan', 'Parmer');

jordan.changeFirstName('John');

Object.create和工厂函数

ECMAScript 5引入了Object.create,允许以不同的方式实例化对象。你可以使用Object.create(obj)来设置原型,而不是使用new运算符。

var Person =  {
    fullName : function () {
        return this.firstName + ' ' + this.lastName;
    },

    changeFirstName : function (name) {
        this.firstName = name;
    }
}

var jordan = Object.create(Person);
jordan.firstName = 'Jordan';
jordan.lastName = 'Parmer';

jordan.changeFirstName('John');

正如您所看到的,您将不得不手动分配属性。这就是为什么创建一个工厂函数为您进行初始属性分配是有意义的:

function createPerson(firstName, lastName) {
    var instance = Object.create(Person);
    instance.firstName = firstName;
    instance.lastName = lastName;
    return instance;
}

var jordan = createPerson('Jordan', 'Parmer');

像这样的事情,我总是要参考《理解JavaScript面向对象编程》,它是关于JavaScript面向对象编程最好的文章之一。

我还想指出我的小型库UberProto,在研究JavaScript的继承机制后创建了它。它提供了Object.create语义作为更方便的封装:

var Person = Proto.extend({
    init : function(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    },

    fullName : function () {
        return this.firstName + ' ' + this.lastName;
    },

    changeFirstName : function (name) {
        this.firstName = name;
    }
});

var jordan = Person.create('Jordan', 'Parmer');

最终问题并不在于“社区”似乎更偏爱什么,而在于了解语言提供了什么来完成某项任务(在您的情况下是创建新对象)。从那里开始,您可以更好地决定哪种方法更适合您。

模块模式

看起来似乎有些混淆模块模式和对象创建。即使它们看起来相似,但其职责不同。由于JavaScript 仅具有函数作用域,因此模块用于封装功能(而不是意外创建全局变量或名称冲突等)。最常见的方法是将功能包装在自执行函数中:

(function(window, undefined) {
})(this);

既然它只是一个函数,你最好在最后返回一些东西(你的API)

var Person = (function(window, undefined) {
    var MyPerson = function(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    };

    MyPerson.prototype.fullName = function () {
        return this.firstName + ' ' + this.lastName;
    };

    MyPerson.prototype.changeFirstName = function (name) {
        this.firstName = name;
    };

    return MyPerson;
})(this);

这就是JS中的模块。它们引入了一个包装函数(在JavaScript中等效于新作用域),并且(可选地)返回一个对象,该对象是模块API。


这两个链接将#3称为模块模式。有什么区别?http://briancray.com/posts/javascript-module-pattern和http://macwright.org/2012/06/04/the-module-pattern.html - Jordan Parmer
我更新了我的回答。差异并不重要。基本上,通常归结为我在编辑中所描述的内容。 - Daff

4
首先,正如 @Daff 已经提到的,这并不是所有模块模式。让我们看一下它们的区别:

模式1 vs 模式2

您可以忽略无用的行。

var firstName = firstName;
var lastName = lastName;

从模式1中可以看出,函数参数已经是本地作用域变量了,就像你在模式2的代码中看到的那样。

显然,这两个函数非常相似。它们都创建了一个闭包,该闭包仅由(公开的)fullNamechangeFirstName函数访问这两个本地变量。区别在于实例化时发生的事情。

  • 在模式2中,您只返回一个继承自Object.prototype的对象(文字字面量)。
  • 在模式1中,您使用称为“构造函数”的函数调用new关键字(也要正确大写)。这将创建一个继承自Person.prototype的对象,在其中可以放置其他方法或所有实例都共享的默认属性。

还有其他变体的构造函数模式。它们可能倾向于对象上的[公共]属性,并将所有方法放在原型上-可以混合使用。请参见此答案,了解模拟基于类的继承的工作原理。

何时使用什么?通常首选原型模式,特别是如果您可能希望扩展所有Person实例的功能-甚至可能不来自同一模块。当然,模式1也有用例,特别是对于不需要继承的单例。

模式3

...现在实际上是模块模式,使用闭包创建静态私有变量。从闭包中导出的内容实际上并不重要,它可以是任何工厂/构造函数/对象文字-仍然是“模块模式”。

当然,在您的模式2中的闭包也可以被认为是使用了“模块模式”,但它的目的是创建一个实例,因此我不会使用这个术语。更好的例子是揭示原型模式或使用模块模式的闭包扩展已有对象-专注于代码模块化。

在您的情况下,该模块导出一个构造函数,该构造函数返回一个对象以访问静态变量。玩弄它,您可以执行以下操作:

var jordan = person_factory();
jordan.set_firstname("Jordan");
var pete = person_factory();
pete.set_firstname("Pete");
var guyFawkes = person_factory();
guyFawkes.set_lastname("Fawkes");

console.log(jordan.fullname()); // "Pete Fawkes"

不确定是否预料到了这种情况。如果是,那么为了获取访问器函数而添加的额外构造函数对我来说似乎有点无用。


+1 哦,非常好的答案。这实际上解决了我正在产生的很多困惑。由于我的编程背景是基于C语言的,我在原型设计方面有些混乱。 - Jordan Parmer

2
“哪个是最好的?”并不是一个有效的问题,因为它们各有优劣,提供不同的功能和权衡。使用其中一种或多种(或者都不用)取决于您选择如何构建程序。
模式 #1 是 JS 对“类”的传统解释。
它允许 prototyping,这与 C 类语言中的 inheritance 完全不同,不要混淆两者。 Prototype 更像其他语言中的 public static 属性/方法。 原型方法也不能访问实例变量(即没有附加到 this 上的变量)。
var MyClass = function (args) { this.thing = args; };
MyClass.prototype.static_public_property = "that";
MyClass.prototype.static_public_method   = function () { console.log(this.thing); };

var myInstance = new MyClass("bob");
myInstance.static_public_method();

模式 #2 创建一个单一对象的单个实例,没有隐式继承。

var MyConstructor = function (args) {
    var private_property = 123,
        private_method = function () { return private_property; },

        public_interface = {
            public_method : function () { return private_method(); },
            public_property : 456
        };

    return public_interface;
};


var myInstance = MyConstructor(789);

没有继承,每个实例都会得到每个函数/变量的新副本
如果你正在处理的对象不会在每个页面上有数十万个实例,那么这是完全可行的。

第3种模式类似于第2种模式,但您正在构建构造函数并可以包含相当于private static方法(您必须每次传递参数,您必须收集返回语句,如果函数旨在返回一个值,而不是直接修改对象或数组,因为这些属性/方法没有访问实例级数据/功能的能力,尽管实例构造函数具有所有“静态”功能的访问权限)。
这里的实际好处是更低的内存占用,因为每个实例都有对这些函数的引用,而不是它们自己的副本

var PrivateStaticConstructor = function (private_static_args) {
    var private_static_props = private_static_args,
        private_static_method = function (args) { return doStuff(args); },

        constructor_function = function (private_args) {
            var private_props = private_args,
                private_method = function (args) { return private_static_method(args); },
                public_prop = 123,
                public_method = function (args) { return private_method(args); },

                public_interface = {
                    public_prop   : public_prop,
                    public_method : public_method
                };

            return public_interface;
        };

    return constructor_function;
};


var InstanceConstructor = PrivateStaticConstructor(123),
    myInstance = InstanceConstructor(456);

这些都在做非常不同的事情。


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