Перевод статьи A Beginner’s Guide to JavaScript’s Prototype. Эта статья не является всеобъемлющим руководством для изучения прототипов как одной из основных концепций клиентского client-side Javascript, который используется в веб-браузерах. Она содержит материалы начального уровня, где на практических примерах даются первичные понятия о назначении и способах использования прототипов для работы с объектами в функциональном стиле программирования.

Как вы уже, вероятно, догадались, что не изучив предварительно основы работы с объектами, вы однозначно не сможете преуспеть в изучении JavaScript. Концепция объектов является основополагающей практически для каждого аспекта языка программирования JavaScript. Изучение того, как создавать объекты, является одной из первых вещей, которую вы изучили в Javascript. И в тоже время само понятие объекта в языке Javascript неотделимо от понятия его прототипа. Поэтому изучив основы работы с прототипами мы сможем глубже вникнуть в природу и особенности использования объектов в Javascript.

Объекты представляют собой особый тип данных, состоящий из пар “ключ–значение”. Наиболее распространенный способ создания объекта — использование литера, то есть с помощью фигурных скобок {}. И далее вы можете произвольно добавлять свойства и методы к объекту, используя точечную нотацию.

let animal = {}
animal.name = 'Leo'
animal.energy = 10

animal.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

animal.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

animal.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

Как видим этот код очень прост и интуитивно понятен. Очевидным продолжением развития нашего объекта, как структуры данных, будет реализация функционала, который может нам понадобится для создания более чем одного экземпляра животного animal. И естественно, следующим шагом для этого будет инкапсуляция всей логики внутри функции, которую мы будем вызывать всякий раз, когда нам нужно создавать новое животное. Мы назовем этот шаблон Functional Instantiation (функциональная реализация) и далее будем называть саму функцию «конструктором» (функцией-конструктором), поскольку она отвечает за «конструирование» нового объекта.

