Javascript, когда использовать прототипы

93

Хотелось бы понять, когда уместно использовать методы прототипа в js. Следует ли их всегда использовать? Или бывают случаи, когда их использование нежелательно и / или приводит к снижению производительности?

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

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

opl
источник

Ответы:

134

Прототипы - это оптимизация .

Отличным примером их правильного использования является библиотека jQuery. Каждый раз, когда вы получаете объект jQuery с помощью $('.someClass'), этот объект имеет десятки «методов». Библиотека может добиться этого, вернув объект:

return {
   show: function() { ... },
   hide: function() { ... },
   css: function() { ... },
   animate: function() { ... },
   // etc...
};

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

Вместо этого эти методы определены в прототипе, и все объекты jQuery «наследуют» этот прототип, чтобы получить все эти методы с очень небольшими затратами времени выполнения.

Одна жизненно важная часть того, как jQuery делает это правильно, заключается в том, что это скрыто от программиста. Это рассматривается исключительно как оптимизация, а не как то, о чем вы должны беспокоиться при использовании библиотеки.

Проблема с JavaScript заключается в том, что функции открытого конструктора требуют, чтобы вызывающий не забыл поставить перед ними префикс, newиначе они обычно не работают. Для этого нет веской причины. jQuery $делает все правильно, скрывая эту бессмыслицу за обычной функцией , поэтому вам не нужно заботиться о том, как реализованы объекты.

Чтобы вы могли легко создать объект с указанным прототипом, ECMAScript 5 включает стандартную функцию Object.create. Его сильно упрощенная версия выглядела бы так:

Object.create = function(prototype) {
    var Type = function () {};
    Type.prototype = prototype;
    return new Type();
};

Он просто позаботится о том, чтобы написать функцию-конструктор и затем вызвать ее с помощью new.

Когда бы вы избегали прототипов?

Полезно сравнить с популярными объектно-ориентированными языками, такими как Java и C #. Они поддерживают два типа наследования:

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

