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