← Курс/Как работает прототипное наследование?#129 из 257+40 XP

Как работает прототипное наследование?

Краткий ответ

В JavaScript каждый объект имеет внутреннюю ссылку [[Prototype]] на другой объект (или null). При обращении к свойству движок сначала ищет его в самом объекте, затем поднимается по цепочке прототипов вверх до null. Это и есть прототипное наследование. Классы в JS — синтаксический сахар над этим же механизмом: class под капотом создаёт функцию-конструктор и настраивает прототипную цепочку.

Полный разбор

Как хранится прототип

const animal = {
  speak() { return `${this.name} говорит` }
}

const dog = Object.create(animal)  // dog.[[Prototype]] = animal
dog.name = 'Рекс'

console.log(dog.name)     // 'Рекс' — собственное свойство dog
console.log(dog.speak())  // 'Рекс говорит' — взято из прототипа!

Цепочка прототипов (Prototype Chain)

dog.speak() ← ищем в dog... нет
           ← ищем в animal... НАШЛИ → выполняем
           ← если бы не нашли → ищем в Object.prototype
           ← если бы не нашли → nullundefined
dog
 │ name: 'Рекс'
 │ [[Prototype]] ──→ animal
                      │ speak: function
                      │ [[Prototype]] ──→ Object.prototype
hasOwnProperty: function
toString: function
                                          │ [[Prototype]] ──→ null

Работа с прототипами напрямую

// Получить прототип объекта
Object.getPrototypeOf(dog) === animal  // true

// Проверить принадлежность свойства
dog.hasOwnProperty('name')    // true — собственное
dog.hasOwnProperty('speak')   // false — унаследованное

// Проверить вхождение в цепочку
dog instanceof Object  // true — Object.prototype есть в цепочке

// __proto__ — устаревший способ, избегай в продакшне
dog.__proto__ === animal  // true (работает, но не рекомендуется)

Function.prototype и оператор new

До ES6 наследование строилось через функции-конструкторы:

// Конструктор
function Animal(name) {
  this.name = name  // собственное свойство
}

// Метод на прототипе — общий для всех экземпляров
Animal.prototype.speak = function() {
  return `${this.name} говорит`
}

// Наследование
function Dog(name, breed) {
  Animal.call(this, name)  // вызов "super"
  this.breed = breed
}

// Настройка цепочки прототипов
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog  // важно восстановить!

Dog.prototype.bark = function() {
  return 'Гав!'
}

const rex = new Dog('Рекс', 'Лабрадор')
console.log(rex.speak())  // 'Рекс говорит' (из Animal.prototype)
console.log(rex.bark())   // 'Гав!' (из Dog.prototype)
console.log(rex instanceof Dog)    // true
console.log(rex instanceof Animal) // true

Классы — синтаксический сахар

// ES6 класс...
class Animal {
  constructor(name) {
    this.name = name
  }
  speak() {
    return `${this.name} говорит`
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name)  // Animal.call(this, name)
    this.breed = breed
  }
  bark() { return 'Гав!' }
}

// ...компилируется в ТО ЖЕ САМОЕ, что выше!
// typeof Animal === 'function'  — это по-прежнему функция!

const rex = new Dog('Рекс', 'Лабрадор')
console.log(Object.getPrototypeOf(rex) === Dog.prototype)    // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype)  // true

prototype vs [[Prototype]]

Очень частая путаница:

Function.prototype  — свойство функции-конструктора
                      (объект, который станет [[Prototype]] экземпляров)

[[Prototype]]       — внутренняя ссылка каждого объекта
                      (доступна через Object.getPrototypeOf())

Dog.prototype           ← объект с методами Dog
rex.[[Prototype]]       ← то же самое что Dog.prototype
Dog.[[Prototype]]       ← Function.prototype (Dog сам — функция)

hasOwnProperty vs in

const obj = Object.create({ inherited: true })
obj.own = true

'own'       in obj  // true  — ищет по всей цепочке
'inherited' in obj  // true  — нашёл в прототипе

obj.hasOwnProperty('own')       // true  — только собственные
obj.hasOwnProperty('inherited') // false — не собственное

Object.create(null) — объект без прототипа

const dict = Object.create(null)  // [[Prototype]] = null
dict.key = 'value'

// Нет методов Object.prototype — чистый словарь
dict.hasOwnProperty  // undefined (нет прототипа!)
dict.toString        // undefined

// Используют как "чистые" хеш-таблицы без риска конфликта с прототипом

