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());
    

    Разбор по шагам:

    1. До создания объекта выполнено присваивание Book.prototype = { ... }.
      Это означает, что Book.prototype теперь указывает на новый объект-литерал, внутри которого есть метод getName.
    2. При выполнении new Book():
    • создаётся новый объект (будущий book);
    • его [[Prototype]] устанавливается равным текущему Book.prototype;
    • затем вызывается Book как конструктор, и внутри него выполняется this.name = 'foo'.

    Следствие: у book появляется собственное свойство name со значением 'foo'.

    1. Затем выполняется строка Book.prototype.getUpperName = function () { ... }.
      Это не замена Book.prototype, а добавление свойства getUpperName в тот же самый объект-прототип, на который уже ссылается book.[[Prototype]].
    2. При вызове book.getUpperName() поиск метода выполняется так:
    • в самом объекте book свойства getUpperName нет;
    • далее поиск продолжается в book.[[Prototype]] (то есть в объекте Book.prototype), где getUpperName уже существует.
    1. Внутри 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.getUpperNamebookBook.prototypeфункция
    this.getName внутри getUpperNamebookBook.prototypeфункция
    this.name внутри getNamebookbookстрока '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'.