JavaScript: вывод метода из prototype после new
Дан код:
function Book() {
this.name = 'foo';
}
Book.prototype = {
getName: function () {
return this.name;
},
};
var book = new Book();
Book.prototype.getUpperName = function () {
return this.getName().toUpperCase();
};
console.log(book.getUpperName());
Что вернет метод console.log(book.getUpperName())?
Теория: как работает prototype
В JavaScript у каждого объекта есть скрытая ссылка на прототип (в спецификации это внутренний слот [[Prototype]]), и при чтении свойства сначала выполняется поиск в самом объекте, затем — по цепочке прототипов.
Функция-конструктор (например, Book) имеет свойство prototype, а оператор new использует именно его, чтобы установить [[Prototype]] у создаваемого объекта.
Ключевое следствие: если Book.prototype указывает на некоторый объект-прототип, то все экземпляры, созданные через new Book(), хранят ссылку на этот объект как на свой прототип.
Если позже добавить новое свойство в этот же объект-прототип (то есть изменить его содержимое, например Book.prototype.getUpperName = ...), то это свойство станет доступно уже созданным экземплярам через цепочку прототипов.
[[Prototype]] указывает на этот объект.Book.prototype = другойОбъект — влияет только на будущие экземпляры, а уже созданные экземпляры продолжают ссылаться на старый объект-прототип.Пошаговый разбор кода
Исходный код (как дано):
function Book() {
this.name = 'foo';
}
Book.prototype = {
getName: function () {
return this.name;
},
};
var book = new Book();
Book.prototype.getUpperName = function () {
return this.getName().toUpperCase();
};
console.log(book.getUpperName());
Разбор по шагам:
- До создания объекта выполнено присваивание
Book.prototype = { ... }.
Это означает, чтоBook.prototypeтеперь указывает на новый объект-литерал, внутри которого есть методgetName. - При выполнении
new Book():
- создаётся новый объект (будущий
book); - его
[[Prototype]]устанавливается равным текущемуBook.prototype; - затем вызывается
Bookкак конструктор, и внутри него выполняетсяthis.name = 'foo'.
Следствие: у book появляется собственное свойство name со значением 'foo'.
- Затем выполняется строка
Book.prototype.getUpperName = function () { ... }.
Это не заменаBook.prototype, а добавление свойстваgetUpperNameв тот же самый объект-прототип, на который уже ссылаетсяbook.[[Prototype]]. - При вызове
book.getUpperName()поиск метода выполняется так:
- в самом объекте
bookсвойстваgetUpperNameнет; - далее поиск продолжается в
book.[[Prototype]](то есть в объектеBook.prototype), гдеgetUpperNameуже существует.
- Внутри
getUpperNameвыполняетсяthis.getName().toUpperCase():
thisпри вызове метода является объектомbook;this.getName()находится в прототипе и возвращаетthis.name;this.name— это собственное свойствоbook, равное'foo';'foo'.toUpperCase()возвращает'FOO'.
Итог: console.log(book.getUpperName()) выведет FOO'.
Схема цепочки прототипов для данного примера:
book ---> Book.prototype ---> Object.prototype ---> null
Таблица поиска свойства (упрощённо):
| Выражение | Где ищется сначала | Где находится в итоге | Что получается |
|---|---|---|---|
book.getUpperName | book | Book.prototype | функция |
this.getName внутри getUpperName | book | Book.prototype | функция |
this.name внутри getName | book | book | строка 'foo' |
Частые ошибки и варианты
TypeError возникает, когда выполняется попытка вызвать не-функцию как функцию (например, если метод не найден и значение равно undefined).В данном задании метод находится в прототипе, поэтому ошибки нет.
Важно: если после создания book выполнить именно переназначение Book.prototype = ... на новый объект, то уже созданный book не начнёт использовать новый прототип, потому что [[Prototype]] у book остался ссылаться на старый объект-прототип.
Пример контр-сценария (для понимания отличий):
function Book() {
this.name = 'foo';
}
Book.prototype = { getName: function () { return this.name; } };
var book = new Book();
/* переназначение prototype на новый объект */
Book.prototype = {
getName: function () { return 'bar'; },
getUpperName: function () { return this.getName().toUpperCase(); },
};
console.log(book.getUpperName()); // метод не найдётся у старого экземпляра
Кратко: экземпляр book ссылается на объект Book.prototype, и после создания экземпляра в этот же прототип добавлен getUpperName, поэтому вызов book.getUpperName() возвращает 'FOO'.