Подтвердить что ты не робот

Простое наследование javascript с использованием шаблона $.extend и модуля

Я уже пару лет размышлял над тем, что люди думают о наследовании с помощью шаблона конструктора шаблона-шаблона и БЕЗ обычного прототипного наследования. Почему программисты не используют шаблон модуля для не-одиночных js-классов? Для меня преимущества следующие:

  • Очень понятный публичный и закрытый охват (легко понять код и api)
  • Не нужно отслеживать указатель 'this' через $.proxy(fn, this) в обратных вызовах
  • Больше нет var = this и т.д. с обработчиками событий и т.д. Всякий раз, когда я вижу 'this', я знаю, что это контекст, который передается в обратный вызов, это НЕ то, что я отслеживаю, чтобы узнать свой объект экземпляр.

Недостатки:

  • Малая деградация
  • Возможна ли опасность "визга пальца" у Дуга Крокфорда?

Рассмотрим это (просто запустите в любой консоли js)

var Animal = function () {
    var publicApi = {
        Name: 'Generic',
        IsAnimal: true,
        AnimalHello: animalHello,
        GetHelloCount:getHelloCount
    };

    var helloCount = 0;

    function animalHello() {
        helloCount++;
        console.log(publicApi.Name + ' says hello (animalHello)');
    }

    function getHelloCount(callback) {
        callback.call(helloCount);
    }

    return publicApi;
};

var Sheep = function (name) {
    var publicApi = {
        Name: name || 'Woolie',
        IsSheep: true,
        SheepHello: sheepHello
    };

    function sheepHello() {
        publicApi.AnimalHello();
        publicApi.GetHelloCount(function() {
            console.log('i (' + publicApi.Name + ') have said hello ' + this + ' times (sheepHello anon callback)');
        });
    }

    publicApi = $.extend(new Animal(), publicApi);
    return publicApi;
};

var sheepie = new Sheep('Sheepie');
var lambie = new Sheep('Lambie');

sheepie.AnimalHello();
sheepie.SheepHello();
lambie.SheepHello();

Мой вопрос в том, каковы недостатки такого подхода, которые я не вижу? Это хороший подход?

Спасибо!

[обновление]

Спасибо за отличные ответы. Хотел бы я дать всем щедрость. Это то, что я искал. В основном, что я думал. Я бы никогда не использовал шаблон модуля для создания более чем нескольких экземпляров чего-то. Обычно только пара. Причина, по которой я думаю, имеет свои преимущества, - это то, что маленькая деградация перформанса, которую вы видите, отбирается в простоте опыта кодирования. У нас есть много кода для написания этих дней. Мы также должны повторно использовать код других людей, и лично я ценю, когда кто-то нашел время, чтобы создать красивый элегантный шаблон, а не догматически придерживаться прототипного наследования, когда это имеет смысл.

4b9b3361

Ответ 1

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

Как упоминалось в сообщении JS - Зачем использовать Prototype?, одна из красавиц прототипа заключается в том, что вам нужно только инициализировать прототипов только один раз, тогда как члены внутри конструктора создаются для каждого экземпляра. Фактически, вы можете напрямую обращаться к прототипу, не создавая новый объект.

Array.prototype.reverse.call([1,2,3,4]);
//=> [4,3,2,1]

function add() {
    //convert arguments into array
    var arr = Array.prototype.slice.call(arguments),
        sum = 0;
    for(var i = 0; i < arr.length; i++) {
        sum += arr[i];
    }

    return sum;
}

add(1,2,3,4,5);
//=> 15

В ваших функциях есть дополнительные накладные расходы, чтобы создать совершенно новый Животные и овцы при каждом вызове конструктора. Некоторые члены, такие как Animal.name, создаются с каждым экземпляром, но мы знаем что Animal.name статично, поэтому было бы лучше создать его экземпляр один раз. Поскольку ваш код подразумевает, что Animal.name должен быть одинаковым для всех животных, легко обновить Animal.name для всего экземпляра, просто обновив Animal.prototype.name, если мы переместим это прототип.

Рассмотрим это

var animals = [];
for(var i = 0; i < 1000; i++) {
    animals.push(new Animal());
}

Функциональное наследование/шаблон модуля

function Animal() {

    return {
      name : 'Generic',
      updateName : function(name) {
          this.name = name;
      }
   }

}


//update all animal names which should be the same
for(var i = 0;i < animals.length; i++) {
    animals[i].updateName('NewName'); //1000 invocations !
}

против. Прототип

