Семейное фото в стиле семейки Аддамс, где каждый подписан как object
Иллюстрация: Кира Кустова

Почти всё в JavaScript — объект

Что бы мы ни использовали при написании JS кода, почти всё это объекты под капотом.

Время чтения: 6 мин

Кратко

Скопировано

В JavaScript объект является прародителем всех других сущностей. Все типы данных и структуры, кроме примитивных, являются потомками объекта. По этой причине абсолютно у всех наследников объекта имеется набор общих методов: toString(), valueOf() и другие.

Как понять

Скопировано

Массивы и функции

Скопировано

Объект — это сущность с набором свойств. Мы можем добавлять, менять и удалять эти свойства.

        
          
          const programmer = { name: 'John', level: 'Junior' }programmer.mainLanguage = 'JavaScript'delete programmer.levelconsole.dir(programmer)
          const programmer = { name: 'John', level: 'Junior' }

programmer.mainLanguage = 'JavaScript'
delete programmer.level

console.dir(programmer)

        
        
          
        
      
консоль, в которой выведены свойства объекта из примера

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

        
          
          const shows = ['Breakind Bad', 'The Office', 'Silicon Valley']shows.length // свойство массиваshows[1] // получить элемент массива, аналогично как у объекта shows['1']
          const shows = ['Breakind Bad', 'The Office', 'Silicon Valley']

shows.length // свойство массива

shows[1] // получить элемент массива, аналогично как у объекта shows['1']

        
        
          
        
      

Аналогичная ситуация с функциями — у них тоже есть набор свойств, который можно увидеть, выведя информацию о ней в консоль.

        
          
          function sum(a, b) {  return a + b}sum.arguments // можно вызвать свойство функцииsum.someField = 'value' // можно присвоить значение в полеconsole.dir(sum)
          function sum(a, b) {
  return a + b
}

sum.arguments // можно вызвать свойство функции
sum.someField = 'value' // можно присвоить значение в поле

console.dir(sum)

        
        
          
        
      

В выводе есть и свойство someField, которое мы присвоили, и набор встроенных свойств и методов.

консоль, в которой выведены свойства функции

Такая структура массивов и функций очень похожа на структуру объектов. Но на самом деле это и есть объекты, и в этом можно легко убедиться.

Давайте посмотрим на свойство __proto__ у функции sum(), описанной выше.

Свойство __proto__ является устаревшим (deprecated), не используйте его в коде, особенно для того, чтобы самостоятельно устанавливать прототип.

консоль, в которой выведен прототип функции sum

Если посмотреть свойство прототипа, то можно заметить, что прототипом текущего прототипа является объект. Заглянув в этот прототип, можно увидеть такую картину:

методы объекта в консоли

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

схема цепочки прототипов

Примитивы

Скопировано

В JavaScript существуют примитивные типы данных, такие как строки, числа или булевы значения. При работе со строкой можно обнаружить, что у неё тоже есть свойства и методы, к которым можно обратиться.

        
          
          const show = 'Breaking Bad'console.log(show.length)// 12console.log(show.charAt(1))// 'r'console.log(show.toUpperCase())// 'BREAKING BAD'
          const show = 'Breaking Bad'

console.log(show.length)
// 12
console.log(show.charAt(1))
// 'r'
console.log(show.toUpperCase())
// 'BREAKING BAD'

        
        
          
        
      

Но строка является примитивным типом данных, откуда же у неё поведение как у объекта? Когда происходит обращение к какому-то свойству или методу у примитива, происходит автоматическая обёртка (autoboxing) в специальный конструктор для примитива, который является наследником объекта. Для строки это будет функция String(). У этого объекта есть свойства и методы, которые и вызываются.

        
          
          const pet = 'dog'const pet2 = new String('dog') // будет создан объектconsole.log(pet === pet2)// false, потому что в pet2 находится объектconsole.dir(pet2)/* Выведет{  0: "d",  1: "o",  2: "g",  length: 3}*/
          const pet = 'dog'
const pet2 = new String('dog') // будет создан объект

