JavaScript. Объекты. Часть вторая из двух
30.06.2021
Теги: JavaScript • Web-разработка • Класс • ООП • Теория • Функция
Прототипное наследование
Допустим, у нас есть объект user
со своими свойствами и методами, и нужно создать объекты admin
и guest
как его слегка изменённые варианты. Хотелось бы повторно использовать то, что есть у объекта user
, не копировать/переопределять его методы, а просто создать новый объект на его основе.
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]]
, которое либо равно null
, либо ссылается на другой объект — прототип. Когда мы хотим прочитать свойство объекта, а оно отсутствует, JavaScript автоматически берёт его из прототипа.Такой механизм называется «прототипным наследованием».
Есть много способов задать [[Prototype]]
, одним из них является использование __proto__
:
let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal;
Обратите внимание, что __proto__
— не то же самое, что [[Prototype]]
— это геттер/сеттер для него. Он существует по историческим причинам, в современном языке его заменяют функции Object.getPrototypeOf
и Object.setPrototypeOf
, которые также получают и устанавливают прототип.
Если мы ищем свойство в rabbit
, а оно отсутствует, JavaScript автоматически берёт его из animal
:
let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // теперь мы можем найти оба свойства в rabbit console.log(rabbit.eats); // true console.log(rabbit.jumps); // true
Если у нас есть метод в animal
, он может быть вызван на rabbit
:
let animal = { eats: true, walk() { console.log('Animal walk'); } }; let rabbit = { __proto__: animal jumps: true, }; rabbit.walk(); // Animal walk
Прототип используется только для чтения свойств. Операции записи/удаления работают напрямую с объектом. В приведённом ниже примере мы присваиваем rabbit
собственный метод walk
:
let animal = { eats: true, walk() { console.log('Animal walk'); } }; let rabbit = { __proto__: animal, jumps: true, walk() { console.log('Rubbit walk'); } }; rabbit.walk(); // Rubbit walk
Свойства-аксессоры — исключение, так как запись в него обрабатывается функцией-сеттером. То есть, это, фактически, вызов функции. По этой причине admin.fullName
работает корректно в приведённом ниже коде:
let user = { name: 'Unknown', surname: 'Unknown', set fullName(value) { [this.name, this.surname] = value.split(' '); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { __proto__: user, isAdmin: true }; console.log(admin.fullName); // Unknown Unknown admin.fullName = 'Сергей Иванов'; console.log(admin.name); // Сергей console.log(admin.surname); // Иванов
this
? Ответ прост — прототипы никак не влияют на this
. Неважно, где находится метод — в объекте или его прототипе. При вызове метода this
— всегда объект перед точкой.
Цикл for…in
Цикл for…in
проходит не только по собственным, но и по унаследованным свойствам объекта:
let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; for(let key in rabbit) { let value = rabbit[key] console.log('key =', key, 'value =', value); }
key = jumps value = true key = eats value = true
Оператор in
Оператор in
возвращает true
как для собственных, так и для унаследованных свойств объекта:
let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; console.log('eats' in rabbit); // true console.log('jumps' in rabbit); // true
Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе:
let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; console.log(rabbit.hasOwnProperty('jumps')); // true — jumps принадлежит rabbit console.log(rabbit.hasOwnProperty('eats')); // false — eats не принадлежит rabbit
Примеси
Допустим, у нас есть класс User
, который реализует пользователей, и класс EventEmitter
, реализующий события. Мы хотели бы добавить функциональность класса EventEmitter
к User
, чтобы пользователи могли легко генерировать события. Для таких случаев существуют «примеси».
Простейший способ реализовать примесь в JavaScript — это создать объект с полезными методами, которые затем могут быть легко добавлены в прототип любого класса.
// объект примеси let helloByeMixin = { hello() { console.log(`Привет, ${this.name}`); }, bye() { console.log(`Пока, ${this.name}`); } }; // класс пользователя class User { constructor(name) { this.name = name; } } // копируем методы Object.assign(User.prototype, helloByeMixin); // теперь User может сказать Привет new User('Сергей').hello(); // Привет, Сергей!
Классы (современный синтаксис)
Мы уже знаем, как создать класс через функцию-конструктор, теперь посмотрим, какие возможности для объектно-ориентированного программирования предоставляет современный JavaScript при использовании ключевого слова class
.
class User { constructor(name) { this.name = name; } hello() { console.log(`Привет, ${this.name}`); } bye() { console.log(`Пока, ${this.name}`); } }
Теперь можно использовать вызов new User()
для создания нового объекта со всеми перечисленными методами. При этом автоматически вызывается метод constructor()
, в нём можно инициализировать объект.
let user = new User('Сергей'); user.hello(); // Привет, Сергей
Что такое класс в JavaScript? Класс — это разновидность функции. Давайте разберемся, что делает конструкция class User {…}
на самом деле.
- Создаёт функцию с именем
User
, тело берёт изconstructor
- Сохраняет методы
hello
иbye
вUser.prototype
При вызове метода объекта user
он будет взят из прототипа User.prototype
. Таким образом, объекты user
имеют доступ к методам класса.
Иногда говорят, что class
— это просто «синтаксический сахар» в JavaScript, потому что мы можем сделать всё то же самое без использования class
.
function User(name) { this.name = name; } User.prototype = { hello() { console.log(`Привет, ${this.name}`); }, bye() { console.log(`Пока, ${this.name}`); } }
Это не совсем так. Методы класса являются неперечисляемыми. Определение класса устанавливает флаг enumerable
в false
для всех методов в prototype
. И это хорошо, так как если мы проходимся циклом for…in
по объекту, то обычно мы не хотим при этом получать методы класса. И классы всегда используют use strict
. Весь код внутри класса автоматически находится в строгом режиме.
Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д.
class User { constructor(name) { // вызывает сеттер this.name = name; } get name() { return this._name; } set name(value) { if (value.length < 4) { console.log('Имя слишком короткое'); return; } this._name = value; } } let user = new User('Сергей'); user.hello(); // Привет, Сергей! user = new User(''); // Имя слишком короткое
При объявлении класса геттеры/сеттеры создаются на User.prototype
вот так:
Object.defineProperties(User.prototype, { name: { get() { return this._name }, set(name) { // ..... } } });
Пример с вычисляемым свойством в скобках [...]
:
class User { ['hel' + 'lo']() { console.log('Привет'); } } new User().hello();
В приведённом выше примере у класса User
были только методы. Давайте добавим свойство:
class User { name = 'Аноним'; constructor(name) { if (name) { this.name = name; } } hello() { console.log(`Привет, ${this.name}!`); } } new User().hello(); new User('Сергей').hello();
Привет, Аноним! Привет, Сергей!
Свойство name
не устанавливается в User.prototype
. Вместо этого оно создаётся оператором new
перед запуском конструктора, это именно свойство объекта. Свойства классов добавлены в язык недавно — так что для старых браузеров может понадобиться полифил.
MyClass
является функцией, тело которой задается в constructor
, в то время как методы, геттеры и сеттеры записываются в MyClass.prototype
. Свойство класса не записывается в MyClass.prototype
— это именно свойство объекта, который возвращает вызов new MyClass()
.
Методы Object
Метод Object.keys()
возвращает массив собственных перечисляемых свойств объекта:
let user = { name: 'Сергей', surname: 'Иванов' }; let keys = Object.keys(user); // [name, surname] keys.forEach(key => { console.log('key =', key, 'value =', user[key]); });
key = name value = Сергей key = surname value = Иванов
Метод Object.values()
возвращает массив значений собственных перечисляемых свойств объекта:
let user = { name: 'Сергей', surname: 'Иванов' }; let values = Object.values(user); // [Сергей, Иванов] values.forEach(value => { console.log('value =', value); });
value = Сергей value = Иванов
Метод Object.entries()
метод возвращает массив собственных перечисляемых свойств и значений:
let user = { name: 'Сергей', surname: 'Иванов' }; let entries = Object.entries(user); // [["name", "Сергей"], ["surname", "Иванов"]] entries.forEach(([key, value]) => { console.log('key = ', key, 'value =', value); }); for (let [key, value] of entries) { console.log('key = ', key, 'value =', value); }
key = name value = Сергей key = surname value = Иванов
Метод Object.create()
создаёт новый объект с указанным прототипом и свойствами:
let animal = { eats: true }; let rabbit = Object.create(animal, { jumps: { writable: true, configurable: true, value: true } }); console.log(rabbit.__proto__); // {eats: true}
// создание объекта... let obj = {}; // ...эквивалентно этому let oobj = Object.create(Object.prototype);
Метод Object.getPrototypeOf()
возвращает прототип объекта:
let animal = { 'eats': true }; let rabbit = { __proto__: animal, 'jumps': true }; let proto = Object.getPrototypeOf(rabbit); console.log(proto); // {eats: true}
Метод Object.setPrototypeOf()
устанавливает прототип объекта:
let animal = { 'eats': true }; let rabbit = { 'jumps': true }; Object.setPrototypeOf(rabbit, animal); console.log(rabbit.__proto__); // {eats: true}
Метод Object.assign()
копирует свойства из одного объекта (или объектов) в другой:
let user = {name: 'Иван'} let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; Object.assign(user, permissions1, permissions2); console.log(user); // {name: "Иван", canView: true, canEdit: true}
Метод Object.getOwnPropertyNames()
возвращает массив со всеми свойствами (независимо от того, перечисляемые они или нет), найденными непосредственно в переданном объекте.
Метод Object.getOwnPropertyDescriptor()
возвращает дескриптор свойства для собственного свойства (которое находится непосредственно в объекте) переданного объекта.
Метод Object.getOwnPropertyDescriptors()
возвращает все дескрипторы собственных свойств (которые находятся непосредственно в объекте) переданного объекта.
Поиск: JavaScript • Web-разработка • Класс • Теория • Функция • Конструктор • ООП • Объект • Object • Прототип • Prototype