F.prototype и Object.create

прототипы≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Каждая функция в JavaScript таскает с собой объект prototype — заготовку для будущих экземпляров. Разберём, как new её использует, зачем там свойство constructor и как задать прототип напрямую через Object.create.

Разделы темы:·

У любой функции есть свойство prototype. Это обычный объект, и сам по себе он ничего не делает — просто лежит и ждёт. Смысл появляется в момент вызова new: новорождённый объект получает F.prototype своим прототипом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратите внимание на вторую строку вывода: свойства strings в самом объекте нет. Оно нашлось на шаг выше по цепочке — в Guitar.prototype. Именно поэтому методы кладут туда: один экземпляр или миллион, а функция в памяти одна.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

prototype — у функции, __proto__ — у объекта. Слова похожие, вещи разные.

new F() выполняет три действия: создаёт пустой объект, ставит ему прототипом F.prototype и вызывает F с этим объектом в this. Ключевое слово — «ставит прототипом», а не «копирует»: связь остаётся живой.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Объект old создан раньше, чем появилось свойство on, — и всё равно его видит. А вот присваивание нового объекта в Lamp.prototype старые экземпляры не трогает: каждый помнит тот объект, который был прототипом на момент его рождения. Меняется не связь, а то, откуда возьмёт прототип следующий new.

Уберём магию — останутся три строчки. Аналог ведёт себя почти как настоящий new, кроме тонкостей вроде new.target и классов. И кроме одного края: проверка instanceof Object не узнаёт объект без прототипа, поэтому конструктор, возвращающий Object.create(null), настоящий new отдаст наружу, а этот аналог — нет.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Объект F.prototype создаётся не пустым: в нём с самого начала есть свойство constructor, указывающее обратно на саму функцию. Экземпляры видят его по цепочке — отсюда привычное obj.constructor.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ловушка: если заменить prototype целиком — объектным литералом — constructor уедет вместе со старым объектом. Ошибки не будет, поиск просто уйдёт выше, до Object.prototype, и ответ окажется неожиданным.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Лечится двумя способами: дописывать свойства в существующий объект (Ticket.prototype.price = 100) или вернуть ссылку руками, добавив constructor: Ticket внутрь литерала. Второй способ хуже лишь одним — такое свойство станет перечислимым и вылезет в for..in.

Object.create(proto) делает то же самое, но без посредника: никакой функции, никакого new — просто «создай объект, прототип у него вот этот». Читается честнее, потому что прототип виден прямо в вызове.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вторым аргументом Object.create принимает дескрипторы свойств — тот же формат, что у Object.defineProperties. Благодаря этому им удобно делать точную копию объекта: вместе с прототипом, геттерами и флагами, которые обычное присваивание теряет.

Object.assign вызывает геттер и записывает в копию его результат — обычное число. Пара Object.create и getOwnPropertyDescriptors переносит сам геттер, поэтому он продолжает считать.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
СпособОткуда берётся прототипКогда удобно
new F()из свойства F.prototypeмного однотипных объектов с общими методами
Object.create(proto)передан прямо в вызовразовый объект, наследование от готового объекта
Object.setPrototypeOf(obj, proto)меняется у уже существующего объектапочти никогда: операция медленная, движок теряет оптимизации

Обычный {} наследует от Object.prototype, а значит уже «знает» слова toString, constructor, valueOf. Пока объект описывает что-то конкретное, это незаметно. Но если ключи приходят извне — слова из текста, имена полей формы, — унаследованные свойства начинают притворяться данными.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Object.create(null) создаёт объект вообще без прототипа. Цепочка обрывается сразу, наследовать нечего — внутри только то, что вы туда положили. Идеальный словарь.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Плата за чистоту — полезных методов тоже нет: dict.hasOwnProperty(key) вызвать уже нельзя, а неявное превращение в строку бросит TypeError. Первое решается через Object.hasOwn(dict, key), второе — привычкой не совать словарь в конкатенацию. А если ключами должны быть не только строки, честнее сразу взять Map.

  • prototype — свойство функции, а не объекта: new берёт оттуда прототип для каждого нового экземпляра
  • Связь живая — свойство, добавленное в prototype позже, видят уже созданные объекты
  • Замена всего объекта prototype на старые экземпляры не влияет и уносит с собой constructor
  • Object.create(proto) задаёт прототип напрямую, а вторым аргументом принимает дескрипторы свойств
  • Object.create(null) даёт чистый словарь без унаследованных ключей — ценой отсутствия методов вроде toString

1/3function A() {} A.prototype.x = 1; const a = new A(); A.prototype = { x: 2 }; — что выведет console.log(a.x)?

2/3const d = Object.create(null); что вернёт выражение "toString" in d?

3/3После function Ticket() {} и Ticket.prototype = { price: 100 } чему равно new Ticket().constructor?

Верните constructor на место

Код ниже работает, но последняя проверка проваливается. Объясните почему и почините двумя разными способами — так, чтобы метод show по-прежнему лежал в прототипе.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Присваивание Note.prototype = {...} выбросило исходный объект-прототип, а вместе с ним и свойство constructor. Поиск уходит выше, до Object.prototype, и n.constructor оказывается равен Object.

Способ первый — не заменять объект, а дополнять его:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Способ второй — оставить литерал, но вернуть ссылку руками. Лучше через defineProperty, иначе constructor станет перечислимым и попадёт в for..in:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Словарь, который не врёт

Счётчик слов ломается на определённых входных данных. Найдите причину и исправьте одной строкой.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

На ключе "constructor" выражение counts[w] || 0 возвращает не ноль, а унаследованную функцию Object: она истинна. Прибавление единицы к функции даёт строку — счётчик превращается в мусор.

Достаточно завести словарь без прототипа:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Альтернатива — const counts = new Map() с методами set и get: у Map ключи вообще не пересекаются со свойствами объекта.