Робота з об'єктами

JavaScript збудовано на основі простої, заснованої на об'єктах парадигми. Об'єкт — це колекція властивостей, а властивість — це зв'язок між назвою (або іменем чи ключем) та значенням. Значенням властивості може бути функція — таку властивість називають методом.

Об'єкти в JavaScript, як і в багатьох інших мовах програмування, можна порівняти з об'єктами реального світу. В JavaScript об'єкт є окремою сутністю, що має властивості й тип. Порівняймо його, наприклад, з чашкою. Чашка є об'єктом, що має властивості. Чашка має колір, конструкцію, вагу, певний матеріал тощо. Так само об'єкти JavaScript можуть мати властивості, котрі визначають їх характеристики.

Додатково до об'єктів, уже визначених в браузері, можна створювати також власні об'єкти. Ця глава описує, як використовувати об'єкти, властивості, функції й методи, та як створювати власні об'єкти.

Створення нових об'єктів

Свої об'єкти можна створити, застосувавши об'єктний ініціалізатор. Окрім цього, можна також спершу створити функцію-конструктор, і потім реалізувати новий об'єкт шляхом звертання до функції з оператором new.

Застосування об'єктних ініціалізаторів

Об'єктні ініціалізатори також звуть літералами об'єктів. Термін "об'єктний ініціалізатор" сумісний з термінологією, прийнятою в середовищі C++.

Синтаксис створення об'єкта за допомогою об'єктного ініціалізатора має такий вигляд:

const obj = {
  property1: value1, // назва властивості може бути ідентифікатором
  2: value2, // або числом
  "property n": value3, // чи рядком
};

Кожна назва властивості до двокрапки — ідентифікатор (або ім'я, або число, або рядковий літерал), та кожний valueN — вираз, чиє значення присвоюється назві властивості. Ім'я властивості також може бути виразом; обчислювані ключі повинні бути огорнуті квадратними дужками. Довідкова сторінка об'єктних ініціалізаторів містить детальніше пояснення синтаксису.

У цьому прикладі новостворений об'єкт присвоюється змінній obj, – це необов'язково. Якщо немає потреби звертатися до такого об'єкта деінде, не обов'язково присвоювати його змінній. (Зверніть увагу, що може бути потреба загорнути літерал об'єкта в дужки, якщо об'єкт зустрічається там, де очікується інструкція, щоб літерал не був сприйнятий за блокову інструкцію.)

Об'єктні ініціалізатори – це вирази, і кожний такий ініціалізатор призводить до створення нового об'єкта щоразу, коли виконується інструкція, в якій він зустрічається. Ідентичні об'єктні ініціалізатори створюють окремі об'єкти, котрі, як їх порівняти, не вважатимуться рівними одне одному.

Наступні інструкції створюють об'єкт і присвоюють його змінній x тоді й лише тоді, якщо вираз cond є істинним:

let x;
if (cond) {
  x = { greeting: "Гей там" };
}

Наступний приклад створює об'єкт myHonda з трьома властивостями. Зауважте, що властивість engine — це також об'єкт зі своїми власними властивостями.

const myHonda = {
  color: "red",
  wheels: 4,
  engine: { cylinders: 4, size: 2.2 },
};

Об'єкти, створені за допомогою ініціалізаторів, називаються простими об'єктами, адже вони є примірниками Object, але не будь-якого іншого об'єктного типу. Частина об'єктних типів має особливий синтаксис ініціалізатора – наприклад, ініціалізатори масивів та літерали регулярних виразів.

Застосування функції-конструктора

Окрім цього, можна створювати об'єкти за допомогою таких двох кроків:

  1. Означити тип об'єкта шляхом написання функції-конструктора. Існує загальноприйнята домовленість називати конструктори з великої літери, з гарним обґрунтуванням.
  2. Створити примірник об'єкта за допомогою new.

Аби означити тип об'єкта, слід створити для нього функцію, яка задає його назву, властивості та методи. Для прикладу припустімо, що потрібно створити об'єктний тип для автомобілів. Нехай цей тип об'єктів називатиметься Car, і він матиме властивості для позначення виробника, моделі та року випуску. Для виконання цього можна написати наступну функцію:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

Зауважте, як використовується this для присвоєння значень властивостям об'єкта, на основі даних, переданих до функції.

Тепер можна створити об'єкт під назвою myCar, як це показано нижче:

const myCar = new Car("Eagle", "Talon TSi", 1993);

Ця інструкція створює myCar, і присвоює передані значення його властивостям. Після цього значення myCar.make дорівнює рядку "Eagle", myCar.model дорівнює 'Talon TSi', а myCar.year — ціле число 1993, і так далі. Порядок аргументів і параметрів повинен залишатись незмінним.

Викликаючи new, можна створити довільну кількість об'єктів Car. Наприклад:

const kenscar = new Car("Nissan", "300ZX", 1992);
const vpgscar = new Car("Mazda", "Miata", 1990);

Об'єкт може містити властивість, яка сама є іншим об'єктом. Наприклад, припустімо, що нам потрібно означити об'єкт під назвою Person, як це показано нижче:

function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}

