Почему определение свойств в прототипе считается антипаттерном

Я часто вижу этот шаблон для определения объектов javascript.

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return "Person called "+this.name;
};

А в этой статье говорится, что добавление свойств непосредственно в объект-прототип считается анти- шаблон.

Исходя из «классических» языков, основанных на классах, необходимость определять свойства отдельно от методов звучит не совсем правильно, тем более в javascript, где метод должен быть просто свойством со значением функции (правильно ли я здесь?)

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


comment
В нем говорится о свойствах данных, которые, как я полагаю, относятся к переменным. Я не думаю, что там что-то упоминается о добавлении функций к прототипу.   -  person Waleed Khan    schedule 10.08.2012
comment
Что сказал Арханас. Точная цитата: тело класса может содержать только методы, а не свойства данных. Прототипы, имеющие свойства данных, обычно считаются анти-шаблоном, так что это всего лишь соблюдение наилучшей практики. Другими словами, под свойствами данных автор подразумевает свойства, которые не являются методами/функциями.   -  person ruakh    schedule 10.08.2012
comment
Ваш вопрос основан на недоразумении, не знаю, что делать. Я тоже не помню, чтобы данные помещались в прототип. Я не могу отредактировать ваш вопрос, чтобы данные были в прототипе, потому что это не тот шаблон, который я часто вижу.   -  person Esailija    schedule 10.08.2012
comment
Меня интересует, почему использование нефункциональных свойств считается анти-шаблоном. Существует очевидная проблема со свойством объекта, которое, возможно, непреднамеренно разделяется между экземплярами, но размещение свойства-примитива в прототипе является эффективным способом предоставления значения свойства по умолчанию.   -  person Tim Down    schedule 10.08.2012
comment
Привет, Тим, если я правильно понимаю ответы, которые были даны мне здесь, если вы поместите примитивное свойство в прототип, каждый экземпляр будет использовать один и тот же примитив (вид статического члена в java)... как переменная класса.   -  person opensas    schedule 11.08.2012


Ответы (6)


В обычных объектно-ориентированных языках у вас есть определение класса, описывающее члены, методы и конструктор.

В JS определение "класса" (на самом деле это не класс, как в других языках... иногда используется термин псевдокласс) - это сам конструктор. Если ваш объект параметризован name, имеет смысл написать

function Person(name) {
    this.name = name;
}

то есть свойство name должно быть установлено в конструкторе.

Конечно, вы можете написать

function Person(name) {
    this.name = name;
    this.describe = function() { ... };
}

и он будет работать так, как вы ожидаете.

Однако в этом случае вы создаете отдельный экземпляр метода при каждом вызове конструктора.

С другой стороны, здесь:

Person.prototype.describe = function () {
    return "Person called "+this.name;
};

вы определяете метод только один раз. Все экземпляры Person получат указатель (называемый __proto__ и недоступный программисту в большинстве браузеров) на Person.prototype. Итак, если вы позвоните

var myPerson = new Person();
myPerson.describe();

это будет работать, потому что JS ищет члены объекта непосредственно в объекте, затем в его прототипе и т. д. вплоть до Object.prototype.

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

person Imp    schedule 10.08.2012
comment
большое спасибо, я не понимал, что каждое определение метода из конструктора будет другим экземпляром функции... - person opensas; 10.08.2012

В этом коде нет ничего плохого. Предположительно имеется в виду:

function Person(name) {
    this.name = name;
}
Person.prototype.age = 15; //<= adding a hardcoded property to the prototype

Теперь вы увидите это:

var pete = new Person('Pete'), mary = new Person('Mary');
pete.age; //=> 15
mary.age  //=> 15

И чаще всего это не то, чего вы хотите. Свойства, назначенные прототипу конструктора, являются общими для всех экземпляров, свойства, назначенные в конструкторе (this.name), специфичны для экземпляра.

person KooiInc    schedule 10.08.2012

