Методы прототипов JavaScript без __proto__
Современные методы Object.create(), Object.getPrototypeOf() и Object.setPrototypeOf() заменяют устаревший __proto__ в JavaScript.
Каждый объект JavaScript связан с другим объектом, называемым его прототипом, и поиск свойств выполняется по этой цепочке. Старым способом чтения или изменения этой связи было специальное свойство __proto__, но оно имеет реальные проблемы: это унаследованный акцессор (а не обычный слот данных), он ведёт себя непоследовательно в качестве ключа объекта, и его можно использовать для повреждения объектов. Современный JavaScript заменяет его явными, предсказуемыми методами конструктора Object.
В этой главе рассматриваются стандартные методы прототипов — Object.create(), Object.getPrototypeOf() и Object.setPrototypeOf() — а также вспомогательные методы для инспекции: Object.keys(), Object.values(), Object.entries() и Object.hasOwn(). Затем рассматривается наиболее полезный случай — объект без прототипа: безопасный «словарь» для произвольных ключей.
Для более широкого понимания работы цепочки прототипов смотрите Прототипное наследование и не только.
Чтение и установка прототипа
Используйте Object.getPrototypeOf() для чтения прототипа объекта и Object.setPrototypeOf() для его изменения. Это стандартизированные замены для чтения и записи __proto__.
Object.create(proto) создаёт совершенно новый объект, прототипом которого является именно proto. Это самый чистый способ создать объект с выбранным прототипом без использования __proto__ вообще.
Почему следует избегать __proto__
__proto__ — это геттер/сеттер, определённый на Object.prototype, а не свойство, которое живёт на вашем объекте. Это различие приводит к двум практическим проблемам:
- Он может давать сбои или вести себя странно в качестве ключа данных. Поскольку
obj.__proto__ = valueзапускает сеттер, вы не можете надёжно хранить ключ буквально с именем"__proto__"на обычном объекте — присвоение не-объекта молча игнорируется, а присвоение объекта меняет прототип вместо добавления ключа. - Это известная поверхность атаки («загрязнение прототипа»). Код, копирующий недоверенные ключи на объект, может быть обманут и записать в
__proto__, загрязняяObject.prototypeдля всей программы.
Стандартизированные методы явно выражают намерение: getPrototypeOf/setPrototypeOf изменяют прототип, тогда как обычная запись свойств никогда его не затрагивает.
Объекты без прототипа
Object.create(null) создаёт объект, прототип которого равен null. Он ничего не наследует — ни toString, ни hasOwnProperty, ни акцессор __proto__.
Проблема словаря, которую это решает
Распространённый паттерн — использование простого объекта как отображения строковых ключей в значения. Проблема в том, что обычный {} уже наследует ключи от Object.prototype, поэтому предоставляемые пользователем ключи вроде "constructor", "toString" или "__proto__" конфликтуют с унаследованными именами и нарушают поиск значений.
Объект с null-прототипом не имеет унаследованных ключей, поэтому каждый ключ ведёт себя именно так, как записано — включая "__proto__":
Именно поэтому объекты с null-прототипом являются безопасными словарями для недоверенных ключей. (Встроенный Map — ещё один хороший вариант, позволяющий использовать не-строковые ключи.)
Добавление методов в объект с null-прототипом
Поскольку прототипа для наследования нет, методы назначаются непосредственно как собственные свойства.
Итерация ключей, значений и записей
Object.keys(), Object.values() и Object.entries() возвращают массивы собственных перечисляемых свойств объекта. Принципиально важно, что они игнорируют цепочку прототипов, поэтому одинаково работают на обычных объектах и объектах с null-прототипом. Подробнее см. Object.keys, values, entries.
Одна оговорка для объектов с null-прототипом: у них нет унаследованного toString, поэтому передача такого объекта напрямую в шаблонные литералы или String() вызовет ошибку. Используйте для итерации вспомогательные методы Object.* (выше), а не полагайтесь на автоматическое преобразование в string.
Проверка собственных свойств с помощью Object.hasOwn()
Для проверки, является ли ключ собственным свойством объекта (а не унаследованным), предпочитайте Object.hasOwn(). Это современная замена obj.hasOwnProperty(), которая работает даже на объектах с null-прототипом, у которых метод hasOwnProperty вообще отсутствует.
Композиция с помощью Object.assign()
Когда вы хотите, чтобы один объект получил поведение нескольких других, композиция зачастую понятнее, чем единственная цепочка наследования: объект может иметь только один прототип, но Object.assign(target, ...sources) может объединить методы из множества источников, копируя их собственные перечисляемые свойства в целевой объект.
Object.assign() также копирует данные в объект с null-прототипом, создавая составной словарь без унаследованной поверхности:
Оговорка о поверхностном копировании: Object.assign() копирует значения свойств, а не глубокие клоны. Object и array-значения копируются по ссылке, поэтому источник и цель совместно используют одни и те же вложенные объекты.
Для глубокого, независимого копирования используйте вместо этого structuredClone(source).
Лучшие практики
- Используйте стандартизированные методы, а не
__proto__. Прибегайте кObject.create(),Object.getPrototypeOf()иObject.setPrototypeOf(). Относитесь к__proto__как к устаревшему акцессору, которого следует избегать в своём коде. - Избегайте изменения прототипов после создания. Установка выбранного прототипа при создании через
Object.create(proto)чище и понятнее, чем последующее изменение черезObject.setPrototypeOf(). - Используйте
Object.create(null)(илиMap) для словарей с недоверенными ключами. Это устраняет унаследованные ключи и предотвращает неожиданное загрязнение прототипа. - Предпочитайте
Object.hasOwn()методуobj.hasOwnProperty(). Он короче, безопаснее в вызове и работает на объектах с null-прототипом. - Используйте композицию через
Object.assign(), когда вам нужно поведение из нескольких источников — просто помните, что это поверхностное копирование.
Для связанных материалов смотрите Методы объектов и «this» и Флаги и дескрипторы свойств.
Заключение
Современные методы прототипов Object.* дают вам явный, предсказуемый контроль над цепочкой прототипов, заменяя причудливый акцессор __proto__. Object.create(null) создаёт чистый объект без прототипа, идеально подходящий для словарей, а Object.keys/values/entries, Object.hasOwn() и Object.assign() позволяют безопасно инспектировать и составлять объекты независимо от их прототипа.