…далі реалізуємо два нових об'єкти Person:

const rand = new Person("Rand McKinnon", 33, "M");
const ken = new Person("Ken Jones", 39, "M");

Потім перепишемо означення типу Car так, щоб воно містило властивість owner, яка приймає об'єкт Person, як наведено далі:

function Car(make, model, year, owner) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.owner = owner;
}

Далі реалізуймо нові об'єкти за допомогою наступного коду:

const car1 = new Car("Eagle", "Talon TSi", 1993, rand);
const car2 = new Car("Nissan", "300ZX", 1992, ken);

Зауважте, як замість передачі буквально рядка чи цілого числа під час створення нових об'єктів, наведені вище інструкції передають об'єкти rand та ken як аргументи для позначення власника авто. Далі, за потреби взнати ім'я власника автомобіля car2, можна доступитися до наступної властивості:

car2.owner.name;

Завжди можна додати до створеного раніше об'єкта нову властивість. Наприклад, така інструкція

car1.color = "black";

…додає властивість color до об'єкта car1, і присвоює їй значення 'black'. Проте це ніяк не впливає на інші об'єкти. Аби додати нову властивість до всіх об'єктів певного типу, доведеться додати цю властивість до означення об'єкта типу Car.

Також для визначення функції-конструктора можна замість синтаксису function використати синтаксис class. Більше про це – в посібнику з класів.

Застосування метода Object.create()

Також можна створювати об'єкти шляхом застосування методу Object.create(). Цей метод може бути дуже корисним, адже він дає можливість вказати прототип для об'єкта, який створюється, без необхідності оголошувати окрему функцію-конструктор.

// Об'єкт тварини Animal, зі своїми властивостями та інкапсульованим методом
const Animal = {
  type: "Безхребетні", // Усталене значення властивості
  displayType() {
    // Метод, що виводитиме тип тварини
    console.log(this.type);
  },
};

// Створюється новий тип тварини під назвою animal1
const animal1 = Object.create(Animal);
animal1.displayType(); // Друкує: Безхребетні

// Створюється новий тип тварини під назвою fish
const fish = Object.create(Animal);
fish.type = "Риби";
fish.displayType(); // Друкує: Риби

Об'єкти і властивості

Об'єкт JavaScript має пов'язані з ним властивості. Властивості об'єкта – по суті те саме, що й змінні, окрім того, що вони пов'язані з об'єктами, а не областями видимості. Властивості об'єкта визначають його характеристики.

Наприклад, цей приклад створює об'єкт під назвою myCar, із властивостями під назвами make, model і year, чиї значення – "Ford", "Mustang" і 1969:

const myCar = {
  make: "Ford",
  model: "Mustang",
  year: 1969,
};

Подібно до змінних JavaScript, імена властивостей - чутливі до регістру. Вони можуть бути лише рядками чи примірниками Symbol: усі ключі перетворюються на рядки, окрім Symbol. Індекси масивів фактично є властивостями з рядковими ключами, що містять цілі числа.

Звертання до властивостей

До властивості об'єкта можна звернутися за її іменем. Доступ до властивостей є у вигляді двох варіантів: запису точки й запису квадратних дужок. Наприклад, до властивостей об'єкта myCar можна звернутися отак:

// Запис точки
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
// Запис квадратних дужок
myCar["make"] = "Ford";
myCar["model"] = "Mustang";
myCar["year"] = 1969;