Functional Instantiation

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy

  animal.eat = function (amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }

  animal.sleep = function (length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }

  animal.play = function (length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

Теперь, когда мы захотим создать новое животное (или, как обычно говорят, новый экземпляр объекта), все, что нам нужно сделать, это вызвать функцию Animal() и передать ей в качестве аргумента имя (название животного) name и уровень его энергии energy. Этот подход прекрасно работает и это невероятно просто.

Тем не менее, как вы можете заметить, в нашей модели животного присутствуют некоторые недостатки. И один из них, и самый основной, то, что мы неизменно имеем дело с тремя методами: eat, sleep и play. Каждый из этих методов не является динамическим, то есть абсолютно не адаптируем для решения новых задач. И совершенно не подходят для описания поведения любого вида животных.

Как вы, вероятно, знаете, мы можем легко добавлять объекту новые методы. Тем не менее нет причин для повторного использования методов eat, sleep и play каждый раз, когда мы хотим создать экземпляр нового вида животного. Мы просто будем тратить память и каждый экземпляр объекта нового вида животного будет «больше», чем это необходимо.

const eagle = Animal('Eagle', 5)

eagle.fly = function (length) {
    console.log(`${this.name} is flies.`)
    this.energy += length
}

Сможем ли мы придумать более рациональное решение? А что если вместо того, чтобы каждый раз добавлять к экземплярам объектов каждого нового вида животного новые методы, мы будем обращаться непосредственно к его некоторому его внутреннему объекту. И каждый экземпляр животного, будет ссылается на этот объект. Назовем эту модель Functional Instantiation with Shared Methods (функциональная реализация с использованием общих методов) это многословно, но достаточно описательно.

Functional Instantiation with Shared Methods

Рассмотрим фрагмент кода ниже:

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy
  animal.eat = animalMethods.eat
  animal.sleep = animalMethods.sleep
  animal.play = animalMethods.play

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

Здесь мы переместили все методы в отдельный объект. И поэтому объект animalMethods по сути будет является централизованным хранилищем всех возможных методов, описывающих поведение присущее разным видам животных. Далее ссылаясь на него внутри нашей функции Animal(), таким образом, решаем проблему потери памяти и чрезмерно «больших» объектов животных. Однако это решение является не самым оптимальным и далее мы в этом убедимся.

Object.create

Давайте усовершенствуем наш пример еще раз, используя метод Object.create().

Метод Object.create позволяет создать объект, которому будут делегированы свойства и методы другого объекта, для случая неудачного поиска соответствующего свойства или метода при обращении к его текущему экземпляру.

Иными словами, Object.create() позволяет вам создать объект, и всякий раз, когда мы пытаемся обратиться к его свойству или методу, а для созданного экземпляра их не определили. То он может обратиться к родительскому объекту, чтобы узнать, есть ли у этого него это свойство или метод. Пока что не очень понятно. Давайте посмотрим следующий пример кода.

const parent = {
  name: 'Stacey',
  age: 35,
  heritage: 'Irish'
}

const child = Object.create(parent)
child.name = 'Ryan'
child.age = 7

console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish

Таким образом, в этом примере поскольку объект child был создан с помощью инструкции Object.create(parent), то при каждом неудачном поиске какого-либо свойства в child, JavaScript будет делегировать его поиск по цепочке в объекте parent. Это означает то, что, хотя у child нет свойства heritage, оно есть у объекта parent. И когда вы пытаетесь вывести на печать значение свойства child.heritage, то получаете значение Irish, унаследованное от метода parent.

Теперь когда метод Object.create() уже находится в нашем арсенале инструментов, подумаем как мы можем использовать его для упрощения кода нашей функции Animal(). Что ж, вместо того, чтобы добавлять все общие методы к животному один за другим, как мы это делали ранее, мы можем использовать Object.create для делегирования свойств объекта animalMethods. Давайте назовем наш новый способ создания объектов Functional Instantiation with Shared Methods and Object.create (функциональная реализация с помощью общих методов и Object.create).

Functional Instantiation with Shared Methods and Object.create

Рассмотрим новый пример кода:

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = Object.create(animalMethods)
  animal.name = name
  animal.energy = energy

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

И так если мы вызываем метод leo.eat(), то интерпретатор JavaScript сначала будет искать его в экземпляре объекта leo. Этот поиск завершится, соответственно, неудачей, но поскольку мы использовали для его создания метод Object.create(), то он делегирует искомый метод из объекта animalMethods, где он был определен.

Теперь все работает отлично. Хотя есть некоторые улучшения, которые мы еще можем сделать. Действительно, это почти по «хакерски» управлять отдельным объектом (animalMethods), чтобы конфигурировать свойства и методы экземпляров нашего объекта. И кажется, это новый довольно эффективный прием, который необходимо внедрить в сам язык. Однако на самом деле эта возможность уже давно реализована в самом языке Javascript. И она же является причиной по которой написан пост — это прототипы prototype.

Так что же собою представляют прототипы prototype в JavaScript? Проще говоря, каждая функция в JavaScript имеет свойство prototype, которое может ссылаться на некоторый объект. Разочаровались, верно? Проверим это на практике:

function doThing () {}
console.log(doThing.prototype) // {}

Что если вместо создания отдельного объекта для управления нашими методами (как мы это делали ранее с animalMethods()), мы поместим каждый из этих методов в прототип функции Animal()? Тогда мы можем использовать Object.create для передачи свойств и методов из animalMethods. То есть использовать его для делегирования в Animal.prototype. Мы назовем этот паттерн Prototypal Instantiation (Прототипирование).

Prototypal Instantiation

Рассмотрим следующий пример кода:

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

Вот так все просто. Опять же, prototype — это просто свойство, которое есть у каждой функции в языке JavaScript, и, как мы видим в коде выше, оно позволяет нам совместно использовать методы во всех экземплярах функции. Все наши функциональные возможности остались прежними, но теперь вместо того, чтобы управлять отдельным объектом для всех методов, мы можем просто использовать “внутренний” объект, встраиваемый в саму функцию Animal(): Animal.prototype.

Заглянем глубже.

На данный момент мы уже знаем три вещи:

  1. Как создать функцию-конструктор для создания объектов.
  2. Как добавить нужные нам методы в прототип функции-конструктора.
  3. Как использовать метод Object.create() для делегирования поиска свойств в прототипе функции.

Эти три задачи представляются довольно фундаментальными для любого языка программирования. Действительно ли JavaScript настолько плох, что нет более простого, «встроенного» способа сделать то же самое еще проще? Как вы, вероятно, уже догадались, он существует: использование ключевого слова new.

То есть существует ключевое слово new позволяет сделать все гораздо проще. Зачем же в этой статье мы ранее рассматривали все эти примитивные шаблоны кода? Да, я наверное слишком методично и подробно подошел к изложению материала этой статьи, однако его целью было преподать вам более глубокое понимание того, что именно происходит «под капотом» ключевого слова new в JavaScript.

Анализируя код реализации функции Animal(), можно выделить две его основные составляющие: создание объекта (конфигурация его свойств) и возвращение его функцией. Соответственно, без создания объекта с помощью метода Object.create() мы не сможем ничего делегировать прототипу функции-конструктора. А без оператора return мы не сможем получить созданный и сконфигурированный объект.

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

Еще одна отличная новость — когда вы вызываете функцию с использованием ключевого слова new, то на создаваемый с ее помощью объект можно сослаться в теле нашей функции, используя другое ключевое слово: this. Поэтому две строки кода, из фрагмента выше, будут создаваться для вас неявно автоматически.

Используя комментарии, чтобы показать, что происходит внутри нашей функции Animal() и вызывая ее с использованием ключевого слова new, код выше можно переписать следующим образом:

function Animal(name, energy){
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

Перепишем наш код, включая общие методы:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

Отметим, что причина того, что ссылка на текущий экземпляр this работает и то, что объект был вообще создан, в том, что мы вызываем функцию-конструктор с ключевым словом new. Название этого паттерна создания объектов — Pseudoclassical Instantiation (псевдоклассическая реализация).

Если мы уберем из строки, где вызываем нашу функцию-конструктор, слово new, то объект никогда не будет создан и не будет неявно возвращен. Если не придерживаться этого, как в примере кода ниже, то шаблон прототипирования окажется бесполезен.

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)
console.log(leo) // undefined

Если вы уже давно занимаетесь программированием и JavaScript не первый язык, который вы возможно изучили, то этот факт вызовет у вас по меньшей мере беспокойство. И вы скажете про себя что-то подобное:

«Черт возьми, этот чувак только что создал новую версию Класса» — Вы.

Для тех кто пока незнаком с объектно-ориентированным программированием, класс позволяет написать шаблон для последующего создания объекта. И затем, когда вы создаете экземпляр этого класса, вы получаете полноценный объект со свойствами и методами, определенными ранее в этом шаблоне.

Звучит знакомо? Так это то же самое, что мы выше реализовывали с помощью нашей функции-конструктора Animal(). Однако вместо использования ключевого слова class, мы использовали обычную функцию JavaScript для воссоздания ее функциональности. Конечно, потребовалось немного дополнительной работы, а также некоторые знания о том, что происходит «под капотом» JavaScript, но результат по сути тот же.

И так у меня для вас еще хорошие новости. JavaScript совсем даже и не мертвый язык. Он постоянно совершенствуется и дополняется комитетом TC-39. Это означает то, что хотя первоначальная версия JavaScript не поддерживала классы, то совершенно не было причин, по которым они не могут войти в официальную спецификацию. И на текущий момент комитет TC-39 это сделал: в 2015 году был выпущен EcmaScript 6 (новая официальная спецификация JavaScript) с поддержкой классов и ключевого слова class. Давайте посмотрим, как будет выглядеть наша функция-конструктор Animal() с учетом нового синтаксиса класса.

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

Красивый и понятный код, не так ли?

Итак, если существует новый способ создания классов, почему же мы потратили так много времени, изучая старые приемы это сделать? Причина в том, что этот новый способ (с использованием ключевого слова class) — это, прежде всего, пока что «синтаксический сахар» в отличие от способа основанного на прототипном наследовании, который мы назвали «псевдоклассическим» шаблоном. Поэтому чтобы понять удобный синтаксис классов EcmaScript 6, сначала необходимо освоить его «псевдоклассическую» модель.

И так, мы рассмотрели основы использования прототипов в JavaScript. Остальная часть этого поста будет посвящена пониманию других не менее полезных вопросов, связанных с его использованием. Далее мы рассмотрим конкретные примеры для закрепления понимания, работы механизм прототипного наследования в JavaScript.

Методы массивов

Выше мы подробно рассмотрели вопрос о том, как можно реализовать обмен свойствами и методами между экземплярами класса. И, как выяснилось, что наилучший из них — прикрепить их к прототипу функции. Эта же модель наследования используется в реализации такой особой структуре данных Javascript как массивы Array. И ранее вы, вероятно, создавали массивы следующим образом:

const friends = []

Оказывается, это тоже так называемый “синтаксический сахар” над инструкцией создания нового экземпляра объекта Array с использованием ключевого слова new. Поэтому следующие две строки кода по сути эквивалентны:

const friendsWithSugar = []

const friendsWithoutSugar = new Array()

Есть еще одна особенность, о которой вы возможно, никогда не задумывались. Как каждый экземпляр массива сразу после его создания уже содержит все встроенные методы ( splice, slice, pop и т. д.)?

И как вы теперь уже догадываетесь, это происходит потому, что они подключаются через Array.prototype, и когда вы создаете новый экземпляр класса Array, вы неявно используете ключевое слово new, которое определяет их делегирование в Array.prototype.

Вот результат вывода в консоли содержимого свойства Array.prototype:

console.log(Array.prototype)

/*
  concat: ƒn concat()
  constructor: ƒn Array()
  copyWithin: ƒn copyWithin()
  entries: ƒn entries()
  every: ƒn every()
  fill: ƒn fill()
  filter: ƒn filter()
  find: ƒn find()
  findIndex: ƒn findIndex()
  forEach: ƒn forEach()
  includes: ƒn includes()
  indexOf: ƒn indexOf()
  join: ƒn join()
  keys: ƒn keys()
  lastIndexOf: ƒn lastIndexOf()
  length: 0n
  map: ƒn map()
  pop: ƒn pop()
  push: ƒn push()
  reduce: ƒn reduce()
  reduceRight: ƒn reduceRight()
  reverse: ƒn reverse()
  shift: ƒn shift()
  slice: ƒn slice()
  some: ƒn some()
  sort: ƒn sort()
  splice: ƒn splice()
  toLocaleString: ƒn toLocaleString()
  toString: ƒn toString()
  unshift: ƒn unshift()
  values: ƒn values()
*/

Точно такая же логика работает в отношении любых объектов вообще. Абсолютно всем объектам Object.prototype делегирует некоторый фиксированный и обязательный набор методов. Поэтому у любого объекта всегда определены такие методы, как toString() и hasOwnProperty().

Статические методы

И так мы говорили о том, зачем и как можно управлять составом свойств и методов в экземплярах классов (объектов). Однако что, если у нас есть метод, который важен для использования самого класса, и его нет необходимости использовать в его экземплярах?

Например, что, если бы у нас была функция, которая бы принимала массив экземпляров объекта Animal (животных) и затем определяла, какого из них нужно кормить следующим? То есть каким-то образом упорядочивает наш массив экземпляров класса по заданному признаку. И для определения какой из экземпляров Animal следующий на очереди для кормежки, будем вызывать некоторую функцию nextToEat().

function nextToEat(animals){
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

Функцию nextToEat() не имеет смысла включать в Animal.prototype, поскольку мы не хотим определять ее как общий метод для всех экземпляров класса. Вместо этого правильнее думать о нём как о вспомогательном методе. И так если nextToEat() не должен передаваться в Animal.prototype, где же тогда его место? Ответ очевиден: мы можем просто вставить nextToEat() в ту же область видимости что и наш класс Animal, а затем вызывать его по необходимости.

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(nextToEat([leo, snoop])) // Leo

Этот код работает, но есть лучший способ реализации.

Всякий раз, когда вам необходим метод, который относится к определенному классу и не требуется его совместного использования его экземплярами класса, то вы можете добавить этот метод в качестве статического.

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
  static nextToEat(animals) {
    const sortedByLeastEnergy = animals.sort((a,b) => {
      return a.energy - b.energy
    })

    return sortedByLeastEnergy[0].name
  }
}

И теперь, поскольку мы добавили метод nextToEat() в класс в виде статического, то он определен в самом классе Animal (а не в его прототипе) и будет доступен с использованием точечной нотации Animal.nextToEat().

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo

Поскольку в начале этого поста мы говорили о прототипном наследовании, то давайте посмотрим, как мы можем добиться такого же результата с помощью кода, написанного в предыдущем стандарте ES5 (без классов). В приведенном выше примере мы использовали ключевое слово static и поместили метод nextToEat() непосредственно в класс. Реализовать этот же шаблон кода с учетом стандарта ES5 так же просто, добавим наш метод непосредственно в объект функции.

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

Animal.nextToEat = function (nextToEat) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo

Получение прототипа объекта

Независимо от того, какой шаблон кода, из рассмотренных выше, вы использовали для создания объекта получить его прототип можно с помощью метода Object.getPrototypeOf.

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)
const proto = Object.getPrototypeOf(leo)