В JavaScript прототипное наследование - это своего рода наследование реализации . Таким образом, в тех ситуациях, когда (в C # или Java) вы должны были бы унаследовать от базового класса, чтобы получить поведение по умолчанию, в которое вы затем вносите небольшие изменения через переопределения, тогда в JavaScript прототипное наследование имеет смысл.

Однако, если вы находитесь в ситуации, когда вы использовали бы интерфейсы на C # или Java, вам не нужна какая-либо конкретная языковая функция в JavaScript. Нет необходимости явно объявлять что-то, что представляет интерфейс, и нет необходимости отмечать объекты как «реализующие» этот интерфейс:

var duck = {
    quack: function() { ... }
};

duck.quack(); // we're satisfied it's a duck!

Другими словами, если каждый «тип» объекта имеет свои собственные определения «методов», тогда нет никакого смысла в наследовании от прототипа. После этого это зависит от того, сколько экземпляров вы выделяете каждого типа. Но во многих модульных конструкциях есть только один экземпляр данного типа.

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

Дэниел Эрвикер
источник
3
Хорошее объяснение. Согласитесь ли вы, что если вы считаете прототипы оптимизацией, то их всегда можно использовать для улучшения вашего кода? Мне интересно, есть ли случаи, когда использование прототипов не имеет смысла или фактически приводит к снижению производительности.
opl
В своем продолжении вы упоминаете, что «это зависит от того, сколько экземпляров каждого типа вы выделяете». Но в приведенном вами примере прототипы не используются. Где понятие выделения экземпляра (вы бы все еще использовали здесь «новый»)? Также: скажем, у метода шарлатана был параметр - будет ли каждый вызов duck.quack (param) вызывать создание нового объекта в памяти (возможно, это не имеет значения, имеет ли он параметр или нет)?
opl
3
1. Я имел в виду, что если существует большое количество экземпляров одного типа утки, то имеет смысл изменить пример так, чтобы quackфункция находилась в прототипе, с которым связаны многие экземпляры утки. 2. Синтаксис литерала объекта { ... }создает экземпляр (нет необходимости newс ним использовать ). 3. Вызов любой функции JS вызывает создание в памяти хотя бы одного объекта - он называется argumentsобъектом и сохраняет аргументы, переданные в вызове: developer.mozilla.org/en/JavaScript/Reference/…
Дэниел Эрвикер,
Спасибо, я принял твой ответ. Но я все еще немного запутался в вашем пункте (1): я не понимаю, что вы подразумеваете под «большим количеством экземпляров одного вида уток». Как вы сказали в (3), каждый раз, когда вы вызываете JS-функцию, в памяти создается один объект - поэтому, даже если у вас есть только один тип утки, разве вы не будете выделять память каждый раз, когда вы вызываете функцию утки (в в каком случае всегда имеет смысл использовать прототип)?
opl
11
+1 Сравнение с jQuery было первым ясным и кратким объяснением того, когда и зачем использовать прототипы, которые я прочитал. Большое спасибо.
GFoley83
46

Вы должны использовать прототипы, если хотите объявить «нестатический» метод объекта.

var myObject = function () {

};

myObject.prototype.getA = function (){
  alert("A");
};

myObject.getB = function (){
  alert("B");
};

myObject.getB();  // This works fine

myObject.getA();  // Error!

var myPrototypeCopy = new myObject();
myPrototypeCopy.getA();  // This works, too.
КитсКеллехер
источник
@keatsKelleher, но мы можем создать нестатический метод для объекта, просто определив метод внутри функции конструктора, используя thisпример, this.getA = function(){alert("A")}верно?
Амр Лабиб
17

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

Скажем, у вас есть базовый Car()класс / объект.

function Car() {
    // do some car stuff
}

затем вы создаете несколько Car()экземпляров.

var volvo = new Car(),
    saab = new Car();

Теперь вы знаете, что каждая машина должна будет двигаться, включаться и т. Д. Вместо того, чтобы прикреплять метод непосредственно к Car()классу (который занимает память на каждый созданный экземпляр), вы можете вместо этого прикрепить методы к прототипу (создавая только методы Once), тем самым предоставляя доступ к этим методам как new, так volvoи saab.

// just mapping for less typing
Car.fn = Car.prototype;

Car.fn.drive = function () {
    console.log("they see me rollin'");
};
Car.fn.honk = function () {
    console.log("HONK!!!");
}

volvo.honk();
// => HONK!!!
saab.drive();
// => they see me rollin'
Хеллатана
источник
2
на самом деле это неверно. volvo.honk () не будет работать, потому что вы полностью заменили объект-прототип, а не расширили его. Если бы вы сделали что-то подобное, все работало бы так, как вы ожидаете: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // 'HONK'
29er,
1
@ 29er - в том, как я написал этот пример, вы правы. Порядок имеет значение. Если бы я оставил этот пример как есть, то Car.prototype = { ... }должен был бы прийти до вызова a, new Car()как показано в этом jsfiddle: jsfiddle.net/mxacA . Что касается вашего аргумента, это был бы правильный способ сделать это: jsfiddle.net/Embnp . Забавно, я не помню, чтобы я отвечал на этот вопрос =)
hellatan
@hellatan, вы можете исправить это, установив конструктор: Car в значение, поскольку вы перезаписали свойство прототипа литералом объекта.
Джош Бедо
@josh спасибо, что указали на это. Я обновил свой ответ, чтобы не перезаписывать прототип литералом объекта, как это должно было быть с самого начала.
hellatan
12

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

Изменение методов в объектах-прототипах или добавление методов мгновенно изменяет природу всех экземпляров соответствующего типа (ов).

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

Заостренный
источник
поэтому, когда я создаю новый объект с помощью методов-прототипов (через ключевое слово new), тогда этот объект не получает новую копию каждой функции (просто своего рода указатель)? Если это так, почему бы вам не использовать прототип?
opl
как @marcel, d'oh ... =)
hellatan
@opi да, ты прав - копии не делалось. Вместо этого символы (имена свойств) в объекте-прототипе просто как бы естественным образом «присутствуют» как виртуальные части каждого объекта-экземпляра. Единственная причина, по которой люди не захотят возиться с этим, - это случаи, когда объекты недолговечны и отличаются друг от друга, или когда не так много «поведения», которым можно было бы поделиться.
Pointy
3

Если я объясню в терминах, основанных на классе, тогда Person - это класс, walk () - это метод прототипа. Таким образом, walk () будет существовать только после того, как вы создадите с его помощью экземпляр нового объекта.

Поэтому, если вы хотите создать копии объекта, такого как Person, u может создать много пользователей, Prototype - хорошее решение, поскольку он экономит память путем совместного использования / наследования одной и той же копии функции для каждого объекта в памяти.

А вот статика в таком сценарии не очень помогает.

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

// its instance method and can access objects data data 
Person.prototype.walk = function(){
alert("person has started walking.");
}
// its like static method
Person.ProcessPerson = function(Person p){
alert("Persons name is = " + p.name);
}

var userOne = new Person();
var userTwo = new Person();

//Call instance methods
userOne.walk();

//Call static methods
Person.ProcessPerson(userTwo);

Таким образом, это больше похоже на метод экземпляра. Подход объекта подобен статическим методам.

https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

Анил Намде
источник