Ім'я властивості об'єкта може будь-яким рядком чи символом JavaScript, включно з порожнім рядком. Проте не можна використовувати запис точки для звертання до властивості, чиє ім'я не є дійсним ідентифікатором JavaScript. Наприклад, до властивості, чиє ім'я містить пробіл чи дефіс, починається з цифри, або зберігається у змінній, можна звернутися лише за допомогою запису квадратних дужок. Також цей запис дуже корисний, коли імена властивостей визначаються динамічно, тобто невідомі до виконання програми. Приклади:

const myObj = {};
const str = "myString";
const rand = Math.random();
const anotherObj = {};
// Створення на myObj додаткових властивостей
myObj.type = "Синтаксис точки – для ключа на ім'я type";
myObj["date created"] = "Цей ключ містить пробіл";
myObj[str] = "Цей ключ знаходиться у змінній str";
myObj[rand] = "Тут ключ – випадкове число";
myObj[anotherObj] = "Цей ключ – об'єкт anotherObj";
myObj[""] = "Цей ключ – порожній рядок";
console.log(myObj);
// {
//   type: 'Синтаксис точки – для ключа на ім'я type',
//   'date created': 'Цей ключ містить пробіл',
//   myString: 'Цей ключ знаходиться у змінній str',
//   '0.6398914448618778': 'Тут ключ – випадкове число',
//   '[object Object]': 'Цей ключ – об'єкт anotherObj',
//   '': 'Цей ключ – порожній рядок'
// }
console.log(myObj.myString); // 'Цей ключ знаходиться у змінній str'

У коді вище ключ anotherObj є об'єктом, тобто не рядком і не символом. Коли він додається до myObj, JavaScript викликає метод anotherObj toString() і використовує результівний рядок як новий ключ.

Також можна звертатися до властивостей через рядкове значення, збережене у змінній. Змінна повинна бути передана в записі квадратних дужок. У прикладі вище змінна str містить "myString", і саме "myString" є іменем властивості. Таким чином, myObj.str поверне значення undefined.

str = "myString";
myObj[str] = "Цей ключ – у змінній str";
console.log(myObj.str); // undefined
console.log(myObj[str]); // 'Цей ключ – у змінній str'
console.log(myObj.myString); // 'Цей ключ – у змінній str'

Це дає змогу звертатися до будь-якої властивості, обраної під час виконання:

let propertyName = "make";
myCar[propertyName] = "Ford";
// звертання до різних властивостей шляхом зміни вмісту змінної
propertyName = "model";
myCar[propertyName] = "Mustang";
console.log(myCar); // { make: 'Ford', model: 'Mustang' }

Проте слід остерігатися використання квадратних дужок для звертання до властивостей, чиї імена взяті з зовнішніх даних. Це може зробити код сприйнятливим щодо атак ін'єкції об'єктів.

Відсутні властивості об'єкта мають значення undefined (а не null).

myCar.nonexistentProperty; // undefined

Перелічення властивостей

Є три нативні способи перерахувати чи обійти властивості об'єкта:

  • Цикл for...in. Цей метод обходить усі перелічувані рядкові властивості об'єкта, а також його ланцюжка прототипів.
  • Object.keys(). Цей метод повертає масив, що містить лише перелічувані власні рядкові імена властивостей ("ключі") об'єкта myObj, але не ключі з його ланцюжка прототипів.
  • Object.getOwnPropertyNames(). Цей метод повертає масив, що містить усі власні рядкові імена властивостей об'єкта myObj, незалежно від їхньої перелічуваності.

Можна використати запис квадратних дужок вкупі з for...in для ітерування всіх перелічуваних властивостей об'єкта. Для ілюстрування того, як це працює, наступна функція демонструє властивості об'єкта, коли передати їй об'єкт та його ім'я як аргументи:

function showProps(obj, objName) {
  let result = "";
  for (const i in obj) {
    // Object.hasOwn() використано для виключення властивостей з
    // ланцюжка прототипів об'єкта і виведення винятково "власних властивостей"
    if (Object.hasOwn(obj, i)) {
      result += `${objName}.${i} = ${obj[i]}\n`;
    }
  }
  console.log(result);
}

Термін "власна властивість" позначає властивості об'єкта, але не включає властивості з ланцюжка прототипів. Тож виклик функції showProps(myCar, 'myCar') надрукує наступне:

