Зачем нужно выносить методы в прототип объекта
Перейти к содержимому

Зачем нужно выносить методы в прототип объекта

  • автор:

Методы прототипов, объекты без свойства __proto__

В первой главе этого раздела мы упоминали, что существуют современные методы работы с прототипами.

Свойство __proto__ считается устаревшим, и по стандарту оно должно поддерживаться только браузерами.

Современные же методы это:

  • Object.create(proto, [descriptors]) – создаёт пустой объект со свойством [[Prototype]] , указанным как proto , и необязательными дескрипторами свойств descriptors .
  • Object.getPrototypeOf(obj) – возвращает свойство [[Prototype]] объекта obj .
  • Object.setPrototypeOf(obj, proto) – устанавливает свойство [[Prototype]] объекта obj как proto .

Эти методы нужно использовать вместо __proto__ .

let animal = < eats: true >; // создаём новый объект с прототипом animal let rabbit = Object.create(animal); alert(rabbit.eats); // true alert(Object.getPrototypeOf(rabbit) === animal); // получаем прототип объекта rabbit Object.setPrototypeOf(rabbit, <>); // заменяем прототип объекта rabbit на <>

У Object.create есть необязательный второй аргумент: дескрипторы свойств. Мы можем добавить дополнительное свойство новому объекту таким образом:

let animal = < eats: true >; let rabbit = Object.create(animal, < jumps: < value: true >>); alert(rabbit.jumps); // true

Формат задания дескрипторов описан в главе Флаги и дескрипторы свойств.

Мы также можем использовать Object.create для «продвинутого» клонирования объекта, более мощного, чем копирование свойств в цикле for..in :

// клон obj c тем же прототипом (с поверхностным копированием свойств) let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

Такой вызов создаёт точную копию объекта obj , включая все свойства: перечисляемые и неперечисляемые, геттеры/сеттеры для свойств – и всё это с правильным свойством [[Prototype]] .

Краткая история

Если пересчитать все способы управления прототипом, то их будет много! И многие из них делают одно и то же!

В силу исторических причин.

  • Свойство «prototype» функции-конструктора существует с совсем давних времён.
  • Позднее, в 2012 году, в стандарте появился метод Object.create . Это давало возможность создавать объекты с указанным прототипом, но не позволяло устанавливать/получать его. Тогда браузеры реализовали нестандартный аксессор __proto__ , который позволил устанавливать/получать прототип в любое время.
  • Позднее, в 2015 году, в стандарт были добавлены Object.setPrototypeOf и Object.getPrototypeOf, заменяющие собой аксессор __proto__ , который упоминается в Приложении Б стандарта, которое не обязательно к поддержке в небраузерных окружениях. При этом де-факто __proto__ всё ещё поддерживается везде.

В итоге сейчас у нас есть все эти способы для работы с прототипом.

Почему же __proto__ был заменён на функции getPrototypeOf/setPrototypeOf ? Читайте далее, чтобы узнать ответ.

Не меняйте [[Prototype]] существующих объектов, если важна скорость

Технически мы можем установить/получить [[Prototype]] в любое время. Но обычно мы устанавливаем прототип только раз во время создания объекта, а после не меняем: rabbit наследует от animal , и это не изменится.

И JavaScript движки хорошо оптимизированы для этого. Изменение прототипа «на лету» с помощью Object.setPrototypeOf или obj.__proto__= – очень медленная операция, которая ломает внутренние оптимизации для операций доступа к свойствам объекта. Так что лучше избегайте этого кроме тех случаев, когда вы знаете, что делаете, или же когда скорость JavaScript для вас не имеет никакого значения.

«Простейший» объект

Как мы знаем, объекты можно использовать как ассоциативные массивы для хранения пар ключ/значение.

…Но если мы попробуем хранить созданные пользователями ключи (например, словари с пользовательским вводом), мы можем заметить интересный сбой: все ключи работают как ожидается, за исключением «__proto__» .

Посмотрите на пример:

let obj = <>; let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // [object Object], не "some value"!

Если пользователь введёт __proto__ , присвоение проигнорируется!

И это не должно удивлять нас. Свойство __proto__ особенное: оно должно быть либо объектом, либо null , а строка не может стать прототипом.

Но мы не намеревались реализовывать такое поведение, не так ли? Мы хотим хранить пары ключ/значение, и ключ с именем «__proto__» не был сохранён надлежащим образом. Так что это ошибка!

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

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