console.log(pet === pet2)
// false, потому что в pet2 находится объект

console.dir(pet2)
/* Выведет
{
  0: "d",
  1: "o",
  2: "g",
  length: 3
}
*/

        
        
          
        
      

Для других типов данных есть аналогичные функции: Number() для чисел, Boolean() для булевых значений. Все эти функции так же являются наследниками объекта.

Главное отличие между объектами (массивами, функциями) и примитивами в том, что примитивы неизменяемые. Попытка изменения или добавления свойств к примитиву ничего не сделает.

        
          
          const cat = 'Boris'cat.color = 'red' // свойство не добавитсяdelete color.length // также ничего не изменитсяconst cats = ['Boris', 'Vasya', 'Murzik']cats.length = 5 // теперь массив стал длинной в 5 элементовcats.someField = 'value' // добавилось полеconsole.dir(cats)/*{  0: "Boris",  1: "Vasya",  2: "Murzik",  someField: "value",  length: 5}*/
          const cat = 'Boris'

cat.color = 'red' // свойство не добавится
delete color.length // также ничего не изменится

const cats = ['Boris', 'Vasya', 'Murzik']
cats.length = 5 // теперь массив стал длинной в 5 элементов
cats.someField = 'value' // добавилось поле

console.dir(cats)
/*
{
  0: "Boris",
  1: "Vasya",
  2: "Murzik",
  someField: "value",
  length: 5
}
*/

        
        
          
        
      

Но не стоит путать примитив и объект, созданный через конструктор для примитива:

        
          
          const cat = new String('Boris')cat.color = 'black'// добавится, так как в cat лежит объект, а не строка
          const cat = new String('Boris')
cat.color = 'black'
// добавится, так как в cat лежит объект, а не строка

        
        
          
        
      

Как пишется

Скопировано

У объектов и массивов поля и методы можно вызывать всегда: и через переменную, и инлайн (inline), то есть без использования переменной.

        
          
          const array = [1, 2, 3, 4]console.log(array[1])// 2const pos = 3console.log(array[pos])// 4console.log(array.map(a => a + 1))// [2, 3, 4, 5]const f = 'map'console.log(array[f](a => a + 1))// [2, 3, 4, 5]const obj = { name: 'Boris', color: 'red' }console.log(obj.color)// 'red'console.log(obj['name']);// 'Boris'const age = Object.assign(obj, {  name: 'Vasya',  age: 30}).ageconsole.log(age)// 30
          const array = [1, 2, 3, 4]
console.log(array[1])
// 2
const pos = 3
console.log(array[pos])
// 4

console.log(array.map(a => a + 1))
// [2, 3, 4, 5]
const f = 'map'
console.log(array[f](a => a + 1))
// [2, 3, 4, 5]

const obj = { name: 'Boris', color: 'red' }
console.log(obj.color)
// 'red'
console.log(obj['name']);
// 'Boris'

const age = Object.assign(obj, {
  name: 'Vasya',
  age: 30
}).age
console.log(age)
// 30

        
        
          
        
      

Почти у всех примитивов без переменной тоже можно обращаться к методам:

        
          
          true.toString()// 'true'Infinity.toString()// 'Infinity''hello world'.toString()// 'hello world'Symbol('tag').toString()// 'Symbol(tag)'9007199254740991n.toString()// '9007199254740991'
          true.toString()
// 'true'

Infinity.toString()
// 'Infinity'

'hello world'.toString()
// 'hello world'

Symbol('tag').toString()
// 'Symbol(tag)'

9007199254740991n.toString()
// '9007199254740991'

        
        
          
        
      

Правда, в случае с числами можно получить синтаксическую ошибку, потому что точка воспринимается как часть самого числа:

        
          
          42.toString()// Uncaught SyntaxError: Invalid or unexpected token
          42.toString()
// Uncaught SyntaxError: Invalid or unexpected token

        
        
          
        
      

Чтобы этого избежать, можно использовать две точки, взять выражение в скобки или вызвать обёртку примитивного типа:

        
          
          42..toString()// '42'(42).toString()// '42'Number(42).toString()// '42'
          42..toString()