myCar.make = Ford
myCar.model = Mustang
myCar.year = 1969

Код вище рівносильний наступному:

function showProps(obj, objName) {
  let result = "";
  Object.keys(obj).forEach((i) => {
    result += `${objName}.${i} = ${obj[i]}\n`;
  });
  console.log(result);
}

Немає нативного способу перерахувати успадковані неперелічувані властивості. Проте цього можна досягти за допомогою наступної функції:

function listAllProperties(myObj) {
  let objectToInspect = myObj;
  let result = [];
  while (objectToInspect !== null) {
    result = result.concat(Object.getOwnPropertyNames(objectToInspect));
    objectToInspect = Object.getPrototypeOf(objectToInspect);
  }
  return result;
}

Докладніше про це – у статті Перелічуваність та власність властивостей.

Видалення властивостей

Неуспадковану властивість можна прибрати за допомогою оператора delete. Наступний код демонструє, як можна прибрати властивість.

// Створюється новий об'єкт, myobj, з двома властивостями, a та b.
const myobj = new Object();
myobj.a = 5;
myobj.b = 12;
// Прибирається властивість a, залишаючи myobj лише властивість b.
delete myobj.a;
console.log("a" in myobj); // false

Успадкування

Усі об'єкти в JavaScript успадковують принаймні від одного іншого об'єкта. Об'єкт, від якого успадковують, відомий як прототип, й успадковані властивості знаходяться в об'єкті у властивості конструктора prototype. Докладніше в Успадкуванні й ланцюжку прототипів.

Означення властивостей для всіх об'єктів одного типу

Додати властивість до всіх об'єктів, створених за допомогою одного конструктора, можна за допомогою властивості prototype. Такий підхід дає змогу означити властивість, котру поділяють всі об'єкти конкретного типу, а не лише один примірник об'єкта. Наступний код додає властивість color до всіх об'єктів типу Car, а потім зчитує значення властивості з примірника car1.

Car.prototype.color = "red";
console.log(car1.color); // "red"

Означення методів

Метод — це функція, асоційована з певним об'єктом. Іншими словами, метод — це така властивість об'єкта, яка є функцією. Означення методів відбувається так само, як і означення звичайних функцій, за винятком того, що вони повинні присвоюватися властивостям об'єкта. Докладніше про це в розділі означення методів. Наприклад:

objectName.methodName = functionName;

const myObj = {
  myMethod: function (params) {
    // щось робить
  },

  // Це також працює!
  myOtherMethod(params) {
    // робить щось інше
  },
};

…де objectName — це наявний об'єкт, methodName — ім'я, присвоєне методові, а functionName — ідентифікатор вже наявної функції.

Далі можна викликати цей метод в контексті об'єкта, як показано нижче:

objectName.methodName(params);

Означення методів здебільшого виконують на об'єкті зі властивості конструктора prototype, аби усі об'єкти такого типу поділяли один метод. Наприклад, можна означити функцію, котра оформлює та виводить властивості раніше означених об'єктів Car.

Car.prototype.displayCar = function () {
  const result = `Чудова ${this.model} від ${this.make}, ${this.year} року випуску`;
  console.log(result);
};

Зверніть увагу на використання this для звертання до об'єкта, котрому належить такий метод. Після цього можна викликати метод displayCar на кожному з об'єктів, ось так:

car1.displayCar();
car2.displayCar();

Застосування this для вказівки на об'єкт

JavaScript має особливе ключове слово this, яке можна вживати всередині метода для вказівки на поточний об'єкт. Наприклад, припустімо, є два об'єкти, Manager та Intern. Кожний з об'єктів має свої власні властивості name, age та job. Зверніть увагу на використання всередині функції sayHi() запису this.name. Бувши доданою до цих двох об'єктів, ця (одна й та ж) функція надрукує повідомлення з іменем відповідного об'єкта, до котрого прикріплена.

const Manager = {
  name: "Іван",
  age: 27,
  job: "Розробник програмного забезпечення",
};
const Intern = {
  name: "Олег",
  age: 21,
  job: "Розробник програмного забезпечення — інтерн",
};

function sayHi() {
  console.log(`Привіт, мене звати ${this.name}`);
}

// додаємо функцію sayHi до обох об'єктів
Manager.sayHi = sayHi;
Intern.sayHi = sayHi;

