F.prototype и Object.create
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Каждая функция в JavaScript таскает с собой объект prototype — заготовку для будущих экземпляров. Разберём, как new её использует, зачем там свойство constructor и как задать прототип напрямую через Object.create.
У любой функции есть свойство prototype. Это обычный объект, и сам по себе он ничего не делает — просто лежит и ждёт. Смысл появляется в момент вызова new: новорождённый объект получает F.prototype своим прототипом.
Обратите внимание на вторую строку вывода: свойства strings в самом объекте нет. Оно нашлось на шаг выше по цепочке — в Guitar.prototype. Именно поэтому методы кладут туда: один экземпляр или миллион, а функция в памяти одна.
prototype — у функции, __proto__ — у объекта. Слова похожие, вещи разные.
new F() выполняет три действия: создаёт пустой объект, ставит ему прототипом F.prototype и вызывает F с этим объектом в this. Ключевое слово — «ставит прототипом», а не «копирует»: связь остаётся живой.
Объект old создан раньше, чем появилось свойство on, — и всё равно его видит. А вот присваивание нового объекта в Lamp.prototype старые экземпляры не трогает: каждый помнит тот объект, который был прототипом на момент его рождения. Меняется не связь, а то, откуда возьмёт прототип следующий new.
Уберём магию — останутся три строчки. Аналог ведёт себя почти как настоящий new, кроме тонкостей вроде new.target и классов. И кроме одного края: проверка instanceof Object не узнаёт объект без прототипа, поэтому конструктор, возвращающий Object.create(null), настоящий new отдаст наружу, а этот аналог — нет.
Объект F.prototype создаётся не пустым: в нём с самого начала есть свойство constructor, указывающее обратно на саму функцию. Экземпляры видят его по цепочке — отсюда привычное obj.constructor.
Ловушка: если заменить prototype целиком — объектным литералом — constructor уедет вместе со старым объектом. Ошибки не будет, поиск просто уйдёт выше, до Object.prototype, и ответ окажется неожиданным.
Лечится двумя способами: дописывать свойства в существующий объект (Ticket.prototype.price = 100) или вернуть ссылку руками, добавив constructor: Ticket внутрь литерала. Второй способ хуже лишь одним — такое свойство станет перечислимым и вылезет в for..in.
Object.create(proto) делает то же самое, но без посредника: никакой функции, никакого new — просто «создай объект, прототип у него вот этот». Читается честнее, потому что прототип виден прямо в вызове.
Вторым аргументом Object.create принимает дескрипторы свойств — тот же формат, что у Object.defineProperties. Благодаря этому им удобно делать точную копию объекта: вместе с прототипом, геттерами и флагами, которые обычное присваивание теряет.
Object.assign вызывает геттер и записывает в копию его результат — обычное число. Пара Object.create и getOwnPropertyDescriptors переносит сам геттер, поэтому он продолжает считать.
| Способ | Откуда берётся прототип | Когда удобно |
|---|---|---|
| new F() | из свойства F.prototype | много однотипных объектов с общими методами |
| Object.create(proto) | передан прямо в вызов | разовый объект, наследование от готового объекта |
| Object.setPrototypeOf(obj, proto) | меняется у уже существующего объекта | почти никогда: операция медленная, движок теряет оптимизации |
Обычный {} наследует от Object.prototype, а значит уже «знает» слова toString, constructor, valueOf. Пока объект описывает что-то конкретное, это незаметно. Но если ключи приходят извне — слова из текста, имена полей формы, — унаследованные свойства начинают притворяться данными.
Object.create(null) создаёт объект вообще без прототипа. Цепочка обрывается сразу, наследовать нечего — внутри только то, что вы туда положили. Идеальный словарь.
Плата за чистоту — полезных методов тоже нет: 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 по-прежнему лежал в прототипе.
Присваивание Note.prototype = {...} выбросило исходный объект-прототип, а вместе с ним и свойство constructor. Поиск уходит выше, до Object.prototype, и n.constructor оказывается равен Object.
Способ первый — не заменять объект, а дополнять его:
Способ второй — оставить литерал, но вернуть ссылку руками. Лучше через defineProperty, иначе constructor станет перечислимым и попадёт в for..in:
Словарь, который не врёт
Счётчик слов ломается на определённых входных данных. Найдите причину и исправьте одной строкой.
На ключе "constructor" выражение counts[w] || 0 возвращает не ноль, а унаследованную функцию Object: она истинна. Прибавление единицы к функции даёт строку — счётчик превращается в мусор.
Достаточно завести словарь без прототипа:
Альтернатива — const counts = new Map() с методами set и get: у Map ключи вообще не пересекаются со свойствами объекта.