Animal.prototype = {
name: 'Generic',
updateName : function(name) {
   this.name = name
};
//update all animal names which should be the same
Animal.prototype.updateName('NewName'); //executed only once :)

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

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

Конечно, пройдя этот маршрут, вам нужно будет отслеживать это.  Это правда, что в вашей реализации есть

  • Нет необходимости отслеживать указатель 'this' через $.proxy(fn, this) в обратных вызовах
  • Нет больше var that = this и т.д. С обработчиками событий и т.д. Всякий раз, когда я вижу 'this', я знаю, что это контекст, который передается в обратный вызов, это НЕ то, что я отслеживаю, чтобы узнать свой экземпляр объекта.

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

Делегирование событий как аналог

Аналогия для повышения производительности с использованием прототипов - это повышение производительности с помощью делегирования событий при манипулировании DOM. Делегирование событий в Javascript

Допустим, у вас большой список продуктов. Yum.

<ul ="grocery-list"> 
    <li>Broccoli</li>
    <li>Milk</li>
    <li>Cheese</li>
    <li>Oreos</li>
    <li>Carrots</li>
    <li>Beef</li>
    <li>Chicken</li>
    <li>Ice Cream</li>
    <li>Pizza</li>
    <li>Apple Pie</li>
</ul>

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

var list = document.getElementById('grocery-list'),
 groceries = list.getElementsByTagName('LI');
//bad esp. when there are too many list elements
for(var i = 0; i < groceries.length; i++) {
    groceries[i].onclick = function() {
        console.log(this.innerHTML);
    }
}

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

//one event handler to manage child elements
 list.onclick = function(e) {
   var target = e.target || e.srcElement;
   if(target.tagName = 'LI') {
       console.log(target.innerHTML);
   }
}

Переписать с использованием комбинации функционального/прототипного наследования

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

var Animal = function () {

    var helloCount = 0;
    var self = this;
    //priviledge methods
    this.AnimalHello = function() {
        helloCount++;
        console.log(self.Name + ' says hello (animalHello)');
    };

    this.GetHelloCount = function (callback) {
        callback.call(null, helloCount);
    }

};

Animal.prototype = {
    Name: 'Generic',
    IsAnimal: true
};

var Sheep = function (name) {

    var sheep = new Animal();
    //use parasitic inheritance to extend sheep
    //http://www.crockford.com/javascript/inheritance.html
    sheep.Name = name || 'Woolie'
    sheep.SheepHello = function() {
        this.AnimalHello();
        var self = this;
        this.GetHelloCount(function(count) {
            console.log('i (' + self.Name + ') have said hello ' + count + ' times (sheepHello anon callback)');
        });
    }

    return sheep;

};

Sheep.prototype = new Animal();
Sheep.prototype.isSheep = true;

var sheepie = new Sheep('Sheepie');
var lambie = new Sheep('Lambie');

sheepie.AnimalHello();
sheepie.SheepHello();
lambie.SheepHello();

Заключение

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

Ответ 2

шаблон конструктора-шаблона-шаблона

Это называется паразитным наследованием или функциональным наследованием.

Для меня преимущества следующие:

  • Очень понятный публичный и закрытый охват (легко понять этот код и api)

То же самое справедливо и для классического шаблона конструктора. Btw, в вашем текущем коде не слишком ясно, должны ли animalHello и getHelloCount быть закрытыми или нет. Определение их права в литерале экспортированного объекта может быть лучше, если вам это нужно.

  • Не нужно отслеживать указатель 'this' через $.proxy(fn, this) в обратных вызовах
  • Больше нет var = this и т.д. с обработчиками событий и т.д. Всякий раз, когда я вижу 'this', я знаю, что это контекст, который передается в обратный вызов, это НЕ то, что я отслеживаю, чтобы узнать свой объект экземпляр.

Это в основном то же самое. Для решения этой проблемы вы используете развод или привязку that. И я не вижу в этом огромного недостатка, поскольку ситуация, когда вы используете объектные "методы" непосредственно как обратные вызовы, довольно редка - и помимо контекста вы часто хотите использовать дополнительные аргументы. Кстати, вы используете ссылку that также в своем коде, там называется publicApi.

Почему программисты не используют шаблон модуля для не-одиночных js-классов?

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

publicApi = $.extend(new Animal(), publicApi);
…
… new Sheep('Sheepie');

Эти части вашего кода также немного запутывают. Вы переписываете переменную с другим объектом здесь, и это происходит в середине вашего кода. Было бы лучше видеть это как "объявление" (вы наследуете родительские свойства здесь!) И помещаете его в начало вашей функции - прямо как var publicApi = $.extend(Animal(), {…});

Кроме того, здесь не следует использовать ключевое слово new. Вы не используете функции в качестве конструкторов, и не хотите создавать экземпляры, наследующие от Animal.prototype (что замедляет ваше выполнение). Кроме того, он смущает людей, которые могут ожидать прототипическое наследование от вызова конструктора с помощью new. Для ясности вы даже можете переименовать функции в makeAnimal и makeSheep.

Что было бы сопоставимым подходом к этому в nodejs?

Этот шаблон проектирования полностью не зависит от среды. Он будет работать в Node.js так же, как на клиенте и в любой другой реализации EcmaScript. Некоторые его аспекты даже не зависят от языка.

Ответ 3

Благодаря вашему подходу вы не сможете переопределить функции и вызвать суперфункцию так удобно.

function foo ()
{
}

foo.prototype.GetValue = function ()
{
        return 1;
}


function Bar ()
{
}

Bar.prototype = new foo();
Bar.prototype.GetValue = function ()
{
    return 2 + foo.prototype.GetValue.apply(this, arguments);
}

Кроме того, в прототипном подходе вы можете обмениваться данными между всеми экземплярами объекта.

function foo ()
{
}
//shared data object is shared among all instance of foo.
foo.prototype.sharedData = {
}

var a = new foo();
var b = new foo();
console.log(a.sharedData === b.sharedData); //returns true
a.sharedData.value = 1;
console.log(b.sharedData.value); //returns 1

Еще одно преимущество прототипа - сохранить память.

function foo ()
{
}

foo.prototype.GetValue = function ()
{
   return 1;
}

var a = new foo();
var b = new foo();
console.log(a.GetValue === b.GetValue); //returns true

В то время как ваш подход,

var a = new Animal();
var b = new Animal();
console.log(a.AnimalHello === b.AnimalHello) //returns false

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

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

function foo ()
{
}
foo.prototype.name = "world";

var a = new foo ();
var b = new foo ();
var c = new foo();
c.name = "bar";

foo.prototype.name = "hello";

console.log(a.name); //returns 'hello'
console.log(b.name); //returns 'hello'
console.log(c.name); //returns 'bar' since has been altered after object creation

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