Прототип ООП с Object.create и именованными конструкторами

Я перешел к Javascript, имея опыт работы с Python и Smalltalk, и я ценю родословную Self и Lisp в этом языке. С ECMAScript5 я хотел попробовать свои силы в прототипном объектно-ориентированном программировании без нового оператора.

Ограничения:

  • необязательный новый оператор для создания классов
  • цепочка прототипов должна быть правильной для instanceof
  • именованные конструкторы для поддержки отладки WebInspector
  • alloc().init() последовательность создания, такая как Objective-C и Python

Вот моя попытка реализации, чтобы соответствовать критериям:

function subclass(Class, Base) {
    "use strict";
    function create(self, args) {
        if (!(self instanceof this))
            self = Object.create(this.prototype);
        var init = self.__init__;
        return init ? init.apply(self, args) : self;
    }
    if (Base instanceof Function) Base = Base.prototype;
    else if (Base===undefined) Base = Object.prototype;

    Class.prototype = Object.create(Base);
    Class.prototype.constructor = Class;
    Class.create = create;

    Class.define = function define(name, fn) { return Class.prototype[name] = fn; };
    Class.define('__name__', Class.name);
    return Class;
}

И, похоже, это работает в простом макете:

function Family(){return Family.create(this, arguments)}
subclass(Family, Object);
Family.define('__init__', function __init__(n){this.name=n; return this;});

function Tribe(){return Tribe.create(this, arguments)}
subclass(Tribe, Family);
function Genus(){return Genus.create(this, arguments)}
subclass(Genus, Tribe);
function Species(){return Species.create(this, arguments)}
subclass(Species, Genus);

Использование класса в качестве фабричной функции:

var dog = Species('dog');
console.assert(dog instanceof Object);
console.assert(dog instanceof Family);
console.assert(dog instanceof Tribe);
console.assert(dog instanceof Genus);
console.assert(dog instanceof Species);

Или используя новый оператор:

var cat = new Species('cat');
console.assert(cat instanceof Object);
console.assert(cat instanceof Family);
console.assert(cat instanceof Tribe);
console.assert(cat instanceof Genus);
console.assert(cat instanceof Species);

console.assert(Object.getPrototypeOf(dog) === Object.getPrototypeOf(cat))

Не упустил ли я из своей реализации необходимые функции прототипа ООП? Существуют ли соглашения или взаимодействия Javascript, для которых я должен внести изменения? Подводя итог, скажите, какие здесь «подводные камни» и есть ли очевидные улучшения?

Я хотел быть DRYer с определениями конструктора, но обнаружил, что свойство name функции недоступно для записи, и именно это поддерживает имена объектов WebKit Inspector. Я смог построить eval, чтобы выполнить то, что хотел, но... фу.


person Shane Holloway    schedule 28.02.2012    source источник
comment
вау, я никогда не видел, чтобы JavaScript использовался таким образом...   -  person Jan Turoň    schedule 29.02.2012
comment
eval опасен, если вы обрабатываете ненадежные данные, но я не понимаю, почему нельзя использовать eval, на самом деле, когда вы делаете это [astring], вы используете какой-то eval, поскольку в js нет ассоциативных массивов.   -  person mpm    schedule 02.03.2012
comment
@camus да, и я даже использовал регулярное выражение /\w+/, чтобы убедиться, что это только допустимое имя метода. Но это похоже на использование кувалды для установки гвоздя — было бы намного элегантнее, если бы было разрешено устанавливать свойство имени функции. В конце концов, Javascript — это язык-прототип...   -  person Shane Holloway    schedule 02.03.2012


Ответы (4)


Редактировать: О, теперь я вижу вопрос. Ответ: нет, вы все правильно поняли. Единственный способ установить имя функции — это использовать объявления функций, то есть во время оценки. Таким образом, вам нужно иметь его в исходном коде (в который eval входит). Ранее я ответил на более простой вопрос, но с той же сутью: Незначительный недостаток с Crockford Prototype Inheritance . Еще один ресурс по этой теме: http://kangax.github.com/nfe/.