console.log(proto)
// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}

proto === Animal.prototype // true

Проанализировав этот код, можно сделать следующие выводы.

Во-первых, вы можете заметить , что proto это объект, который включает 4 метода: constructor, eat, sleep, и play. Мы использовали метод getPrototypeOf(), который возвращает прототип объекта leo, в котором все они определены.

Говоря о прототипах, мы не упоминали еще об одной вещи. По умолчанию объект proto будет иметь еще одно свойство: constructor. Оно указывает на исходную функцию или класс, из которого был создан этот экземпляр. Поскольку JavaScript по умолчанию помещает свойство constructor в прототип, то любой экземпляр объекта может получить доступ к своему конструктору с помощью инструкции вида instance.constructor.

Второй важный вывод следует из следующей строки кода: Object.getPrototypeOf(leo) === Animal.prototype. Функция-конструктор Animal() имеет свойство prototype, через которое его методы доступны во всех экземплярах. Метод getPrototypeOf()позволяет нам также получить прототип нашего экземпляра.

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = new Animal('Leo', 7)
console.log(leo.constructor) // Выведет в консоли функцию конструктор

Для того, чтобы связать все о чем мы говорили ранее с методом Object.create(), сначала надо отметить, что причина по которой работает прототипное наследование следующая.

Любые экземпляры объекта Animal будут всегда делегировать свои свойства и методы, отсутствующие в текущем экземпляре объекта, из его прототипа Animal.prototype. Поэтому, когда вы пытаетесь получить доступ к методу leo.constructor(), то так как у экземпляра leo его нет, то он делегирует его поиск в прототипе Animal.prototype, который в свою очередь его содержит. Если содержимое этого абзаца для вас не имеет смысла, то вернитесь и прочитайте выше о методе Object.create().

