7 июня 2022 г.

Объекты и прототипы

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/object.

В этом разделе мы рассмотрим нововведения, которые касаются именно объектов.

По классам – чуть позже, в отдельном разделе, оно того заслуживает.

Короткое свойство

Зачастую у нас есть переменные, например, 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

Функция 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);

Object.is(value1, value2)

Новая функция для проверки равенства значений.

Возвращает 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 термин «метод объекта» был просто альтернативным названием для свойства-функции.

Теперь это уже не так. Добавлены именно «методы объекта», которые, по сути, являются свойствами-функциями, привязанными к объекту.

Их особенности:

  1. Более короткий синтаксис объявления.
  2. Наличие в методах специального внутреннего свойства [[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». Теперь перейдём к другим отличиям.

super

В 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 равно самому себе.
Карта учебника