// '42'

(42).toString()
// '42'

Number(42).toString()
// '42'

        
        
          
        
      

Вызов методов или свойств не сработает у null и undefined:

        
          
          null.toString()// Uncaught TypeError: Cannot read property 'toString' of nullnull.valueOf()// Uncaught TypeError: Cannot read property 'valueOf' of nullnull.length// Uncaught TypeError: Cannot read property 'length' of nullundefined.toString()// Uncaught TypeError: Cannot read property 'toString' of undefinedundefined.valueOf()// Uncaught TypeError: Cannot read property 'valueOf' of undefinedundefined.length// Uncaught TypeError: Cannot read property 'length' of undefined
          null.toString()
// Uncaught TypeError: Cannot read property 'toString' of null

null.valueOf()
// Uncaught TypeError: Cannot read property 'valueOf' of null

null.length
// Uncaught TypeError: Cannot read property 'length' of null

undefined.toString()
// Uncaught TypeError: Cannot read property 'toString' of undefined

undefined.valueOf()
// Uncaught TypeError: Cannot read property 'valueOf' of undefined

undefined.length
// Uncaught TypeError: Cannot read property 'length' of undefined

        
        
          
        
      

На практике

Скопировано

Егор Огарков советует

Скопировано

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

На собеседовании

Скопировано
Задать вопрос в рубрику
🤚 Я знаю ответ

Viktar Nezhbart  отвечает

Скопировано

Начнём с лаконичного определения, зафиксированного в спецификации ECMAScript:

Прототип — это объект, предоставляющий другим объектам общие (shared) свойства.

В свою очередь MDN определяет прототипы как механизм, благодаря которому объекты получают доступ (inherit) к свойствам (features) других объектов.

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

При попытке обратиться к свойству, которое не определено в самом объекте, производится поиск в прототипе объекта, а затем в прототипе прототипа и далее, пока искомое свойство не будет найдено или не будет достигнут конец цепочки прототипов (prototype chain), так как у базового объекта Object.prototype прототипом является null.

У каждого объекта есть встроенное свойство, указывающее на его прототип. Попробуем его получить напрямую:

☝️ При запуске console.dir() в Node.js для просмотра скрытых свойств объекта потребуются дополнительные параметры. Здесь и далее приводятся результаты выполнения в Node.js.

        
          
          const obj = { name: 'Объект' }console.dir(obj.prototype, {showHidden: true, depth: 0 })// undefined
          const obj = { name: 'Объект' }
console.dir(obj.prototype, {showHidden: true, depth: 0 })
// undefined

        
        
          
        
      

Ничего не вышло. Дело в том, что для доступа к прототипу следует использовать специальные методы.

💡 Статический метод Object.getPrototypeOf() возвращает прототип объекта.

        
          
          const obj = { name: 'Объект' }console.dir(Object.getPrototypeOf(obj), {showHidden: true, depth: 0 })// [Object: null prototype] {//   [constructor]: [Function],//   [__defineGetter__]: [Function],//   [__defineSetter__]: [Function],//   [hasOwnProperty]: [Function],//   [__lookupGetter__]: [Function],//   [__lookupSetter__]: [Function],//   [isPrototypeOf]: [Function],//   [propertyIsEnumerable]: [Function],//   [toString]: [Function],//   [valueOf]: [Function],//   ['__proto__']: [Getter/Setter],//   [toLocaleString]: [Function]// }
          const obj = { name: 'Объект' }
console.dir(Object.getPrototypeOf(obj), {showHidden: true, depth: 0 })
// [Object: null prototype] {
//   [constructor]: [Function],
//   [__defineGetter__]: [Function],
//   [__defineSetter__]: [Function],
//   [hasOwnProperty]: [Function],
//   [__lookupGetter__]: [Function],
//   [__lookupSetter__]: [Function],
//   [isPrototypeOf]: [Function],
//   [propertyIsEnumerable]: [Function],
//   [toString]: [Function],
//   [valueOf]: [Function],
//   ['__proto__']: [Getter/Setter],
//   [toLocaleString]: [Function]
// }

        
        
          
        
      