Возможно, вы ранее уже видели, как используется свойство __proto__ для получения прототипа экземпляра. Использование этого приема в настоящее время является, так сказать, пережитком прошлого. Использование свойства __proto__ ухудшает переносимость вашего кода, так как оно отсутствует в реализациях браузеров IE или Opera. Вместо этого используйте метод Object.getPrototypeOf(instance), как мы это делали выше.

Как определить содержит ли прототип указанное свойство

В определенных случаях вам будет необходимо узнать, содержится ли некоторое свойство в самом экземпляре объекта либо в его прототипе, которому он делегирует. Давайте посмотрим на это на практике. Допустим, наша задача перебор свойств объекта leo в цикле и вывод их в консоли в виде ключей, а также соответствующих им значений. Для этого используем цикл for, и это, вероятно, будет выглядеть следующим образом:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)

for(let key in leo) {
  console.log(`Key: ${key}. Value: ${leo[key]}`)
}

Что же вы ожидаете увидеть в итоге? Скорее всего, что-то вроде этого:

Key: name. Value: Leo
Key: energy. Value: 7

Однако в консоли скрипт выведет следующее:

Key: name. Value: Leo
Key: energy. Value: 7
Key: eat. Value: function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}
Key: sleep. Value: function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}
Key: play. Value: function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

