38. Объекты и прототипы JavaScript
В этом разделе мы рассмотрим нововведения, которые касаются именно объектов.
По классам – чуть позже, в отдельном разделе, оно того заслуживает.
Зачастую у нас есть переменные, например, name и isAdmin, и мы хотим использовать их в объекте.
При объявлении объекта в этом случае достаточно указать только имя свойства, а значение будет взято из переменной с аналогичным именем.
Например:
'use strict';
let name = "Вася";
let isAdmin = true;
let user = {
name,
isAdmin
};
alert( JSON.stringify(user) ); // {"name": "Вася", "isAdmin": true}
В качестве имени свойства можно использовать выражение, например:
'use strict';
let propName = "firstName";
let user = {
[propName]: "Вася"
};
alert( user.firstName ); // Вася
Или даже так:
'use strict';
let a = "Мой ";
let b = "Зелёный ";
let c = "Крокодил";
let user = {
[(a + b + c).toLowerCase()]: "Гена"
};
alert( user["мой зелёный крокодил"] ); // Гена
В ES5 для прототипа был метод-геттер:
- Object.getPrototypeOf(obj)
В ES-2015 также добавился сеттер:
- Object.setPrototypeOf(obj, newProto)
…А также «узаконено» свойство __proto__, которое даёт прямой доступ к прототипу. Его, в качестве «нестандартного», но удобного способа работы с прототипом, реализовали почти все браузеры (кроме IE10-), так что было принято решение добавить его в стандарт.
Функция Object.assign получает список объектов и копирует в первый target свойства из остальных.
Синтаксис:
Object.assign(target, src1, src2...)
При этом последующие свойства перезаписывают предыдущие.
Например:
'use strict';
let user = { name: "Вася" };
let visitor = { isAdmin: false, visits: true };
let admin = { isAdmin: true };
Object.assign(user, visitor, admin);
// user <- visitor <- admin
alert( JSON.stringify(user) ); // name: Вася, visits: true, isAdmin: true
Его также можно использовать для 1-уровневого клонирования объекта:
'use strict';
let user = { name: "Вася", isAdmin: false };
// clone = пустой объект + все свойства user
let clone = Object.assign({}, user);
Новая функция для проверки равенства значений.
Возвращает true, если значения value1 и value2 равны, иначе false.
Она похожа на обычное строгое равенство ===, но есть отличия:
// Сравнение +0 и -0
alert( Object.is(+0, -0)); // false
alert( +0 === -0 ); // true
// Сравнение с NaN
alert( Object.is(NaN, NaN) ); // true
alert( NaN === NaN ); // false
Отличия эти в большинстве ситуаций некритичны, так что непохоже, чтобы эта функция вытеснила обычную проверку ===. Что интересно – этот алгоритм сравнения, который называется SameValue, применяется во внутренних реализациях различных методов современного стандарта.
Долгое время в JavaScript термин «метод объекта» был просто альтернативным названием для свойства-функции.
Теперь это уже не так. Добавлены именно «методы объекта», которые, по сути, являются свойствами-функциями, привязанными к объекту.
Их особенности:
- Более короткий синтаксис объявления.
- Наличие в методах специального внутреннего свойства [[HomeObject]] («домашний объект»), ссылающегося на объект, которому метод принадлежит. Мы посмотрим его использование чуть дальше.
Для объявления метода вместо записи "prop: function() {…}" нужно написать просто "prop() { … }".
Например:
'use strict';
let name = "Вася";
let user = {
name,
// вместо "sayHi: function() {" пишем "sayHi() {"
sayHi() {
alert(this.name);
}
};
user.sayHi(); // Вася
Как видно, для создания метода нужно писать меньше букв. Что же касается вызова – он ничем не отличается от обычной функции. На данном этапе можно считать, что «метод» – это просто сокращённый синтаксис для свойства-функции. Дополнительные возможности, которые даёт такое объявление, мы рассмотрим позже.
Также методами станут объявления геттеров get prop() и сеттеров set prop():
'use strict';
let name = "Вася", surname="Петров";
let user = {
name,
surname,
get fullName() {
return `${name} ${surname}`;
}
};
alert( user.fullName ); // Вася Петров
Можно задать и метод с вычисляемым названием:
'use strict';
let methodName = "getFirstName";
let user = {
// в квадратных скобках может быть любое выражение,
// которое должно вернуть название метода
[methodName]() { // вместо [methodName]: function() {
return "Вася";
}
};
alert( user.getFirstName() ); // Вася
Итак, мы рассмотрели синтаксические улучшения. Если коротко, то не надо писать слово «function». Теперь перейдём к другим отличиям.
В ES-2015 появилось новое ключевое слово super. Оно предназначено только для использования в методах объекта.
Вызов super.parentProperty позволяет из метода объекта получить свойство его прототипа.
Например, в коде ниже rabbit наследует от animal.
Вызов super.walk() из метода объекта rabbit обращается к animal.walk():
'use strict';
let animal = {
walk() {
alert("I'm walking");
}
};
let rabbit = {
__proto__: animal,
walk() {
alert(super.walk); // walk() { … }
super.walk(); // I'm walking
}
};
rabbit.walk();
Как правило, это используется в классах, которые мы рассмотрим в следующем разделе, но важно понимать, что «классы» здесь на самом деле ни при чём. Свойство super работает через прототип, на уровне методов объекта.
При обращении через super используется [[HomeObject]] текущего метода, и от него берётся __proto__. Поэтому super работает только внутри методов.
В частности, если переписать этот код, оформив rabbit.walk как обычное свойство-функцию, то будет ошибка:
'use strict';
let animal = {
walk() {
alert("I'm walking");
}
};
let rabbit = {
__proto__: animal,
walk: function() { // Надо: walk() {
super.walk(); // Будет ошибка!
}
};
rabbit.walk();
Ошибка возникнет, так как rabbit.walk теперь обычная функция и не имеет [[HomeObject]]. Поэтому в ней не работает super.
Исключением из этого правила являются функции-стрелки. В них используется super внешней функции. Например, здесь функция-стрелка в setTimeout берёт внешний super:
'use strict';
let animal = {
walk() {
alert("I'm walking");
}
};
let rabbit = {
__proto__: animal,
walk() {
setTimeout(() => super.walk()); // I'm walking
}
};
rabbit.walk();
Ранее мы говорили о том, что у функций-стрелок нет своего this, arguments: они используют те, которые во внешней функции. Теперь к этому списку добавился ещё и super.
Свойство [[HomeObject]] – не изменяемое
При создании метода – он привязан к своему объекту навсегда. Технически можно даже скопировать его и запустить отдельно, и super продолжит работать:
'use strict';
let animal = {
walk() { alert("I'm walking"); }
};
let rabbit = {
__proto__: animal,
walk() {
super.walk();
}
};
let walk = rabbit.walk; // скопируем метод в переменную
walk(); // вызовет animal.walk()
// I'm walking
В примере выше метод walk() запускается отдельно от объекта, но всё равно, благодаря [[HomeObject]], сохраняется доступ к его прототипу через super.
Это – скорее технический момент, так как методы объекта, всё же, предназначены для вызова в контексте этого объекта. В частности, правила для this в методах – те же, что и для обычных функций. В примере выше при вызове walk() без объекта this будет undefined.
Улучшения в описании свойств:
- Запись name: name можно заменить на просто name
- Если имя свойства находится в переменной или задано выражением expr, то его можно указать в квадратных скобках [expr].
- Свойства-функции можно оформить как методы: "prop: function() {}" → "prop() {}".
В методах работает обращение к свойствам прототипа через super.parentProperty.
Для работы с прототипом:
- Object.setPrototypeOf(obj, proto) – метод для установки прототипа.
- obj.__proto__ – ссылка на прототип.
Дополнительно:
- Метод Object.assign(target, src1, src2...) – копирует свойства из всех аргументов в первый объект.
- Метод Object.is(value1, value2) проверяет два значения на равенство. В отличие от === считает +0 и -0 разными числами. А также считает, что NaN равно самому себе.