Постойте, а как насчёт функций-конструкторов? Ведь они имеют свойство prototype доступное напрямую. Свойство prototype и прототип функции-конструктора (ведь функция это тоже объект) — это не одно и то же:

        
          
          // Функция-конструкторfunction Person(name) {  this.name = name;}console.dir(Person.prototype, {showHidden: true, depth: 0 })// { [constructor]: [Function] }console.dir(Object.getPrototypeOf(Person), {showHidden: true, depth: 0 })// {//   [length]: 0,//   [name]: '',//   [arguments]: [Getter/Setter],//   [caller]: [Getter/Setter],//   [constructor]: [Function],//   [apply]: [Function],//   [bind]: [Function],//   [call]: [Function],//   [toString]: [Function],//   [Symbol(Symbol.hasInstance)]: [Function]// }
          // Функция-конструктор
function Person(name) {
  this.name = name;
}

console.dir(Person.prototype, {showHidden: true, depth: 0 })
// { [constructor]: [Function] }

console.dir(Object.getPrototypeOf(Person), {showHidden: true, depth: 0 })
// {
//   [length]: 0,
//   [name]: '',
//   [arguments]: [Getter/Setter],
//   [caller]: [Getter/Setter],
//   [constructor]: [Function],
//   [apply]: [Function],
//   [bind]: [Function],
//   [call]: [Function],
//   [toString]: [Function],
//   [Symbol(Symbol.hasInstance)]: [Function]
// }

        
        
          
        
      

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

Как у объекта появляется прототип?

  • Прототип объекта можно указать при создании объекта с помощью Object.create():
        
          
          // Объект-прототипconst shape = { color: 'green' }// создадим новый объект на основе объекта-прототипаconst myShape = Object.create(shape)// Добавим свойство в объект-прототипshape.isCircle = true// Получим доступ к свойствуconsole.log(myShape.isCircle)// true
          // Объект-прототип
const shape = { color: 'green' }

// создадим новый объект на основе объекта-прототипа
const myShape = Object.create(shape)

// Добавим свойство в объект-прототип
shape.isCircle = true

// Получим доступ к свойству
console.log(myShape.isCircle)
// true

        
        
          
        
      

☝️ Обратите внимание: все изменения объекта-прототипа shape будут доступны в объекте myShape, даже если эти изменения произойдут после создания объекта myShape.

  • Когда объект создаётся с помощью конструктора, прототип объекта назначается в соответствии со значением поля prototype функции-конструктора:
        
          
          // Функция-конструкторfunction Bear(name) {  this.name = name}// Добавим свойство в BearBear.id = 'медведь'// Создадим новый объектconst panda = new Bear('Панда')// Получим прототип объектаconst pandaPrototype = Object.getPrototypeOf(panda)// А теперь добавим свойство в Bear.prototypeBear.prototype.isBear = true;// Отобразим свойства созданного объектаconsole.dir(panda, {showHidden: true })// Bear { name: 'Панда', isBear: true }// Убедимся, что свойство prototype конструктора является прототипом объектаconsole.log(Object.is(Bear.prototype, pandaPrototype))// true// Убедимся, что свойство prototype и прототип конструктора это не одно и то жеconsole.log(Object.is(Bear.prototype, Object.getPrototypeOf(Bear))// false
          // Функция-конструктор
function Bear(name) {
  this.name = name
}

// Добавим свойство в Bear
Bear.id = 'медведь'

// Создадим новый объект
const panda = new Bear('Панда')

// Получим прототип объекта
const pandaPrototype = Object.getPrototypeOf(panda)

// А теперь добавим свойство в Bear.prototype
Bear.prototype.isBear = true;

// Отобразим свойства созданного объекта
console.dir(panda, {showHidden: true })
// Bear { name: 'Панда', isBear: true }

// Убедимся, что свойство prototype конструктора является прототипом объекта
console.log(Object.is(Bear.prototype, pandaPrototype))
// true