Неожиданные вещи могут случаться также при присвоении свойства toString , которое по умолчанию функция, и других свойств, которые тоже на самом деле являются встроенными методами.

Как же избежать проблемы?

Во-первых, мы можем переключиться на использование коллекции Map , и тогда всё будет в порядке.

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

Свойство __proto__ – не обычное, а аксессор, заданный в Object.prototype :

Так что при чтении или установке obj.__proto__ вызывается соответствующий геттер/сеттер из прототипа obj , и именно он устанавливает/получает свойство [[Prototype]] .

Как было сказано в начале этой секции учебника, __proto__ – это способ доступа к свойству [[Prototype]] , это не само свойство [[Prototype]] .

Теперь, если мы хотим использовать объект как ассоциативный массив, мы можем сделать это с помощью небольшого трюка:

let obj = Object.create(null); let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // "some value"

Object.create(null) создаёт пустой объект без прототипа ( [[Prototype]] будет null ):

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

Мы можем назвать такой объект «простейшим» или «чистым словарным объектом», потому что он ещё проще, чем обычные объекты <. >.

Недостаток в том, что у таких объектов не будет встроенных методов объекта, таких как toString :

let obj = Object.create(null); alert(obj); // Ошибка (no toString)

…Но обычно это нормально для ассоциативных массивов.

Обратите внимание, что большая часть методов, связанных с объектами, имеют вид Object.something(. ) . К примеру, Object.keys(obj) . Подобные методы не находятся в прототипе, так что они продолжат работать для таких объектов:

let chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见"; alert(Object.keys(chineseDictionary)); // hello,bye

Итого

Современные способы установки и прямого доступа к прототипу это:

  • Object.create(proto[, descriptors]) – создаёт пустой объект со свойством [[Prototype]] , указанным как proto (может быть null ), и необязательными дескрипторами свойств.
  • Object.getPrototypeOf(obj) – возвращает свойство [[Prototype]] объекта obj (то же самое, что и геттер __proto__ ).
  • Object.setPrototypeOf(obj, proto) – устанавливает свойство [[Prototype]] объекта obj как proto (то же самое, что и сеттер __proto__ ).

Встроенный геттер/сеттер __proto__ не безопасен, если мы хотим использовать созданные пользователями ключи в объекте. Как минимум потому, что пользователь может ввести «__proto__» как ключ, от чего может возникнуть ошибка. Если повезёт – последствия будут лёгкими, но, вообще говоря, они непредсказуемы.

Так что мы можем использовать либо Object.create(null) для создания «простейшего» объекта, либо использовать коллекцию Map .

Кроме этого, Object.create даёт нам лёгкий способ создать поверхностную копию объекта со всеми дескрипторами:

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

Мы также ясно увидели, что __proto__ – это геттер/сеттер для свойства [[Prototype]] , и находится он в Object.prototype , как и другие методы.

Мы можем создавать объекты без прототипов с помощью Object.create(null) . Такие объекты можно использовать как «чистые словари», у них нет проблем с использованием строки «__proto__» в качестве ключа.

  • Object.keys(obj) / Object.values(obj) / Object.entries(obj) – возвращают массив всех перечисляемых собственных строковых ключей/значений/пар ключ-значение.
  • Object.getOwnPropertySymbols(obj) – возвращает массив всех собственных символьных ключей.
  • Object.getOwnPropertyNames(obj) – возвращает массив всех собственных строковых ключей.
  • Reflect.ownKeys(obj) – возвращает массив всех собственных ключей.
  • obj.hasOwnProperty(key): возвращает true , если у obj есть собственное (не унаследованное) свойство с именем key .

Все методы, которые возвращают свойства объектов (такие как Object.keys и другие), возвращают «собственные» свойства. Если мы хотим получить и унаследованные, можно воспользоваться циклом for..in .

Задачи

Добавьте toString в словарь

важность: 5

Имеется объект dictionary , созданный с помощью Object.create(null) для хранения любых пар ключ/значение .

Добавьте ему метод dictionary.toString() , который должен возвращать список ключей, разделённых запятой. Ваш toString не должен выводиться при итерации объекта с помощью цикла for..in .

Вот так это должно работать:

let dictionary = Object.create(null); // ваш код, который добавляет метод dictionary.toString // добавляем немного данных dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // здесь __proto__ -- это обычный ключ // только apple и __proto__ выведены в цикле for(let key in dictionary) < alert(key); // "apple", затем "__proto__" >// ваш метод toString в действии alert(dictionary); // "apple,__proto__"

В методе можно получить все перечисляемые ключи с помощью Object.keys и вывести их список.