Есть предложение использовать свойство displayName, чтобы отделить неизменяемое имя функции от ее внешнего вида в отладчике. Это реализовано в Firefox и некоторых других вещах, и является подставкой для включения в es6, но пока не является частью предварительной спецификации: http://wiki.ecmascript.org/doku.php?id=solman:name_property_of_functions

Вот документ от некоторых людей, работающих над Chrome, на тему именования функций http://querypoint-debugging.googlecode.com/files/NamingJSFunctions.pdf

А вот проблема с хромом, в которой обсуждается, почему он еще не реализован: http://code.google.com/p/chromium/issues/detail?id=17356

На исходный ответ:

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

Во-первых, это простая «наследуемая» функция, которая позволяет вам делать такие вещи, как:

var MyObjCtor = heritable({
  constructor: function MyObj(){ /* ctor stuff */},
  super: SomeCtor,
  prop1: val,
  prop2: val,
  /**etc..*/
});


function heritable(definition){
  var ctor = definition.constructor;
  Object.defineProperty(ctor, 'super', {
    value: definition.super,
    configurable: true,
    writable: true
  });
  ctor.prototype = Object.create(ctor.super.prototype);
  delete definition.super;

  Object.keys(definition).forEach(function(prop){
    var desc = Object.getOwnPropertyDescriptor(definition, prop);
    desc.enumerable = false;
    Object.defineProperty(ctor.prototype, prop, desc);
  });

  function construct(){
    var obj = new (ctor.bind.apply(ctor, [].concat.apply([null], arguments)));
    ctor.super.call(obj);
    return obj;
  }

  construct.prototype = ctor.prototype;

  return construct;
}

Другой заключается в создании структур для использования с libffi (node-ffi). По сути, здесь у вас есть как наследование конструктора, так и наследование прототипа. Вы создаете конструкторы, которые наследуются от конструкторов, которые создают экземпляры структур. https://github.com/Benvie/node-ffi-tools/blob/master/lib/Struct.js

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

person Community    schedule 02.03.2012
comment
объединенные ссылки, примеры кода и обзор кода очень полезны — спасибо. - person Shane Holloway; 08.03.2012

Это может не отвечать на ваш вопрос, и, вероятно, это не все, что вам нужно, но это еще один способ работы с OO JavaScript. Мне это нравится в основном из-за синтаксиса; мне проще грок, но у меня нет твоего бэкграунда.

// ------- Base Animal class -----------------------
var Animal = function(name) {
    this.name = name;
};

Animal.prototype.GetName = function() {
    return this.name;
};
// ------- End Base Animal class -----------------------

// ------- Inherited Dog class -----------------------
var Dog = function(name, breed) { 
    Animal.call(this, name);
    this.breed = breed;
};

Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

Dog.prototype.GetBreed = function() {
  return this.breed; 
};

Dog.prototype.Bark = function() {
    alert(this.GetName() + ' the ' + this.GetBreed() + ' says "Arf!"');
};
// ------- End Inherited Dog class -----------------------

// Usage
var harper = new Dog('Harper', 'miniature pinscher');
harper.Bark();
person JoshNaro    schedule 06.03.2012
comment
Мне нравится такое использование .call для инициализации экземпляра, это очень чисто, но почему вы используете имена методов, начинающиеся с заглавной буквы? В соответствии с соглашениями об именах в JavaScript все имена должны начинаться со строчной буквы, кроме имен конструкторов и некоторых имен хост-объектов. - person Luc125; 07.03.2012
comment
Мне не нравятся соглашения об именах, которые я видел, где почти все написано в camelCase. Я использую PascalCase для своих классов и общедоступных методов и CamelCase для всего остального, чтобы облегчить чтение. - person JoshNaro; 07.03.2012
comment
Я видел эту идиому Javascript ООП в нескольких местах — по сути, это псевдоклассический пример Крокфорда без его вспомогательных методов. К сожалению, мой вариант использования требует, чтобы классы работали правильно как фабричные методы, так и стандартные конструкторы Javascript. - person Shane Holloway; 07.03.2012

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