Проанализируем полученный результат. Цикл for in осуществляет перебор перечисляемых enumerable свойств как самого объекта, так и его прототипа, которому он делегирует. Поскольку по умолчанию любое свойство, которое вы добавляете в прототип функции, является перечисляемым, то мы видим не только name и energy , но и все методы прототипа — eat, sleep, and play. Чтобы это исправить, нам нужно определить методы прототипа как не перечисляемые. Либо нам нужен другой способ их селекции, при котором метод console.log будет выводить только те свойства, которые определены в экземпляре объекта leo. А не в его прототипе, которому делегирует leo при каждом неудачном поиске свойств. Вот где метод hasOwnProperty() может действительно нам помочь.

hasOwnProperty() — проверяет свойство каждого объекта и возвращает логическое значение, указывающее, имеет ли объект указанное свойство в качестве своего собственного, а не его прототип, которому делегирует наш объект. Это именно то, что нам нужно. Теперь, благодаря этому новому знанию, мы можем модифицировать наш код, используя метод hasOwnProperty() внутри нашего цикла for.

//...

const leo = new Animal('Leo', 7)

for(let key in leo) {
  if (leo.hasOwnProperty(key)) {
    console.log(`Key: ${key}. Value: ${leo[key]}`)
  }
}

И теперь в консоли мы увидим только свойства, относящиеся к самому объекту leo, а не к его прототипу.

