В чем разница между этими тремя реализациями шаблонов модулей в JavaScript?

Я видел следующие три блока кода в качестве примеров шаблона модуля 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. Как это отличается от первых двух? Мне кажется, что все три шаблона могут использоваться для инкапсуляции переменных и функций.

ПРИМЕЧАНИЕ: Этот пост на самом деле не отвечает на вопрос, и я не считаю его дубликатом.

Ответы

Ответ 1

Я не рассматриваю их шаблоны модулей, но больше шаблонов создания объектов. Лично я бы не рекомендовал ни один из ваших примеров. В основном потому, что я думаю, что переназначение аргументов функции для чего-либо другого, кроме перегрузки метода, не очень хорошо. Давайте вернемся назад и рассмотрим два способа создания объектов в JavaScript:

Protoypes и оператор 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 и factory функция

В ECMAScript 5 представлен Object.create, который позволяет использовать другой способ создания объектов. Вместо использования оператора new вы используете Object.create(obj) для установки Prototype.

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');

Как вы можете видеть, вам придется назначать свои свойства вручную. Вот почему имеет смысл создать функцию factory, которая выполняет для вас первоначальное назначение свойств:

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

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

Как всегда с такими вещами я должен ссылаться на Понимание JavaScript OOP, который является одной из лучших статей по объектно-ориентированному программированию 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');

В конце концов, речь идет не о том, что "сообщество", по-видимому, предпочитает, а больше понимает, что язык обеспечивает для достижения определенной задачи (в вашем случае создавая новые obejcts). Оттуда вы можете решить намного лучше, какой вы предпочитаете.

Шаблоны модулей

Кажется, что есть некоторая путаница с модульными шаблонами и созданием объекта. Даже если он выглядит похожим, у него разные обязанности. Поскольку 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-интерфейсом модулей.

Ответ 2

Во-первых, как уже упоминалось @Daff, это не все шаблоны модулей. Давайте рассмотрим различия:

Образец 1 против шаблона 2

Вы можете опустить бесполезные строки

var firstName = firstName;
var lastName = lastName;

из шаблона 1. Аргументы функции - это уже локальные переменные, как вы можете видеть в вашем шаблоне-2-code.

Очевидно, что функции очень похожи. Оба создают замыкание по этим двум локальным переменным, к которым имеют доступ только функции (подвергаемые) fullName и changeFirstName. Разница заключается в том, что происходит при создании экземпляра.

  • В шаблоне 2 вы просто возвращаете объект (литерал), который наследует от Object.prototype.
  • В шаблоне 1 вы используете new ключевое слово с функцией, которая называется "конструктором" (а также правильно заглавной), Это создаст объект, который наследует от Person.prototype, где вы можете разместить другие методы или свойства по умолчанию, которые будут распространяться все экземпляры.

Существуют и другие варианты шаблонов конструктора . Они могут использовать [общедоступные] свойства объектов, а также использовать все методы на прототипе - вы можете их смешивать. См. этот ответ о том, как работает эмуляция наследования на основе классов.

Когда использовать что? Шаблон прототипа обычно предпочтительнее, особенно если вы захотите расширить функциональность всех экземпляров Person - возможно, даже не из того же модуля. Конечно, есть примеры использования шаблона 1, особенно для синглтонов, которые не нуждаются в наследовании.

Образец 3

... теперь фактически является шаблоном модуля, используя закрытие для создания статических частных переменных. То, что вы экспортируете из закрытия, фактически не имеет значения, оно может быть любым литералом типа fabric/constructor/object, все еще являющимся "шаблоном модуля".

Конечно, замыкание в вашем шаблоне 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"

Не уверен, ожидалось ли это. Если это так, дополнительный конструктор, чтобы получить функции доступа, кажется мне бесполезным.

Ответ 3

"Что лучше?" на самом деле это не вопрос. Они все делают разные вещи, приходят с различными компромиссами и предлагают разные преимущества.
Использование одного или другого или всех трех (или ни одного) сводится к тому, как вы решили разрабатывать свои программы.

Образец № 1 - традиционный подход JS к классам.

Он допускает prototyping, который на самом деле не следует путать с inheritance в языках типа C.
Прототипирование больше похоже на 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 (вы должны передавать аргументы, 100% времени, и вы должны собирать возвращаемые операторы, если функция предназначена для возврата значения, а не для непосредственного изменения объекта или массива, поскольку эти реквизиты/методы не имеют доступа к данным/функциональность, несмотря на то, что экземпляр-конструктор имеет доступ ко всем "статическим" функциям).
Практическое преимущество здесь - более низкий объем памяти, так как каждый экземпляр ссылается на эти функции, а не на их собственную копию.

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);

Все это очень разные вещи.