Я люблю ООП и родом из очень дружественного к ООП прошлого. Мне потребовалось некоторое время, чтобы разобраться с объектами Prototype (представьте себе методы прототипа как статическую функцию... которая работает только с инициализированными объектами). Многие вещи, сделанные в javascript, кажутся очень «неправильными» из-за языка, который использует хорошую безопасность и инкапсуляцию.

Единственная реальная причина использовать «новый» оператор — если вы создаете несколько экземпляров или пытаетесь предотвратить выполнение какой-либо логики до тех пор, пока не будет инициировано событие.

Я знаю, что это не ответ ... это было слишком много, чтобы писать комментарии. Чтобы действительно получить лучшую перспективу, вы должны создать объект и переменные и просмотреть свой DOM... вы поймете, что из-за этого вы можете буквально попасть куда угодно из любого контекста. Я нашел эти статьи очень полезными для заполнения деталей.

Удачи.

ОБНОВЛЕНИЕ

Итак, если вам нужен фабричный метод для создания разных экземпляров одного и того же объекта, вот несколько советов, которые я хотел бы услышать:

  • Пожалуйста, пожалуйста, пожалуйста, забудьте обо всех известных вам правилах объектно-ориентированной инкапсуляции. Я говорю это потому, что при совместном использовании ваших прото-объектов, литеральных объектов и объектов-экземпляров будет так много способов доступа к вещам, которые не будут для вас естественными.
  • Если вы планируете использовать какой-либо тип абстракции или наследования, я настоятельно рекомендую поиграть с DOM. Создайте несколько объектов и найдите их в DOM и посмотрите, что происходит. Вы можете имитировать наследование, используя существующие прототипы (меня убило отсутствие интерфейсов и абстрактных классов для расширения!).
  • Знайте, что все в javascript является объектом... это позволяет вам делать очень классные вещи (передавать функции, возвращать функции, создавать простые методы обратного вызова). Посмотрите на вызовы javascript '.call()', '.apply()' и '.bind()'.

Знание этих двух вещей поможет вам найти решение. Чтобы поддерживать чистоту DOM и следовать хорошей практике разработки javascript, держите глобальное пространство имен в чистоте (ссылка на окно в DOM). Это также заставит вас чувствовать себя «лучше» в отношении всех вещей, которые вы считаете «неправильными» в том, что вы делаете. И будьте последовательны.

Это то, чему я научился методом проб и ошибок. Javascript — отличный и уникальный язык, если вы понимаете его отличие от классических объектно-ориентированных языков. Удачи!

person jjNford    schedule 03.03.2012
comment
Спасибо за понимание. У меня есть конкретный вариант использования, который требует стиля создания класса без нового фабричного метода, но при этом должен быть совместим с ожиданиями javascript в другом месте. - person Shane Holloway; 07.03.2012
comment
@ShaneHolloway Хорошо, круто. Затем вы должны использовать ключевое слово «новое» для создания ваших объектов. Я обновлю свой ответ, чтобы узнать, поможет ли он вам. - person jjNford; 07.03.2012
comment
спасибо за большой опыт и советы. Я согласен с тем, что прототипное программирование немного утомительно для начала. - person Shane Holloway; 08.03.2012

Вы не можете создавать именованные конструкторы без eval. Все, что вы найдете, это конструктор со скрытым свойством с именем, если библиотека реализует пространства имен.

Отвечая на остальную часть вопроса, существует множество библиотек классов javascript. Вы можете написать свою собственную, но это немного запутанно, чтобы понять и реализовать правильную систему наследования, если вы хотите сделать это правильно.

Недавно я написал свою собственную библиотеку классов, пытаясь улучшить все библиотеки: Classful JS. Я думаю, стоит взглянуть, потому что его простой дизайн, использование и некоторые другие улучшения, такие как вызов любого суперметода из подкласса (я не видел ни одной библиотеки, которая может это сделать, все они могут вызывать только суперметод, который переопределение).

person Gabriel Llamas    schedule 06.03.2012