Кратко
СкопированоВ JavaScript объект является прародителем всех других сущностей. Все типы данных и структуры, кроме примитивных, являются потомками объекта. По этой причине абсолютно у всех наследников объекта имеется набор общих методов: to
, value
и другие.
Как понять
СкопированоМассивы и функции
СкопированоОбъект — это сущность с набором свойств. Мы можем добавлять, менять и удалять эти свойства.
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)
В выводе есть и свойство some
, которое мы присвоили, и набор встроенных свойств и методов.
Такая структура массивов и функций очень похожа на структуру объектов. Но на самом деле это и есть объекты, и в этом можно легко убедиться.
Давайте посмотрим на свойство _
у функции sum
, описанной выше.
Свойство _
является устаревшим (deprecated), не используйте его в коде, особенно для того, чтобы самостоятельно устанавливать прототип.
Если посмотреть свойство прототипа, то можно заметить, что прототипом текущего прототипа является объект. Заглянув в этот прототип, можно увидеть такую картину:
В этой цепочке следующего прототипа уже нет, а это значит, что мы дошли до самого конца цепочки, то есть нашли прародителя. Если подобным образом вывести в консоль любой массив, то спускаясь ниже по цепочке прототипов, в конце обязательно будет именно прототип объекта. Любая сущность в 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 самостоятельно решит что сделать, чтобы выдать нужный результат.
На собеседовании
Скопировано отвечает
СкопированоНачнём с лаконичного определения, зафиксированного в спецификации ECMAScript:
Прототип — это объект, предоставляющий другим объектам общие (shared) свойства.
В свою очередь MDN определяет прототипы как механизм, благодаря которому объекты получают доступ (inherit) к свойствам (features) других объектов.
Прототип позволяет указать какие свойства будут доступны созданным от него объектам, а также предоставляет доступ к своему собственному прототипу.
При попытке обратиться к свойству, которое не определено в самом объекте, производится поиск в прототипе объекта, а затем в прототипе прототипа и далее, пока искомое свойство не будет найдено или не будет достигнут конец цепочки прототипов (prototype chain), так как у базового объекта Object
прототипом является null
.
У каждого объекта есть встроенное свойство, указывающее на его прототип. Попробуем его получить напрямую:
☝️ При запуске console
в 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
возвращает прототип объекта.
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
будут доступны в объекте my
, даже если эти изменения произойдут после создания объекта my
.
- Когда объект создаётся с помощью конструктора, прототип объекта назначается в соответствии со значением поля
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
. Свойство is
наследуется, хотя было добавлено в Bear
уже после создания объекта panda
.
💡 Статический метод Object
проверяет, являются ли переданные ему в качестве аргументов значения идентичными.
- Когда объект создаётся как экземпляр класса, прототип объекта назначается в соответствии со значением поля
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
☝️ Обратите внимание: свойство get
не наследуется объектом person
, а свойство set
наследуется, хотя было добавлено в Person.prototype уже после создания объекта person
.
Возможно ли изменить прототип созданного объекта?
Да, но крайне не рекомендуется. Для этого можно использовать метод set
:
// Объект-прототип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' }
⚠️ Следует избегать изменения прототипа у существующего объекта, так как это сильно снижает производительность.