Связанные уроки курса

  • Прототипы — базовый разбор
  • Классы
  • Наследование классов
  • F.prototype
  • Встроенные прототипы
  • Как отвечать на собеседовании

    **Начни с механизма**: "В JS нет классического наследования как в Java. Вместо этого объекты связаны цепочкой прототипов. При обращении к свойству JS ищет его сначала в объекте, затем поднимается по цепочке."

    **Объясни связь с классами**: "Классы — синтаксический сахар. Под капотом — те же функции-конструкторы и прототипы."

    **Покажи Object.create**: это самый прямой способ показать прототипное наследование без магии new/class.

    **Объясни путаницу prototype vs [[Prototype]]** — это отличает джуниора от мидла.

    Красные флаги ответа

    1. **"В JS есть классы как в Java"** — нет. Классы в JS — синтаксический сахар. typeof MyClass === 'function'. Это принципиально другая модель.

    2. **Путаница Dog.prototype и dog.[[Prototype]]** — не понимать разницу между свойством конструктора и внутренней ссылкой объекта означает непонимание базового механизма.

    3. **"hasOwnProperty и in делают одно и то же"** — нет. in ищет по всей цепочке, hasOwnProperty — только в самом объекте.

    Примеры

    Прототипная цепочка вручную, затем то же самое через классы — показываем что это одно и то же

    // ===== 1. ПРОТОТИПНАЯ ЦЕПОЧКА ВРУЧНУЮ =====
    console.log('=== Ручные прототипы ===')
    
    // Базовый объект
    const Vehicle = {
      type: 'транспортное средство',
      describe() {
        return `${this.brand || '?'}${this.type}`
      },
      toString() {
        return `[${this.type}: ${this.brand || '?'}]`
      }
    }
    
    // Создаём объект с Vehicle как прототипом
    const Car = Object.create(Vehicle)
    Car.type = 'автомобиль'
    Car.drive = function() {
      return `${this.brand} едет`
    }
    
    // Конкретный экземпляр
    const tesla = Object.create(Car)
    tesla.brand = 'Tesla'
    tesla.model = 'Model S'
    
    // Поиск по цепочке:
    console.log(tesla.brand)      // 'Tesla' — собственное
    console.log(tesla.type)       // 'автомобиль' — из Car
    console.log(tesla.describe()) // 'Tesla — автомобиль' — из Vehicle
    console.log(tesla.drive())    // 'Tesla едет' — из Car
    
    // Цепочка прототипов
    console.log('\n--- Цепочка прототипов ---')
    let proto = tesla
    let depth = 0
    while (proto !== null) {
      const props = Object.getOwnPropertyNames(proto)
        .filter(k => k !== '__proto__')
      console.log(`[${depth}] ${proto === tesla ? 'tesla' : proto === Car ? 'Car' : proto === Vehicle ? 'Vehicle' : 'Object.prototype'}: [${props.join(', ')}]`)
      proto = Object.getPrototypeOf(proto)
      depth++
    }
    
    // hasOwnProperty vs in
    console.log('\n--- hasOwnProperty vs in ---')
    console.log('brand - own?', tesla.hasOwnProperty('brand'))    // true
    console.log('type - own?', tesla.hasOwnProperty('type'))     // false
    console.log('describe - in?', 'describe' in tesla)           // true
    console.log('toString - in?', 'toString' in tesla)           // true
    
    // ===== 2. КЛАССЫ = ТЕ ЖЕ ПРОТОТИПЫ =====
    console.log('\n=== Классы под капотом ===')
    
    class Animal {
      constructor(name, sound) {
        this.name = name    // собственное свойство
        this.sound = sound  // собственное свойство
      }
    
      speak() {  // метод — на Animal.prototype
        return `${this.name}: ${this.sound}!`
      }
    
      toString() {
        return `Animal(name=${this.name})`
      }
    }
    
    class Dog extends Animal {
      constructor(name) {
        super(name, 'Гав')  // вызов Animal.constructor
        this.tricks = []    // собственное свойство
      }
    
      learn(trick) {  // метод — на Dog.prototype
        this.tricks.push(trick)
        return this
      }
    
      perform() {
        return this.tricks.length > 0
          ? `${this.name} умеет: ${this.tricks.join(', ')}`
          : `${this.name} ничему не обучен`
      }
    }
    
    const rex = new Dog('Рекс')
    rex.learn('сидеть').learn('лежать').learn('кувырок')
    
    console.log(rex.speak())    // Рекс: Гав!
    console.log(rex.perform())  // Рекс умеет: сидеть, лежать, кувырок
    
    // Доказательство: это те же прототипы
    console.log('\n--- Доказательство: class = прототипы ---')
    console.log('typeof Animal:', typeof Animal)                          // function
    console.log('rex.[[Prototype]] === Dog.prototype:',
      Object.getPrototypeOf(rex) === Dog.prototype)                       // true
    console.log('Dog.prototype.[[Prototype]] === Animal.prototype:',
      Object.getPrototypeOf(Dog.prototype) === Animal.prototype)          // true
    console.log('Animal.prototype.[[Prototype]] === Object.prototype:',
      Object.getPrototypeOf(Animal.prototype) === Object.prototype)       // true
    
    // Собственные свойства vs унаследованные
    console.log('\n--- Собственные свойства rex ---')
    console.log(Object.getOwnPropertyNames(rex))  // ['name', 'sound', 'tricks']
    
    console.log('--- Методы на Dog.prototype ---')
    console.log(Object.getOwnPropertyNames(Dog.prototype))  // ['constructor', 'learn', 'perform']
    
    // instanceof проверяет цепочку прототипов
    console.log('\n--- instanceof ---')
    console.log('rex instanceof Dog:', rex instanceof Dog)        // true
    console.log('rex instanceof Animal:', rex instanceof Animal)  // true
    console.log('rex instanceof Object:', rex instanceof Object)  // true