// Убедимся, что свойство prototype и прототип конструктора это не одно и то же
console.log(Object.is(Bear.prototype, Object.getPrototypeOf(Bear))
// false

        
        
          
        
      

☝️ Обратите внимание: свойство id не наследуется объектом panda, потому что находится в самом объекте Bear. Свойство isBear наследуется, хотя было добавлено в Bear.prototype уже после создания объекта panda.

💡 Статический метод Object.is() проверяет, являются ли переданные ему в качестве аргументов значения идентичными.

  • Когда объект создаётся как экземпляр класса, прототип объекта назначается в соответствии со значением поля prototype объекта родительского класса:
        
          
          // Родительский классclass Person {  constructor(name) {    this.name = name  }}// Добавим метод в объект родительского классаPerson.getSkill = function() {  return this.skill}// Создадим экземпляр классаconst person = new Person('Иван')// Добавим свойство в Person.prototypePerson.prototype.setSkill = function(skill) {  this.skill = skill}// Добавим Ивану умение работать курьеромperson.setSkill('Курьер')// Получим прототип объектаconst proto = Object.getPrototypeOf(person)// Отобразим свойства созданного объектаconsole.dir(person, {showHidden: true})// Person { name: 'Иван', skill: 'Курьер' }// Убедимся, что свойство Person.prototype является прототипом объектаconsole.log(Object.is(Person.prototype, proto))// true
          // Родительский класс
class Person {
  constructor(name) {
    this.name = name
  }
}

// Добавим метод в объект родительского класса
Person.getSkill = function() {
  return this.skill
}

// Создадим экземпляр класса
const person = new Person('Иван')

// Добавим свойство в Person.prototype
Person.prototype.setSkill = function(skill) {
  this.skill = skill
}

// Добавим Ивану умение работать курьером
person.setSkill('Курьер')

// Получим прототип объекта
const proto = Object.getPrototypeOf(person)

// Отобразим свойства созданного объекта
console.dir(person, {showHidden: true})
// Person { name: 'Иван', skill: 'Курьер' }

// Убедимся, что свойство Person.prototype является прототипом объекта
console.log(Object.is(Person.prototype, proto))
// true

        
        
          
        
      

☝️ Обратите внимание: свойство getSkill не наследуется объектом person, а свойство setSkill наследуется, хотя было добавлено в Person.prototype уже после создания объекта person.

Возможно ли изменить прототип созданного объекта?

Да, но крайне не рекомендуется. Для этого можно использовать метод setPrototypeOf():

        
          
          // Объект-прототипconst pants = {  color: 'black',  showInfo: () => console.log('Брюки')}// Создадим новый объектconst myPants = Object.create(pants)myPants.size = 48myPants.showInfo()// Брюкиconsole.dir(myPants, {showHidden: true, depth: 0 })// { size: 48, color: 'black' }// Новый объект-прототипconst shorts = {  color: 'white',  showInfo: () => console.log('Элегантные шорты')};// Брюки превращаются…Object.setPrototypeOf(myPants, shorts)myPants.showInfo()// Элегантные шортыconsole.dir(myPants, {showHidden: true, depth: 0 })// { size: 48, color: 'white' }
          // Объект-прототип
const pants = {
  color: 'black',
  showInfo: () => console.log('Брюки')
}

// Создадим новый объект
const myPants = Object.create(pants)

myPants.size = 48
myPants.showInfo()
// Брюки

console.dir(myPants, {showHidden: true, depth: 0 })
// { size: 48, color: 'black' }

// Новый объект-прототип
const shorts = {
  color: 'white',
  showInfo: () => console.log('Элегантные шорты')
};

// Брюки превращаются…
Object.setPrototypeOf(myPants, shorts)

myPants.showInfo()
// Элегантные шорты

console.dir(myPants, {showHidden: true, depth: 0 })
// { size: 48, color: 'white' }

        
        
          
        
      

⚠️ Следует избегать изменения прототипа у существующего объекта, так как это сильно снижает производительность.