Manager.sayHi(); // Привіт, мене звати Іван
Intern.sayHi(); // Привіт, мене звати Олег

this – це "прихований параметр" виклику функції, котрий передається шляхом задання об'єкта перед функцією, котра викликається. Наприклад, для Manager.sayHi() this – це об'єкт Manager, тому що Manager стоїть перед функцією sayHi(). Якщо звернутись до тієї самої функції з іншого об'єкта, то this також зміниться. Якщо для виклику функції використовуються інші методи, як то Function.prototype.call() чи Reflect.apply(), то можна явно передати значення this у вигляді аргументу.

Означення гетерів та сетерів

[Гетер] – це функція, пов'язана зі властивістю, котра отримує значення цієї конкретної властивості. Сетер – функція, пов'язана зі властивістю, котра задає значення цієї конкретної властивості. Вкупі гетер і сетер можуть опосередковано представляти значення властивості.

Гетери й сетери можуть бути:

В об'єктних ініціалізаторах гетери й сетери означаються подібно до звичайних методів, але мають на початку означень ключові слова get або set. Метод-гетер не повинен очікувати на параметр, натомість метод-сетер очікує рівно на один параметр (нове значення, котре присвоюється). Наприклад:

const myObj = {
  a: 7,
  get b() {
    return this.a + 1;
  },
  set c(x) {
    this.a = x / 2;
  },
};

console.log(myObj.a); // 7
console.log(myObj.b); // 8, повернене з гетера b()
myObj.c = 50; // Викликає сетер c(x)
console.log(myObj.a); // 25

Об'єкт myObj містить такі властивості:

  • myObj.a — число
  • myObj.b — гетер, який додає 1 до значення myObj.a і повертає результат
  • myObj.c — сетер, який встановлює значенням myObj.a половину того числа, яке присвоюється до myObj.c

Також можна додавати гетери й сетери до об'єкта будь-коли після його створення, за допомогою методу Object.defineProperties(). Перший параметр цього методу — це об'єкт, на якому потрібно додати гетер чи сетер. Другий параметр - це об'єкт, чиї імена властивостей є назвами гетерів чи сетерів, а значення властивостей містять об'єкти з означенням функції гетера чи сетера. Ось приклад, як можна означити такі само гетер і сетер, які було вжито в попередньому прикладі:

const myObj = { a: 0 };

Object.defineProperties(myObj, {
  b: {
    get() {
      return this.a + 1;
    },
  },
  c: {
    set(x) {
      this.a = x / 2;
    },
  },
});

myObj.c = 10; // Запускає сетер, який присвоює властивості 'a' значення 10 / 2 (5)
console.log(myObj.b); // Запускає гетер, який віддає a + 1, тобто 6

Яку з цих двох форм обрати — залежить від стилю програмування та конкретного завдання під руками. Якщо є змога змінити означення вихідного об'єкта, то ви, мабуть, означите гетери й сетери у вихідному ініціалізаторі. Ця форма компактніша і дещо природніша. Проте якщо потрібно додати гетери й сетери постфактум, оскільки не ми писали прототип, чи конкретний об'єкт — єдиним варіантом залишається друга форма. Друга форма, ймовірно, найкраще ілюструє динамічну природу JavaScript, хоча це може робити код складнішим для прочитання і розуміння.

Порівняння об'єктів

Об'єкти в JavaScript є посилальним типом. Два окремі об'єкти ніколи не будуть рівними, навіть якщо вони мають ідентичні властивості. Істинним буде лише порівняння об'єкта із самим собою.

// Дві змінні, два окремі об'єкти з однаковими властивостями
const fruit = { name: "apple" };
const fruitbear = { name: "apple" };

fruit == fruitbear; // повертає false
fruit === fruitbear; // повертає false
// Дві змінні, єдиний об'єкт
const fruit = { name: "apple" };
const fruitbear = fruit; // До fruitbear присвоюється посилання об'єкта fruit

// Тут fruit та fruitbear вказують на один об'єкт
fruit == fruitbear; // повертає true
fruit === fruitbear; // повертає true

fruit.name = "grape";
console.log(fruitbear); // { name: "grape" }; не { name: "apple" }

Більше інформації про оператори порівняння можна знайти у розділі операторів рівності.

Дивіться також