Как говорит arxanas, в статье упоминаются свойства данных.

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

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


Пример. Следующее приводит к неправильному поведению:

function Set() {

}

// shared between instances
// each instance adds values to **the same** array
Set.prototype.elements = [];

Set.prototype.add = function(x) {
   this.elements.push(x);
};

Так должно быть:

function Set() {
    // each instance gets its own array
    this.elements = [];
}

Set.prototype.add = function(x) {
   this.elements.push(x);
};

Подвести итог:

  • Добавьте в прототип свойства, которые должны быть общими для всех экземпляров.
  • Назначьте конкретные данные экземпляра внутри функции-конструктора.
person Felix Kling    schedule 10.08.2012

Так же, как arxanas написал в своем комментарии. Свойства данных в прототипе более или менее похожи на переменные уровня класса в традиционном ООП. И они не используются ежедневно, если только у вас нет особой потребности. Это все.

person Tomasz Zieliński    schedule 10.08.2012

Объявление свойств прототипа вовсе не является антипаттерном. Когда я смотрю на объект prototype, я думаю: «Это то, что прототипный объект этого типа имеет для данных и методов».

Другие предостерегают от присвоения свойствам ссылочного значения в прототипе, например: Foo.prototype.bar = []; --- потому что массивы и объекты являются ссылочными типами. Ссылочный тип неизменяем, поэтому каждый экземпляр «класса» относится к одному и тому же массиву или объекту. Просто установите для них значение null в прототипе, а затем присвойте им значение в конструкторе.

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

Это становится особенно полезным, если вы создаете разделяемую библиотеку, для которой требуется документация.

Рассмотрим этот пример:

/**
 * class Point
 * 
 * A simple X-Y coordinate class
 *
 * new Point(x, y)
 * - x (Number): X coordinate
 * - y (Number): Y coordinate
 *
 * Creates a new Point object
 **/
function Point(x, y) {
    /**
     * Point#x -> Number
     *
     * The X or horizontal coordinate
     **/
    this.x = x;

    /**
     * Point#y -> Number
     *
     * The Y or vertical coordinate
     **/
    this.y = y;
}

Point.prototype = {
    constructor: Point,

    /**
     * Point#isAbove(other) -> bool
     * - other (Point): The point to compare this to
     *
     * Checks to see if this point is above another
     **/
    isAbove: function(other) {
        return this.y > other.y;
    }
};

(Формат документации: PDoc)

Просто читать документацию здесь немного неудобно, потому что информация о свойствах x и y встроена в функцию-конструктор. Сравните это с «анти-шаблоном» включения этих свойств в прототип:

/**
 * class Point
 * 
 * A simple X-Y coordinate class
 *
 * new Point(x, y)
 * - x (Number): X coordinate
 * - y (Number): Y coordinate
 *
 * Creates a new Point object
 **/
function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype = {

    /**
     * Point#x -> Number
     *
     * The X or horizontal coordinate
     **/
    x: 0,

    /**
     * Point#y -> Number
     *
     * The Y or vertical coordinate
     **/
    y: 0,

    constructor: Point,

    /**
     * Point#isAbove(other) -> bool
     * - other (Point): The point to compare this to
     *
     * Checks to see if this point is above another
     **/
    isAbove: function(other) {
        return this.y > other.y;
    }

};

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

В прототипе есть все, и он является каноническим источником информации о том, что есть у "прототипа" Point объекта как для методов , так и для данных.

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

person Greg Burghardt    schedule 28.10.2014

Возможно, это связано с тем, что изменение объекта, которым вы не владеете, считается антишаблоном.

Это означает, что если вы не создавали объект, вы не владеете этим объектом. Включая:

  • Собственные объекты (объект, массив и т. д.)
  • DOM-объекты
  • Объекты объектной модели браузера (BOM) (например, window)
  • Объекты библиотеки

Источник Maintainable Javascript by Nicholas C. Zakas

person Remi    schedule 23.06.2020