Чтобы сделать toString неперечисляемым, давайте определим его, используя дескриптор свойства. Синтаксис Object.create позволяет нам добавить в объект дескрипторы свойств как второй аргумент.

let dictionary = Object.create(null, < toString: < // определяем свойство toString value() < // значение -- это функция return Object.keys(this).join(); >> >); dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // apple и __proto__ выведены в цикле for(let key in dictionary) < alert(key); // "apple", затем "__proto__" >// список свойств, разделённых запятой, выведен с помощью toString alert(dictionary); // "apple,__proto__"

Когда мы создаём свойство с помощью дескриптора, все флаги по умолчанию имеют значение false . Таким образом, в коде выше dictionary.toString – неперечисляемое свойство.

Смотрите главу Флаги и дескрипторы свойств для ознакомления.

Прототип объекта

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/prototype-inheritance.

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

Связующим звеном выступает специальное свойство __proto__ .

Прототип proto

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

Свойство __proto__ доступно во всех браузерах, кроме IE10-, а в более старых IE оно, конечно же, тоже есть, но напрямую к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее.

Пример кода (кроме IE10-):

var animal = < eats: true >; var rabbit = < jumps: true >; rabbit.__proto__ = animal; // в rabbit можно найти оба свойства alert( rabbit.jumps ); // true alert( rabbit.eats ); // true
  1. Первый alert здесь работает очевидным образом – он выводит свойство jumps объекта rabbit .
  2. Второй alert хочет вывести rabbit.eats , ищет его в самом объекте rabbit , не находит – и продолжает поиск в объекте rabbit.__proto__ , то есть, в данном случае, в animal .

Иллюстрация происходящего при чтении rabbit.eats (поиск идёт снизу вверх):

Объект, на который указывает ссылка __proto__ , называется «прототипом». В данном случае получилось, что animal является прототипом для rabbit .

Также говорят, что объект rabbit «прототипно наследует» от animal .

Обратим внимание – прототип используется исключительно при чтении. Запись значения, например, rabbit.eats = value или удаление delete rabbit.eats – работает напрямую с объектом.

В примере ниже мы записываем свойство в сам rabbit , после чего alert перестаёт брать его у прототипа, а берёт уже из самого объекта:

var animal = < eats: true >; var rabbit = < jumps: true, eats: false >; rabbit.__proto__ = animal; alert( rabbit.eats ); // false, свойство взято из rabbit

Другими словами, прототип – это «резервное хранилище свойств и методов» объекта, автоматически используемое при поиске.

У объекта, который является __proto__ , может быть свой __proto__ , у того – свой, и так далее. При этом свойства будут искаться по цепочке.

Ссылка proto в спецификации

Если вы будете читать спецификацию ECMAScript – свойство __proto__ обозначено в ней как [[Prototype]] .

Двойные квадратные скобки здесь важны, чтобы не перепутать его с совсем другим свойством, которое называется prototype , и которое мы рассмотрим позже.

Метод hasOwnProperty

Обычный цикл for..in не делает различия между свойствами объекта и его прототипа.

Он перебирает всё, например:

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < alert( key + " = " + rabbit[key] ); // выводит и "eats" и "jumps" >

Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе.

Вызов obj.hasOwnProperty(prop) возвращает true , если свойство prop принадлежит самому объекту obj , иначе false .

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; alert( rabbit.hasOwnProperty('jumps') ); // true: jumps принадлежит rabbit alert( rabbit.hasOwnProperty('eats') ); // false: eats не принадлежит

Для того, чтобы перебрать свойства самого объекта, достаточно профильтровать key через hasOwnProperty :

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < if (!rabbit.hasOwnProperty(key)) continue; // пропустить "не свои" свойства alert( key + " = " + rabbit[key] ); // выводит только "jumps" >

Object.create(null)

Зачастую объекты используют для хранения произвольных значений по ключу, как коллекцию:

var data = <>; data.text = "Привет"; data.age = 35; // . 

При дальнейшем поиске в этой коллекции мы найдём не только text и age , но и встроенные функции:

var data = <>; alert(data.toString); // функция, хотя мы её туда не записывали

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

Чтобы этого избежать, мы можем исключать свойства, не принадлежащие самому объекту:

var data = <>; // выведет toString только если оно записано в сам объект alert(data.hasOwnProperty('toString') ? data.toString : undefined);

Однако, есть путь и проще:

var data = Object.create(null); data.text = "Привет"; alert(data.text); // Привет alert(data.toString); // undefined

Объект, создаваемый при помощи Object.create(null) не имеет прототипа, а значит в нём нет лишних свойств. Для коллекции – как раз то, что надо.

Методы для работы с proto

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

Чтение: Object.getPrototypeOf(obj) Возвращает obj.__proto__ (кроме IE8-) Запись: Object.setPrototypeOf(obj, proto) Устанавливает obj.__proto__ = proto (кроме IE10-).

Кроме того, есть ещё один вспомогательный метод:

Создание объекта с прототипом: Object.create(proto, descriptors) Создаёт пустой объект с __proto__ , равным первому аргументу (кроме IE8-), второй необязательный аргумент может содержать дескрипторы свойств.

Итого

  • В JavaScript есть встроенное «наследование» между объектами при помощи специального свойства __proto__ .
  • При установке свойства rabbit.__proto__ = animal говорят, что объект animal будет «прототипом» rabbit .
  • При чтении свойства из объекта, если его в нём нет, оно ищется в __proto__ . Прототип задействуется только при чтении свойства. Операции присвоения obj.prop = или удаления delete obj.prop совершаются всегда над самим объектом obj .

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

В современных браузерах есть методы для работы с прототипом:

  • Object.getPrototypeOf(obj) (кроме IE8-)
  • Object.setPrototypeOf(obj, proto) (кроме IE10-)
  • Object.create(proto, descriptors) (кроме IE8-)

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

Также мы рассмотрим, как свойство __proto__ используется внутри самого языка JavaScript и как организовать классы с его помощью.

Когда использовать методы в конструкторе, а когда — в прототипе?

Я не понимаю, в каких случаях необходимо метод объекта объявлять в самом конструкторе, а когда — в прототипе.

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

function A() < this.method = function() < alert('Метод конструктора'); >> A.prototype.method2 = function() < alert('Метод прототипа'); >let object = new A();

Известно ведь, что в новом стандарте ES6 с синтаксисом классов и вовсе нет методов в конструкторе.

  • Вопрос задан более трёх лет назад
  • 1172 просмотра

2 комментария

Простой 2 комментария

lazalu68

«в новом стандарте ES6 с синтаксисом классов и вовсе нет методов в конструкторе» — конечно есть.

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

«Когда использовать методы в конструкторе, а когда — в прототипе?» — и в том и в другом случае ответ один: когда это нужно. Если не видите нужды в чем-то, то не делайте этого; все просто. Нужно забить память объявлением однотипных функций? Объявляйте методы в конструкторе. Не нужно? Объявляйте в прототипе.

Хорошего примера объявления методов в конструкторе, к сожалению, не могу привести.

Зачем нужен prototype?

Зачем существует прототипирование ? Почему нельзя просто добавлять в объект новые функции obj.newFunc = function()<> , а затем использовать их ? Разве это не то же самое что и obj.prototype.newFunc = function() <> ? Объясните в чём разница и зачем нужен prototype ?

Отслеживать
задан 13 авг 2015 в 15:14
Станислав Далинин Станислав Далинин
377 1 1 золотой знак 9 9 серебряных знаков 20 20 бронзовых знаков

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

13 авг 2015 в 15:17
Всё равно плохо доходит.
13 авг 2015 в 15:23

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Для того, что бы понять, зачем нужен prototype , надо понять, что такое классы и зачем они нужны: Пример ‘из учебника’

function Animal (name) < this.name = name >var dog = new Animal("Sharky"); 

Что произошло? Мы создали новый объект dog , у которого есть свойство name , сделали мы это с помощью функции-конструктора Animal .
В данном случае Animal — класс, dog — экземпляр

Теперь к prototype , допустим, мы хотим, что бы у всех экземпляров класс Animal был метод walk() . Добавлять этот метод к каждому экземпляру после создания, ни какого редактора не хватит, поэтому нам и нужен prototype .
Вкратце — в прототипе ( prototype ) содержатся все унаследованные методы/свойства, которое нам выдает класс при создании

function Animal (name) < this.name = name; // для каждого экземпляра свое имя >Animal.prototype.walk = function () < // общая для всех экземпляров alert("I can walk"); >var dog = new Animal("dog"); var cat = new Animal("cat"); alert(dog.name); // dog alert(cat.name); // cat // и dog, и cat имеют метод walk dog.walk(); cat.walk(); // I can walk 

Prototype — это хранилище общих методов и свойств для всех экземпляров одного класса. Что бы лучше понять, почитай про ООП в js( тут например )

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *