← JavaScript/Прототипное наследование#79 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Прототипное наследование

Реальная проблема: общие методы для всех экземпляров

В интернет-магазине тысячи объектов-товаров. У каждого есть методы getPrice(), formatName(), isAvailable(). Если создавать эти методы внутри конструктора — каждый объект получит свою копию функции. Тысяча товаров = тысяча одинаковых функций в памяти. Прототипы решают это: метод существует в одном экземпляре, а все объекты его разделяют.

Что такое прототип

Каждый объект в JavaScript имеет скрытую ссылку [[Prototype]] на другой объект — свой прототип. При обращении к свойству объекта JS ищет его сначала в самом объекте, потом в прототипе, потом в прототипе прототипа — и так до null. Это цепочка прототипов.

На основе предыдущих уроков

  • «Объекты» — объекты как коллекции свойств
  • «Конструктор/new» — new создаёт объект и связывает его с Constructor.prototype
  • «this» — в методах прототипа this = вызывающий объект, не прототип
  • Как работает поиск свойств

    const animal = { eats: true, breathes: true }
    const dog    = { barks: true }
    
    // Устанавливаем прототип: dog[[Prototype]] = animal
    Object.setPrototypeOf(dog, animal)
    
    console.log(dog.barks)    // true — собственное свойство
    console.log(dog.eats)     // true — найдено в прототипе animal
    console.log(dog.flies)    // undefined — нет нигде в цепочке
    
    // Цепочка: dog → animal → Object.prototype → null

    Object.create — создать объект с нужным прототипом

    const vehicleProto = {
      describe() {
        return `${this.brand} ${this.model} (${this.year})`
      },
      isNew() {
        return new Date().getFullYear() - this.year < 3
      }
    }
    
    const car = Object.create(vehicleProto)
    car.brand = 'Toyota'
    car.model = 'Camry'
    car.year  = 2022
    
    console.log(car.describe())  // 'Toyota Camry (2022)'
    console.log(Object.getPrototypeOf(car) === vehicleProto)  // true

    Конструкторы и прототипы — эффективное наследование

    Методы в Constructor.prototype разделяются всеми экземплярами — один объект функции в памяти:

    function Product(name, price) {
      // собственные свойства — уникальны для каждого экземпляра
      this.name  = name
      this.price = price
    }
    
    // Методы — в прототипе — один раз для всех
    Product.prototype.getPrice = function() {
      return `${this.price} ₽`
    }
    
    Product.prototype.isExpensive = function() {
      return this.price > 10000
    }
    
    const laptop = new Product('Ноутбук', 75000)
    const mouse  = new Product('Мышь', 1500)
    
    console.log(laptop.getPrice())     // '75000 ₽'
    console.log(mouse.isExpensive())   // false
    
    // Один объект метода разделяется всеми экземплярами:
    console.log(laptop.getPrice === mouse.getPrice)  // true — одна функция!

    Наследование через Object.create

    function Animal(name) {
      this.name = name
    }
    Animal.prototype.speak = function() {
      return `${this.name} издаёт звук`
    }
    
    function Dog(name, breed) {
      Animal.call(this, name)  // вызов родительского конструктора
      this.breed = breed
    }
    
    // Dog.prototype наследует от Animal.prototype
    Dog.prototype = Object.create(Animal.prototype)
    Dog.prototype.constructor = Dog  // восстанавливаем constructor
    
    // Переопределяем / добавляем методы
    Dog.prototype.speak = function() {
      return `${this.name} лает: Гав!`
    }
    Dog.prototype.fetch = function() {
      return `${this.name} приносит мяч`
    }
    
    const rex = new Dog('Рекс', 'Лабрадор')
    console.log(rex.speak())           // 'Рекс лает: Гав!'
    console.log(rex.fetch())           // 'Рекс приносит мяч'
    console.log(rex instanceof Dog)    // true
    console.log(rex instanceof Animal) // true — цепочка работает

    hasOwnProperty — только свои свойства

    console.log(rex.hasOwnProperty('name'))   // true — собственное
    console.log(rex.hasOwnProperty('speak'))  // false — из прототипа
    
    // В цикле for...in — свои + прототипные. Фильтруй если нужно:
    for (const key in rex) {
      if (rex.hasOwnProperty(key)) {
        console.log(key, rex[key])  // только name и breed
      }
    }

    Типичные ошибки

    Ошибка 1: забыть восстановить constructor

    // Сломано:
    Dog.prototype = Object.create(Animal.prototype)
    // Dog.prototype.constructor теперь Animal — не Dog!
    console.log(new Dog('Rex').constructor === Dog)  // false
    
    // Исправлено:
    Dog.prototype = Object.create(Animal.prototype)
    Dog.prototype.constructor = Dog  // восстановить!

    Ошибка 2: изменение встроенных прототипов

    // НИКОГДА так не делай — ломает совместимость:
    Array.prototype.last = function() { return this[this.length - 1] }
    String.prototype.reverse = function() { return [...this].reverse().join('') }
    
    // Исправлено — используй вспомогательные функции:
    function last(arr) { return arr[arr.length - 1] }

    Ошибка 3: установка прототипа через __proto__

    // Устарело и медленно:
    dog.__proto__ = animal
    
    // Современно:
    Object.setPrototypeOf(dog, animal)
    // Или при создании:
    const dog = Object.create(animal)

    В реальных проектах

  • До ES6: весь ООП-код использовал конструкторы и прототипы
  • Классы: синтаксический сахар над прототипами — компилируются именно в это
  • Понимание важно: дебаггер показывает [[Prototype]], ошибки типа «x is not a function» часто связаны с цепочкой
  • Object.keys/values: не включают прототипные свойства
  • Примеры

    Иерархия пользователей через прототипное наследование

    // Базовый конструктор
    function User(name, email) {
      this.name  = name
      this.email = email
      this.createdAt = new Date().toISOString()
    }
    
    User.prototype.toString = function() {
      return `User(${this.name}, ${this.email})`
    }
    
    User.prototype.canAccess = function(resource) {
      return false  // базовая реализация — нет доступа
    }
    
    // AdminUser — наследует от User
    function AdminUser(name, email, department) {
      User.call(this, name, email)  // вызов родительского конструктора
      this.department = department
      this.permissions = new Set(['read', 'write', 'delete'])
    }
    
    AdminUser.prototype = Object.create(User.prototype)
    AdminUser.prototype.constructor = AdminUser
    
    AdminUser.prototype.canAccess = function(resource) {
      return true  // администратор — полный доступ
    }
    
    AdminUser.prototype.grant = function(permission) {
      this.permissions.add(permission)
      return this
    }
    
    AdminUser.prototype.toString = function() {
      return `Admin(${this.name}, dept: ${this.department})`
    }
    
    // Создание экземпляров
    const user  = new User('Алексей', 'alex@mail.ru')
    const admin = new AdminUser('Мария', 'maria@mail.ru', 'IT')
    
    console.log(user.toString())          // 'User(Алексей, alex@mail.ru)'
    console.log(admin.toString())         // 'Admin(Мария, dept: IT)'
    console.log(user.canAccess('files'))  // false
    console.log(admin.canAccess('files')) // true
    
    // Проверка цепочки прототипов
    console.log(admin instanceof AdminUser)  // true
    console.log(admin instanceof User)       // true — цепочка работает!
    console.log(admin.constructor === AdminUser)  // true
    
    // hasOwnProperty
    console.log(admin.hasOwnProperty('name'))      // true — собственное
    console.log(admin.hasOwnProperty('canAccess')) // false — из прототипа
    
    // Один метод для всех User — экономия памяти
    const user2 = new User('Иван', 'ivan@mail.ru')
    console.log(user.toString === user2.toString)  // true — одна функция!
    
    // Object.keys — только собственные свойства
    console.log(Object.keys(admin))
    // ['name', 'email', 'createdAt', 'department', 'permissions']

    Прототипное наследование

    Реальная проблема: общие методы для всех экземпляров

    В интернет-магазине тысячи объектов-товаров. У каждого есть методы getPrice(), formatName(), isAvailable(). Если создавать эти методы внутри конструктора — каждый объект получит свою копию функции. Тысяча товаров = тысяча одинаковых функций в памяти. Прототипы решают это: метод существует в одном экземпляре, а все объекты его разделяют.

    Что такое прототип

    Каждый объект в JavaScript имеет скрытую ссылку [[Prototype]] на другой объект — свой прототип. При обращении к свойству объекта JS ищет его сначала в самом объекте, потом в прототипе, потом в прототипе прототипа — и так до null. Это цепочка прототипов.

    На основе предыдущих уроков

  • «Объекты» — объекты как коллекции свойств
  • «Конструктор/new» — new создаёт объект и связывает его с Constructor.prototype
  • «this» — в методах прототипа this = вызывающий объект, не прототип
  • Как работает поиск свойств

    const animal = { eats: true, breathes: true }
    const dog    = { barks: true }
    
    // Устанавливаем прототип: dog[[Prototype]] = animal
    Object.setPrototypeOf(dog, animal)
    
    console.log(dog.barks)    // true — собственное свойство
    console.log(dog.eats)     // true — найдено в прототипе animal
    console.log(dog.flies)    // undefined — нет нигде в цепочке
    
    // Цепочка: dog → animal → Object.prototype → null

    Object.create — создать объект с нужным прототипом

    const vehicleProto = {
      describe() {
        return `${this.brand} ${this.model} (${this.year})`
      },
      isNew() {
        return new Date().getFullYear() - this.year < 3
      }
    }
    
    const car = Object.create(vehicleProto)
    car.brand = 'Toyota'
    car.model = 'Camry'
    car.year  = 2022
    
    console.log(car.describe())  // 'Toyota Camry (2022)'
    console.log(Object.getPrototypeOf(car) === vehicleProto)  // true

    Конструкторы и прототипы — эффективное наследование

    Методы в Constructor.prototype разделяются всеми экземплярами — один объект функции в памяти:

    function Product(name, price) {
      // собственные свойства — уникальны для каждого экземпляра
      this.name  = name
      this.price = price
    }
    
    // Методы — в прототипе — один раз для всех
    Product.prototype.getPrice = function() {
      return `${this.price} ₽`
    }
    
    Product.prototype.isExpensive = function() {
      return this.price > 10000
    }
    
    const laptop = new Product('Ноутбук', 75000)
    const mouse  = new Product('Мышь', 1500)
    
    console.log(laptop.getPrice())     // '75000 ₽'
    console.log(mouse.isExpensive())   // false
    
    // Один объект метода разделяется всеми экземплярами:
    console.log(laptop.getPrice === mouse.getPrice)  // true — одна функция!

    Наследование через Object.create

    function Animal(name) {
      this.name = name
    }
    Animal.prototype.speak = function() {
      return `${this.name} издаёт звук`
    }
    
    function Dog(name, breed) {
      Animal.call(this, name)  // вызов родительского конструктора
      this.breed = breed
    }
    
    // Dog.prototype наследует от Animal.prototype
    Dog.prototype = Object.create(Animal.prototype)
    Dog.prototype.constructor = Dog  // восстанавливаем constructor
    
    // Переопределяем / добавляем методы
    Dog.prototype.speak = function() {
      return `${this.name} лает: Гав!`
    }
    Dog.prototype.fetch = function() {
      return `${this.name} приносит мяч`
    }
    
    const rex = new Dog('Рекс', 'Лабрадор')
    console.log(rex.speak())           // 'Рекс лает: Гав!'
    console.log(rex.fetch())           // 'Рекс приносит мяч'
    console.log(rex instanceof Dog)    // true
    console.log(rex instanceof Animal) // true — цепочка работает

    hasOwnProperty — только свои свойства

    console.log(rex.hasOwnProperty('name'))   // true — собственное
    console.log(rex.hasOwnProperty('speak'))  // false — из прототипа
    
    // В цикле for...in — свои + прототипные. Фильтруй если нужно:
    for (const key in rex) {
      if (rex.hasOwnProperty(key)) {
        console.log(key, rex[key])  // только name и breed
      }
    }

    Типичные ошибки

    Ошибка 1: забыть восстановить constructor

    // Сломано:
    Dog.prototype = Object.create(Animal.prototype)
    // Dog.prototype.constructor теперь Animal — не Dog!
    console.log(new Dog('Rex').constructor === Dog)  // false
    
    // Исправлено:
    Dog.prototype = Object.create(Animal.prototype)
    Dog.prototype.constructor = Dog  // восстановить!

    Ошибка 2: изменение встроенных прототипов

    // НИКОГДА так не делай — ломает совместимость:
    Array.prototype.last = function() { return this[this.length - 1] }
    String.prototype.reverse = function() { return [...this].reverse().join('') }
    
    // Исправлено — используй вспомогательные функции:
    function last(arr) { return arr[arr.length - 1] }

    Ошибка 3: установка прототипа через __proto__

    // Устарело и медленно:
    dog.__proto__ = animal
    
    // Современно:
    Object.setPrototypeOf(dog, animal)
    // Или при создании:
    const dog = Object.create(animal)

    В реальных проектах

  • До ES6: весь ООП-код использовал конструкторы и прототипы
  • Классы: синтаксический сахар над прототипами — компилируются именно в это
  • Понимание важно: дебаггер показывает [[Prototype]], ошибки типа «x is not a function» часто связаны с цепочкой
  • Object.keys/values: не включают прототипные свойства
  • Примеры

    Иерархия пользователей через прототипное наследование

    // Базовый конструктор
    function User(name, email) {
      this.name  = name
      this.email = email
      this.createdAt = new Date().toISOString()
    }
    
    User.prototype.toString = function() {
      return `User(${this.name}, ${this.email})`
    }
    
    User.prototype.canAccess = function(resource) {
      return false  // базовая реализация — нет доступа
    }
    
    // AdminUser — наследует от User
    function AdminUser(name, email, department) {
      User.call(this, name, email)  // вызов родительского конструктора
      this.department = department
      this.permissions = new Set(['read', 'write', 'delete'])
    }
    
    AdminUser.prototype = Object.create(User.prototype)
    AdminUser.prototype.constructor = AdminUser
    
    AdminUser.prototype.canAccess = function(resource) {
      return true  // администратор — полный доступ
    }
    
    AdminUser.prototype.grant = function(permission) {
      this.permissions.add(permission)
      return this
    }
    
    AdminUser.prototype.toString = function() {
      return `Admin(${this.name}, dept: ${this.department})`
    }
    
    // Создание экземпляров
    const user  = new User('Алексей', 'alex@mail.ru')
    const admin = new AdminUser('Мария', 'maria@mail.ru', 'IT')
    
    console.log(user.toString())          // 'User(Алексей, alex@mail.ru)'
    console.log(admin.toString())         // 'Admin(Мария, dept: IT)'
    console.log(user.canAccess('files'))  // false
    console.log(admin.canAccess('files')) // true
    
    // Проверка цепочки прототипов
    console.log(admin instanceof AdminUser)  // true
    console.log(admin instanceof User)       // true — цепочка работает!
    console.log(admin.constructor === AdminUser)  // true
    
    // hasOwnProperty
    console.log(admin.hasOwnProperty('name'))      // true — собственное
    console.log(admin.hasOwnProperty('canAccess')) // false — из прототипа
    
    // Один метод для всех User — экономия памяти
    const user2 = new User('Иван', 'ivan@mail.ru')
    console.log(user.toString === user2.toString)  // true — одна функция!
    
    // Object.keys — только собственные свойства
    console.log(Object.keys(admin))
    // ['name', 'email', 'createdAt', 'department', 'permissions']

    Задание

    Создай систему контента для блога. Конструктор Content(title, author) хранит title, author, createdAt (текущая дата ISO), views=0. В прототипе: view() — увеличивает views и возвращает this, getAge() — возвращает количество дней с момента создания (используй Date.now()). Конструктор Article(title, author, category) наследует от Content через Object.create. Добавь в прототип Article: publish() — устанавливает isPublished=true и возвращает this, getInfo() — возвращает строку формата "[category] title by author (N просмотров)".

    Подсказка

    Content.call(this, title, author) в конструкторе Article. Article.prototype = Object.create(Content.prototype). getInfo: `[${this.category}] ${this.title} by ${this.author} (${this.views} просмотров)`.

    Загружаем среду выполнения...
    Загружаем AI-помощника...