Прототип ОО в JavaScript - PullRequest
       32

Прототип ОО в JavaScript

43 голосов
/ 29 июня 2011

TL; DR:

Нужны ли фабрики / конструкторы в ОО? Можем ли мы сделать переключение парадигмы и полностью отбросить их?

BackStory:

В последнее время я занимался созданием прототипов ОО в JavaScript и обнаружил, что 99% ОО, выполняемых в JavaScript, навязывают ему классические ОО-модели.

Мое мнение о прототипе ОО состоит в том, что оно включает две вещи. Статический прототип методов (и статических данных) и привязка данных. Нам не нужны фабрики или конструкторы.

В JavaScript это литералы объектов, содержащие функции и Object.create.

Это означало бы, что мы можем моделировать все как статический проект / прототип и абстракцию привязки данных, которые предпочтительно подключаются прямо к базе данных в стиле документа. То есть объекты извлекаются из базы данных и создаются путем клонирования прототипа с данными. Это означало бы, что нет логики конструктора, нет фабрик, нет new.

Пример кода:

Псевдо-пример будет:

var Entity = Object.create(EventEmitter, {
    addComponent: {
        value: function _addComponent(component) {
            if (this[component.type] !== undefined) {
                this.removeComponent(this[component.type]);
            }

            _.each(_.functions(component), (function _bind(f) {
                component[f] = component[f].bind(this);
            }).bind(this));

            component.bindEvents();

            Object.defineProperty(this, component.type, {
                value: component,
                configurable: true
            });

            this.emit("component:add", this, component);
        }
    },
    removeComponent: {
        value: function _removeComponent(component) {
            component = component.type || component;

            delete this[component];

            this.emit("component:remove", this, component);
        }
    }
}

var entity = Object.create(Entity, toProperties(jsonStore.get(id)))

Незначительное объяснение:

Конкретный код является многословным, потому что ES5 является многословным. Entity выше - это план / прототип. Любой фактический объект с данными будет создан с использованием Object.create(Entity, {...}).

Фактические данные (в данном случае компоненты) напрямую загружаются из хранилища JSON и вводятся непосредственно в вызов Object.create. Разумеется, аналогичный шаблон применяется для создания компонентов, и в базе данных хранятся только те свойства, которые передают Object.hasOwnProperty.

Когда сущность создается впервые, она создается с пустым {}

Актуальные вопросы:

Теперь мои актуальные вопросы

  • Примеры с открытым исходным кодом JS-прототипа OO?
  • Это хорошая идея?
  • Соответствует ли это идеям и концепциям, заложенным в прототип ООП?
  • Не будет ли использовать какие-либо конструкторы / фабрики функции кусать меня в задницу где-нибудь? Можем ли мы сойти с рук, не используя конструкторы. Существуют ли какие-либо ограничения при использовании вышеуказанной методологии, когда нам потребуются фабрики для их преодоления.

Ответы [ 4 ]

26 голосов
/ 30 июня 2011

Я не думаю, что логика конструктора / фабрики вообще необходима, пока вы меняете свое отношение к объектно-ориентированному программированию.В моем недавнем исследовании этой темы я обнаружил, что прототипическое наследование больше подходит для определения набора функций, которые используют конкретные данные.Это не чуждое понятие для тех, кто обучается классическому наследованию, но загвоздка в том, что эти «родительские» объекты не определяют данные, с которыми нужно работать.

var animal = {
    walk: function()
    {
        var i = 0,
            s = '';
        for (; i < this.legs; i++)
        {
            s += 'step ';
        }

        console.log(s);
    },
    speak: function()
    {
        console.log(this.favoriteWord);
    }
}

var myLion = Object.create(animal);
myLion.legs = 4;
myLion.favoriteWord = 'woof';

Итак, в приведенном выше примеремы создаем функциональность, которая сопровождает животное, а затем создаем объект, обладающий этой функциональностью, вместе с данными, необходимыми для выполнения действий.Это неудобно и странно для любого, кто привык к классическому наследованию в течение любого отрезка времени.В ней нет ни одной из теплых размытостей публичной / частной / защищенной иерархии видимости членов, и я буду первым, кто признает, что заставляет меня нервничать.

Кроме того, мой первый инстинкт, когда я вижуПриведенная выше инициализация объекта myLion заключается в создании фабрики для животных, чтобы я мог создавать львов, тигров и медведей (о мой) простым вызовом функции.И, я думаю, для большинства программистов это естественный способ мышления - многословность приведенного выше кода безобразна и, похоже, лишена элегантности.Я не решил, является ли это просто классическим обучением, или это фактическая ошибка вышеуказанного метода.

Теперь о наследовании.

Я всегда понимал наследование в JavaScript, чтобыбыть труднымНавигация по входам и выходам цепочки прототипов не совсем понятна.Пока вы не используете его с Object.create, что исключает перенаправление всех ключевых слов на основе функций из уравнения.

Скажем, мы хотели расширить вышеуказанный объект animal и сделать человека.

var human = Object.create(animal)
human.think = function()
{
    console.log('Hmmmm...');
}

var myHuman = Object.create(human);
myHuman.legs = 2;
myHuman.favoriteWord = 'Hello';

Это создает объект, имеющий human в качестве прототипа, который, в свою очередь, имеет animal в качестве прототипа.Достаточно просто.Нет неправильного направления, нет «нового объекта с прототипом, равным прототипу функции».Просто простое наследование прототипа.Это просто и понятно.Полиморфизм тоже прост.

human.speak = function()
{
    console.log(this.favoriteWord + ', dudes');
}

Из-за того, как работает цепочка прототипов, myHuman.speak будет найден в human, прежде чем он найден в animal, и поэтому наш человек вместо этого является серферомпросто скучного старого животного.

Итак, в заключение ( TLDR ):

Функциональность псевдоклассического конструктора была отчасти привязана к JavaScript, чтобы эти программисты могли сделать эти программистытренироваться в классическом ООП более комфортно.Это не , в любом случае, необходимо, но это означает отказ от классических понятий, таких как видимость членов и (тавтологически) конструкторы.

В ответ вы получаете гибкость и простоту.Вы можете создавать «классы» на лету - каждый объект сам является шаблоном для других объектов.Установка значений для дочерних объектов не повлияет на прототип этих объектов (то есть, если бы я использовал var child = Object.create(myHuman), а затем установил child.walk = 'not yet', animal.walk не изменилось бы - действительно, протестируйте его).

Простотанаследования честно ошеломляет.Я много читал о наследовании в JavaScript и написал много строк кода, пытаясь понять это.Но это действительно сводится к объектам, наследуемым от других объектов .Это так просто, и все ключевое слово new запутывает это.

Эту гибкость трудно использовать в полной мере, и я уверен, что мне еще предстоит это сделать, но этотам и интересно ориентироваться.Я думаю, что большая часть причины того, что он не использовался для большого проекта, заключается в том, что он просто не понимается так, как мог бы, и, ИМХО, мы заперты в классических шаблонах наследования, которые мы все изучили, когда мыпреподавали C ++, Java и т. д.

Edit

Я думаю, что я выступил с хорошими аргументами против конструкторов.Но мой аргумент против фабрик нечеткий.

После дальнейшего созерцания, во время которого я несколько раз шлепнулся к обеим сторонам забора, я пришел к выводу, что фабрики также не нужны. Если бы animal (выше) была дана другая функция initialize, было бы тривиально создать и инициализировать новый объект, который наследуется от animal.

var myDog = Object.create(animal);
myDog.initialize(4, 'Meow');

Новый объект, инициализирован и готов к использованию.

@ Raynos - Вы совершенно обалденный снайпер. Я должен готовиться к 5 дням, ничего не делая продуктивно.

11 голосов
/ 30 июня 2011

Согласно вашему комментарию, вопрос в основном "нужны ли знания конструктора?" Я чувствую, что это так.

Примером игрушки будет хранение частичных данных. На заданном наборе данных в памяти при сохранении я могу выбрать только сохранение определенных элементов (либо в целях эффективности, либо в целях обеспечения согласованности данных, например, значения по сути бесполезны после их сохранения). Давайте возьмем сеанс, где я сохраняю имя пользователя и количество нажатий на кнопку справки (из-за отсутствия лучшего примера). Когда я сохраняю это в моем примере, я не использую количество кликов, так как я сохраняю его в памяти сейчас, и в следующий раз я загружаю данные (в следующий раз, когда пользователь входит в систему, подключается или что-то еще), я инициализирую значение с нуля (предположительно до 0). Этот конкретный вариант использования является хорошим кандидатом для логики конструктора.

Ааа, но вы всегда можете просто вставить это в статический прототип: Object.create({name:'Bob', clicks:0}); Конечно, в этом случае. Но что, если значение сначала не всегда равно 0, а скорее требует вычислений? Uummmm, скажем, пользователи стареют в секундах (при условии, что мы сохранили имя и DOB). Опять же, элемент, который мало используется, сохраняется, так как в любом случае его нужно будет пересчитать при извлечении. Итак, как вы храните возраст пользователя в статическом прототипе?

Очевидный ответ - логика конструктора / инициализатора.

Есть еще много сценариев, хотя я не чувствую, что идея тесно связана с js oop или каким-либо конкретным языком. Необходимость создания сущности логика присуща тому, как компьютерные системы моделируют мир. Иногда элементы, которые мы храним, будут простым извлечением и внедрением в схему, подобную оболочке-прототипу, а иногда значения являются динамическими, и их необходимо будет инициализировать.

UPDATE

Хорошо, я собираюсь попробовать более реальный пример, и чтобы избежать путаницы, предположим, что у меня нет базы данных и мне не нужно сохранять какие-либо данные. Допустим, я делаю пасьянс-сервер. Каждая новая игра будет (естественно) новым экземпляром прототипа Game. Мне ясно, что здесь требуется логика инициализации (и многое из этого):

Мне, например, понадобится в каждом игровом экземпляре не просто статичная / жестко закодированная колода карт, а случайная колода. Если бы он был статичным, пользователь каждый раз играл бы в одну и ту же игру, что явно нехорошо.

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

Как это сделать со статическим Object.create()?

2 голосов
/ 30 июня 2011

Краткий ответ на ваш вопрос "Нужны ли фабрики / конструкторы в прототипах ОО?"нет.Фабрики / Конструкторы служат только одной цели: инициализировать вновь созданный объект (экземпляр) в определенное состояние.

Это, как говорится, часто используется, потому что некоторым объектам нужен какой-то код инициализации.

Давайте использовать предоставленный вами код сущности на основе компонентов.Типичная сущность - это просто набор компонентов и несколько свойств:

var BaseEntity = Object.create({},
{
    /* Collection of all the Entity's components */
    components:
    {
        value: {}
    }

    /* Unique identifier for the entity instance */
    , id:
    {
        value: new Date().getTime()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    /* Use for debugging */
    , createdTime:
    {
        value: new Date()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    , removeComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }

    , addComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }
});

Теперь следующий код создаст новые сущности на основе 'BaseEntity'

function CreateEntity()
{
    var obj = Object.create(BaseEntity);

    //Output the resulting object's information for debugging
    console.log("[" + obj.id + "] " + obj.createdTime + "\n");

    return obj;
}

Кажется достаточно простым,пока вы не перейдете к ссылке на свойства:

setTimeout(CreateEntity, 1000);
setTimeout(CreateEntity, 2000);
setTimeout(CreateEntity, 3000);

вывод:

[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)

Так почему же это так?Ответ прост: из-за наследования на основе прототипа.Когда мы создавали объекты, не было никакого кода для установки свойств id и createdTime на фактическом экземпляре, как это обычно делается в конструкторах / фабриках.В результате при обращении к свойству оно извлекается из цепочки прототипов, которая в итоге становится единым значением для всех сущностей.

Аргументом этого является то, что Object.create () должен быть переданВторой параметр для установки этого значения.Мой ответ был бы просто: разве это не то же самое, что вызов конструктора или использование фабрики?Это просто еще один способ установки состояния объекта.

Теперь в вашей реализации, где вы рассматриваете (и по праву так) все прототипы как набор статических методов и свойств, вы инициализируете объект, присваивая значениясвойства данных из источника данных.Возможно, он не использует new или какой-либо тип фабрики, но это код инициализации.

Подводя итог: в прототипе JavaScript ООП - new не требуется - фабрики не нужны - код инициализации обычнонужен, что обычно делается через new, фабрики или другую реализацию, которую вы не хотите допустить, это инициализация объекта

2 голосов
/ 30 июня 2011

Пример статически клонируемого «Типа»:

var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Теперь мы могли бы клонировать этот тип в другом месте, да? Конечно, нам нужно использовать var myType = Object.Create(MyType), но тогда мы закончили, да? Теперь мы можем просто myType.size, и это размер вещи. Или мы могли бы прочитать цвет или изменить его и т. Д. Мы не создали конструктор или что-то еще, верно?

Если вы сказали, что там нет конструктора, вы ошибаетесь. Позвольте мне показать вам, где находится конструктор:

// The following var definition is the constructor
var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Потому что мы уже ушли и создали все, что хотели, и мы уже все определили. Это все, что делает конструктор. Таким образом, даже если мы только клонируем / используем статические объекты (что я и вижу в приведенных выше фрагментах), у нас все еще есть конструктор. Просто статический конструктор. Определив тип, мы определили конструктор. Альтернатива этой модели строительства объекта:

var MyType = {}
MyType.size = Sizes.large

Но в конечном итоге вы захотите использовать Object.Create (MyType), и когда вы это сделаете, вы будете использовать статический объект для создания целевого объекта. И тогда он становится таким же, как в предыдущем примере.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...