Key: name. Value: Leo
Key: energy. Value: 7

Если вы все еще не совсем понимаете, что позволяет сделать hasOwnProperty(), то следующий ниже фрагмент кода, я надеюсь, поможет все прояснить.

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)

leo.hasOwnProperty('name') // true
leo.hasOwnProperty('energy') // true
leo.hasOwnProperty('eat') // false
leo.hasOwnProperty('sleep') // false
leo.hasOwnProperty('play') // false

Проверяем является ли объект экземпляром класса

Допустим вам необходимо узнать, является ли объект экземпляром определенного класса. Для этого вы можете использовать оператор instanceof. Способ его использования довольно прост, но фактически его синтаксис выглядит немного необычно, если вы никогда не пользовались им ранее.

object instanceof Class

Выражение выше вернет true, если объект object является экземпляром класса Class, и false, если соответственно это не так. Возвращаясь к нашему примеру с функцией Animal, у нас получится следующее:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

function User () {}

const leo = new Animal('Leo', 7)

leo instanceof Animal // true
leo instanceof User // false

Принцип работы оператора instanceof заключается в проверке наличия свойства constructor.prototype в цепочке прототипов тестируемого экземпляра объекта. В приведенном выше примере выражение leo instanceof Animal возвращает значение true, поскольку Object.getPrototypeOf(leo) === Animal.prototype.

Соответственно leo instanceof User будет возвращать false, поскольку Object.getPrototypeOf (leo)! == User.prototype.

Создание новой отдельной функции-конструктора

Проверьте себя, сможете ли вы обнаружить ошибку в коде ниже?

function Animal(name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)

Неопытный JavaScript-разработчик может легко запутаться при анализе кода из примера выше. Как видно здесь мы используем паттерн pseudoclassical pattern, о котором мы уже говорили ранее. То есть, при вызове функции-конструктора Animal нам сначала необходимо убедиться, что мы вызываем ее с использованием ключевого слова new. Если мы этого не сделаем, то ключевое слово this не будет ссылаться на текущий экземпляр, а присваиваемые таким образом значения свойств не будут неявно возвращаться при создании нового объекта.

В коде ниже закомментированные строки — это то, что фактически происходит «под капотом», когда мы используем при вызове функции ключевого слово new.

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}

Это достаточно важная деталь и другие разработчики, использующие ваш код должны о ней знать. Предполагая, что мы работаем в команде с другими разработчиками, можем ли мы гарантировать, что наша функция-конструктор Animal будет всегда вызывается совместно с ключевым словом new? Оказывается, для этого есть instanceof оператор, о котором мы уже знаем.

