В первой главе этого раздела мы упомянули, что существуют современные методы настройки прототипа.
Установка или чтение прототипа с помощью obj.__proto__
считается устаревшим и несколько устаревшим (перенесено в так называемое «Приложение B» стандарта JavaScript, предназначенное только для браузеров).
Современные методы получения/установки прототипа:
Object.getPrototypeOf(obj) – возвращает [[Prototype]]
объекта obj
.
Object.setPrototypeOf(obj, proto) – устанавливает [[Prototype]]
объекта obj
значение proto
.
Единственное использование __proto__
, которое не осуждается, — это использование свойства при создании нового объекта: { __proto__: ... }
.
Хотя для этого тоже есть специальный метод:
Object.create(proto[, descriptors]) – создает пустой объект с заданным proto
как [[Prototype]]
и необязательными дескрипторами свойств.
Например:
пусть животное = { ест: правда }; // создаем новый объект с животным в качестве прототипа пусть кролик = Object.create(животное); // то же, что {__proto__: животное} оповещение(кролик.ест); // истинный alert(Object.getPrototypeOf(кролик) === животное); // истинный Object.setPrototypeOf(кролик, {}); // меняем прототип кролика на {}
Метод Object.create
немного более мощный, поскольку у него есть необязательный второй аргумент: дескрипторы свойств.
Здесь мы можем предоставить дополнительные свойства новому объекту, например:
пусть животное = { ест: правда }; пусть кролик = Object.create(animal, { прыжки: { значение: правда } }); оповещение(кролик.прыжки); // истинный
Дескрипторы имеют тот же формат, что описан в главе «Флаги свойств и дескрипторы».
Мы можем использовать Object.create
для более мощного клонирования объекта, чем копирование свойств в for..in
:
пусть клон = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Этот вызов создает по-настоящему точную копию obj
, включая все свойства: перечисляемые и неперечислимые, свойства данных и методы установки/получания — все, и с правильным [[Prototype]]
.
Существует так много способов управления [[Prototype]]
. Как это произошло? Почему?
Это по историческим причинам.
Прототипическое наследование было в языке с самого его зарождения, но способы управления им со временем развивались.
Свойство prototype
функции-конструктора работало с очень древних времен. Это самый старый способ создания объектов с заданным прототипом.
Позже, в 2012 году, Object.create
появился в стандарте. Он давал возможность создавать объекты с заданным прототипом, но не давал возможности его получить/установить. В некоторых браузерах реализован нестандартный метод доступа __proto__
, который позволяет пользователю получить/установить прототип в любое время, чтобы предоставить разработчикам больше гибкости.
Позже, в 2015 году, в стандарт были добавлены Object.setPrototypeOf
и Object.getPrototypeOf
для выполнения тех же функций, что и __proto__
. Поскольку __proto__
де-факто был реализован повсюду, он был признан устаревшим и попал в Приложение B стандарта, то есть: необязательно для небраузерных сред.
Позже, в 2022 году, было официально разрешено использовать __proto__
в объектных литералах {...}
(вынесено из Приложения B), но не в качестве метода получения/установки obj.__proto__
(все еще в Приложении B).
Почему __proto__
был заменен функциями getPrototypeOf/setPrototypeOf
?
Почему __proto__
частично реабилитирован и разрешено его использование в {...}
, но не в качестве метода получения/установки?
Это интересный вопрос, требующий от нас понять, почему __proto__
плох.
И скоро мы получим ответ.
Не меняйте [[Prototype]]
на существующих объектах, если скорость имеет значение.
Технически мы можем получить/установить [[Prototype]]
в любое время. Но обычно мы устанавливаем его только один раз во время создания объекта и больше не изменяем: rabbit
наследует от animal
, и это не изменится.
И движки JavaScript хорошо оптимизированы для этого. Изменение прототипа «на лету» с помощью Object.setPrototypeOf
или obj.__proto__=
— очень медленная операция, поскольку она нарушает внутреннюю оптимизацию операций доступа к свойствам объекта. Так что избегайте этого, если вы не знаете, что делаете, или если скорость JavaScript для вас совершенно не имеет значения.
Как мы знаем, объекты можно использовать как ассоциативные массивы для хранения пар ключ/значение.
…Но если мы попытаемся хранить в нем предоставленные пользователем ключи (например, введенный пользователем словарь), то увидим интересный глюк: все ключи работают нормально, кроме "__proto__"
.
Посмотрите пример:
пусть объект = {}; let key = Prompt("Какой ключ?", "__proto__"); obj[key] = "некоторое значение"; предупреждение (объект [ключ]); // [объект Object], а не «какое-то значение»!
Здесь, если пользователь вводит __proto__
, назначение в строке 4 игнорируется!
Это, конечно, может быть удивительно для неразработчика, но вполне понятно для нас. Свойство __proto__
является особенным: оно должно быть либо объектом, либо null
. Строка не может стать прототипом. Вот почему присвоение строки __proto__
игнорируется.
Но мы не собирались реализовывать такое поведение, верно? Мы хотим сохранить пары ключ/значение, но ключ с именем "__proto__"
не был сохранен должным образом. Так это ошибка!
Здесь последствия не страшны. Но в других случаях мы можем хранить в obj
объекты вместо строк, и тогда прототип действительно будет изменен. В результате выполнение пойдет совершенно неожиданным образом.
Что еще хуже – обычно разработчики вообще не задумываются о такой возможности. Из-за этого такие ошибки трудно заметить и даже превратить их в уязвимости, особенно когда JavaScript используется на стороне сервера.
Неожиданные вещи также могут произойти при назначении obj.toString
, поскольку это встроенный метод объекта.
Как мы можем избежать этой проблемы?
Сначала мы можем просто переключиться на использование Map
для хранения вместо простых объектов, тогда всё в порядке:
пусть карта = новая карта(); let key = Prompt("Какой ключ?", "__proto__"); map.set(ключ, «некоторое значение»); оповещение(map.get(ключ)); // "некоторое значение" (как и предполагалось)
…Но синтаксис Object
зачастую более привлекателен, поскольку он более краток.
К счастью, мы можем использовать объекты, потому что создатели языка уже давно задумались над этой проблемой.
Как мы знаем, __proto__
— это не свойство объекта, а свойство доступа Object.prototype
:
Итак, если obj.__proto__
читается или устанавливается, соответствующий метод получения/установки вызывается из его прототипа и получает/устанавливает [[Prototype]]
.
Как было сказано в начале этого раздела руководства: __proto__
— это способ доступа к [[Prototype]]
, а не сам [[Prototype]]
.
Теперь, если мы намерены использовать объект как ассоциативный массив и избежать подобных проблем, мы можем сделать это с помощью небольшой хитрости:
пусть obj = Object.create(null); // или: obj = { __proto__: null } let key = Prompt("Какой ключ?", "__proto__"); obj[key] = "некоторое значение"; предупреждение (объект [ключ]); // "некоторое значение"
Object.create(null)
создает пустой объект без прототипа ( [[Prototype]]
имеет null
):
Итак, для __proto__
не существует унаследованного метода получения/установки. Теперь оно обрабатывается как обычное свойство данных, поэтому приведенный выше пример работает правильно.
Мы можем называть такие объекты «очень простыми» или «чистыми словарными» объектами, потому что они даже проще, чем обычный простой объект {...}
.
Недостатком является то, что у таких объектов отсутствуют встроенные методы объекта, например toString
:
пусть obj = Object.create(null); предупреждение (объект); // Ошибка (нет toString)
…Но обычно для ассоциативных массивов это нормально.
Обратите внимание, что большинство методов, связанных с объектами, — это Object.something(...)
, например Object.keys(obj)
— их нет в прототипе, поэтому они будут продолжать работать с такими объектами:
пусть chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见"; оповещение(Object.keys(chineseDictionary)); // привет, пока
Чтобы создать объект с заданным прототипом, используйте:
Object.create
предоставляет простой способ поверхностного копирования объекта со всеми дескрипторами:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
литеральный синтаксис: { __proto__: ... }
, позволяет указать несколько свойств.
или Object.create(proto[, descriptors]) позволяет указать дескрипторы свойств.
Современные методы получения/установки прототипа:
Object.getPrototypeOf(obj) – возвращает [[Prototype]]
объекта obj
(так же, как метод получения __proto__
).
Object.setPrototypeOf(obj, proto) – устанавливает для [[Prototype]]
объекта obj
значение proto
(так же, как установщик __proto__
).
Получение/установка прототипа с использованием встроенного метода получения/установки __proto__
не рекомендуется, теперь он находится в Приложении B спецификации.
Мы также рассмотрели объекты без прототипов, созданные с помощью Object.create(null)
или {__proto__: null}
.
Эти объекты используются как словари для хранения любых (возможно, созданных пользователем) ключей.
Обычно объекты наследуют встроенные методы и метод получения/установки __proto__
из Object.prototype
, что делает соответствующие ключи «занятыми» и потенциально вызывает побочные эффекты. При null
прототипе объекты действительно пусты.
важность: 5
Существует dictionary
объектов, созданный как Object.create(null)
для хранения любых пар key/value
.
Добавьте в него dictionary.toString()
, который должен возвращать список ключей, разделенных запятыми. Ваша toString
не должна отображаться внутри for..in
.
Вот как это должно работать:
пусть словарь = Object.create(null); // ваш код для добавления метода Dictionary.toString // добавляем некоторые данные словарь.apple = "Яблоко"; словарь.__proto__ = "тест"; // __proto__ здесь является обычным ключом свойства // в цикле только apple и __proto__ for(введите ключ в словаре) { предупреждение (ключ); // "яблоко", затем "__proto__" } // ваша toString в действии оповещение (словарь); // "яблоко,__прото__"
Метод может брать все перечислимые ключи с помощью Object.keys
и выводить их список.
Чтобы сделать toString
неперечисляемым, давайте определим его с помощью дескриптора свойства. Синтаксис Object.create
позволяет нам предоставлять объекту дескрипторы свойств в качестве второго аргумента.
пусть словарь = Object.create(null, { toString: { // определяем свойство toString value() { // значение является функцией вернуть Object.keys(this).join(); } } }); словарь.apple = "Яблоко"; словарь.__proto__ = "тест"; // apple и __proto__ в цикле for(введите ключ в словаре) { предупреждение (ключ); // "яблоко", затем "__proto__" } // список свойств, разделенных запятыми, по toString оповещение (словарь); // "яблоко,__прото__"
Когда мы создаем свойство с использованием дескриптора, его флаги по умолчанию имеют false
. Итак, в приведенном выше коде dictionary.toString
не является перечислимым.
См. раздел «Флаги свойств и дескрипторы».
важность: 5
Давайте создадим новый объект rabbit
:
функция Кролик(имя) { это.имя = имя; } Rabbit.prototype.sayHi = function() { оповещение(это.имя); }; let Rabbit = новый Кролик («Кролик»);
Эти вызовы делают то же самое или нет?
кролик.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(кролик).sayHi(); кролик.__proto__.sayHi();
В первом вызове this == rabbit
, в остальных this
равно Rabbit.prototype
, потому что на самом деле это объект перед точкой.
Таким образом, только первый вызов показывает Rabbit
, остальные показывают undefined
:
функция Кролик(имя) { это.имя = имя; } Rabbit.prototype.sayHi = function() { предупреждение(это.имя); } let Rabbit = новый Кролик («Кролик»); кролик.sayHi(); // Кролик Rabbit.prototype.sayHi(); // неопределенный Object.getPrototypeOf(кролик).sayHi(); // неопределенный кролик.__proto__.sayHi(); // неопределенный