Таким образом, если конструктор нового объекта вызывается с ключевым словом new, то this внутри тела функции-конструктора будет по сути являться ссылкой ее на экземпляр instanceof. Вот код, который поясняет вышесказанное:

function Animal (name, energy) {
  if (this instanceof Animal === false) {
    console.warn('Forgot to call Animal with the new keyword')
  }

  this.name = name
  this.energy = energy
}

Теперь вместо того, чтобы просто записывать предупреждение пользователю нашей функции, что если мы повторно вызовем функцию, но уже с ключевым словом new?

function Animal (name, energy) {
  if (this instanceof Animal === false) {
    return new Animal(name, energy)
  }

  this.name = name
  this.energy = energy
}

Теперь независимо от того, как была вызвана пользователем наша функция-конструктор объектов Animal, а также использовалось ли при этом ключевое слово new или нет, она все равно будет работать корректно.

Создаём свою реализацию метода Object.create

В этом посте мы в значительной степени уделяли внимание созданию объектов с использованием метода Object.create(), который делегирует прототипу функции-конструктора. На этом этапе вы должны знать как использовать метод Object.create() внутри своего кода, но есть еще одна деталь, о которой вы, вероятно, еще не задумывались. Это как на самом деле метод Object.create() работает, как говорится, под капотом. И чтобы вам стало это понятно, мы собираемся воссоздать всю его функциональность, воспользуюсь более простыми средствами языка.

И так, что мы уже знаем о работе метода Object.create()?

  1. Он принимает аргумент, который должен быть объектом.
  2. Он создает объект, который делегирует объекту аргумента при неудачных поисках свойств и методов в экземпляре объекта.
  3. Возвращает новый созданный объект.

Давайте начнем с пункта #1.

Object.create = function (objToDelegateTo) {

}

Это достаточно просто.

Теперь по пункту #2 — нам нужно создать объект, который будет делегировать объекту, передаваемому через аргумент, при неудачных поисках его свойств. Это немного сложнее. Для этого мы используем наши знания о том, как работает ключевое слово new и прототипы в JavaScript. Во-первых, внутри кода нашей реализации аналога метода Object.create() мы создадим пустую функцию Fn. Затем присвоим ей в качестве прототипа объект, переданный из аргумента. Далее создадим новый объект, то есть вызовем нашу пустую функцию Fn с использованием ключевого слова new.

Теперь нам остается, используя оператор return, возвратить из функции вновь созданный объект — пункт #3 тоже выполнен. Что же у нас получилось, смотрим код ниже:

Object.create = function (objToDelegateTo) {
  function Fn(){}
  Fn.prototype = objToDelegateTo
  return new Fn()
}

Необычно? Давайте пройдемся по нему.

Вначале мы создаем новую функцию Fn, которая получает новое значение для своего свойства prototype. Когда мы вызываем её с помощью ключевого слова new, мы знаем, что получаем объект, который далее будет делегировать прототипу функции при неудачных поисках её свойств. Таким образом, если мы переопределим прототип функции, то сможем самостоятельно определять, какому объекту делегировать при неудачных поисках. Иначе говоря мы перезаписываем прототип функции Fn объектом objToDelegateTo, который передаем в качестве аргумента при вызове нашего аналога Object.create.

Обратите внимание, что мы использовали только один аргумент метода Object.create(). Настоящая реализация этого метода поддерживает второй, необязательный аргумент, который позволяет вам добавить больше свойств к создаваемому объекту.

Стрелочные функции

Стрелочные функции не имеют ключевого слова this. Поэтому стрелочные функции не могут быть использованы в качестве функций-конструкторов, и если вы попытаетесь вызвать стрелочную функцию стрелки с ключевым словом new, то это вызовет появление ошибки с соответствующим исключением.

const Animal = () => {}

const leo = new Animal() // Error: Animal is not a constructor

Кроме того, поскольку мы только что продемонстрировали, что шаблон pseudoclassical pattern нельзя использовать со стрелочными функциями, то становится ясно они не имеют свойства, содержащего её прототип: prototype.

Оставить комментарий