Домівка > Javascript > Об’єкти і об’єктно орієнтоване програмування у Javascript

Об’єкти і об’єктно орієнтоване програмування у Javascript

Ми вже знаємо основи використання об’єктів, а тепер час зазирнути глибше.

Подібно до масивів, об’єкти в Javascript є контейнерами (також відомими як агрегатні або складні типи даних). Об’єкти мають дві основні відмінності від масивів:

  • Масиви містять значення, проіндексовані числами; об’єкти містять властивості індексовані рядками або символами.
  • Масиви впорядковані (arr[0] завжди іде перед arr[1]); об’єкти ж ні (ви не можете гарантувати, що obj.a іде перед obj.b).

Ці відмінності доволі езотеричні (але важливі), отже, давайте думати про властивості (це не каламбур) завдяки яким об’єкти дійсно особливі. Властивість складається з ключа (рядок чи символ) і значення. Що робить об’єкти особливими так це те, що ви можете доступатись до властивостей за їх ключем.

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

Загалом, якщо ви бажаєте пройтись списком усього вмісту контейнера (називається перелічування), ви ймовірно використовуватимете масив чи об’єкт. Але об’єкти є контейнерами, які таки підтримують перелічування властивостей; вам лише потрібно бути обізнаними із складностями цього процесу.

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

for…in

Традиційним способом перелічування властивостей об’єкта є for...in. Розглянемо об’єкт, що має деякі властивості і самотню символьну властивість:

const SYM = Symbol();
const o = { a: 1, b: 2, c: 3, [SYM]: 4 };
for(let prop in o) {
 if(!o.hasOwnProperty(prop)) continue;
  console.log(`${prop}: ${o[prop]}`);
}

Виглядає досить прямолінійно…окрім того, що ви ймовірно закономірно цікавитесь, що hasOwnProperty робить. Вона опікується небезпекою, що зачаїлась у циклі for...in, яка буде незрозумілою до певного моменту пізніше в цій главі: успадковані властивості. У цьому прикладі, ми могли б опустити цей виклик і побачили б ніяких відмінностей. Однак, якщо ви перелічуєте властивості інших типів об’єктів, особливо тих, що прийшли з інших частин сирцевого коду, ви можете зустріти властивості на які ви не очікували. Я заохочую вас зробити використання hasOwnProperty звичкою. Скоро ми дізнаємось чому це важливо і ви матимете знання, щоб визначити коли ще безпечно (або бажано) опустити.

Зауважте, що цикл for...in не включає властивості з символьними ключами.

Хоча це й можливо використати цикл for...in для ітерації по масиву, зазвичай це вважається поганою ідеєю. Я рекомендую використовувати цикл for або forEach для масивів.

Object.keys

Object.keys дає можливість отримати всі переліковні рядкові властивості об’єкта як масив:

const SYM = Symbol();
const o = { a: 1, b: 2, c: 3, [SYM]: 4 };
Object.keys(o).forEach(prop => console.log(`${prop}: ${o[prop]}`));

Цей приклад видає той самий результат, що і цикл for..in (і нам непотрібно перевіряти hasOwnProperty). Це зручно коли потрібно отримати ключі властивостей об’єкта у масиві. Наприклад, це полегшує отримання списку властивостей об’єкта, які починаються на букву x:

const o = { apple: 1, xochitl: 2, balloon: 3, guitar: 4, xylophone: 5, };
Object.keys(o)
.filter(prop => prop.match(/^x/))
.forEach(prop => console.log(`${prop}: ${o[prop]}`));

Об’єктно Орієнтоване Програмування

Об’єктно Орієнтоване Програмування (ООП) – це стара парадигма в інформатиці. Деякі з концепцій, які ми зараз знаємо як ООП почали з’являтись іще в 1950х, але лише після введення Simula 67 і Smalltalk з’явилась впізнавана концепція ООП.

Базова ідея проста і інтуїтивна: об’єкт це логіно поєднані дані і функціональність. Концепія має на меті відобразити наше природнє розуміння світу. Автомобіль є об’єктом який має дані (виробник, модель, кількість дверей і т.д.) і функціональність (прискоритись, відчинити двері, зупинитись і т.д.). Більше того, ООП дає можливість думати про речі абстрактніше (автомобіль) і конкретніше (певний автомобіль).

Перед зануренням у тему, давайте покриємо базовий словник ООП. Клас покликається до загального поняття (автомобіль). Примірник (або об’єкт-примірник) покликається до певної речі (певного автомобіля, такого як “Мій автомобіль”). Функціональність (прискоритись) називається методом. Функціональність, що стосується класу, але не конкретного об’єкта, називається методом класу (наприклад, “Згенерувати новий номер” може бути методом класу: він поки що не пов’язаний із конкретним автомобілем, і очевидно, що ми не очікуємо від автомобіля знань і можливостей генерувати нові дійсні номери). Коли створюється об’єкт запускається його конструктор. Конструктор ініціалізує цей примірник. ООП також надає нам можливість ієрархічної категоризації класів. Наприклад, може існувати більш загальний клас для транспортних засобів. Транспортний засіб може мати дальність (відстань яку він може проїхати без дозаправки або дозарядки), але на відміну від автомобіля, він може не мати коліс (човен є прикладом транспортного засобу, який може не мати коліс). Ми кажемо, що транспортний засіб є суперкласом автомобіля, і що автомобіль є підкласом транспортного засобу. Траспортний засіб може мати багато підкласів: автомобілі, човни, мотоцикли, велосипеди і т.д. І підкласи, у свою чергу, можуть мати власні підкласи. Наприклад, підкласами човна можуть бути вітрильник, гребний човен, каное, буксир, моторний човен і т.д. Ми будемо використовувати приклад автомобіля впродовж всієї цієї статті, оскільки він є прикладом об’єкта з реального світу з яким ми всі знайомі (навіть якщо ми і не дуже любимо його).

Створення класів і примірників

До появи ES6, створення класів у JavaScript було метушливою, неінтуїтивною справою. ES6 запровадив певний зручний синтаксис для створення класів:

 class Car {
  constructor() {
  }
 }

Це облаштовує новий клас названий Car. Жодного примірника (конкретного автомобіля) покищо не було створено. Щоб створити конкретний автомобіль, ми використовуємо ключове слова new:

const car1 = new Car();
const car2 = new Car();

Тепер ми маємо два примірники класу Car. Перед тим як робити клас Car складнішим, давайте розглянемо оператор instanceof, який каже чи є певний об’єкт примірником певного класу:

car1 instanceof Car // true
car1 instanceof Array // false

З цього ми бачимо, що car1 є примірником Car, а не Array.
Давайте зробимо клас Car трошки цікавішим. Ми додамо до нього деякі дані (виробник, модель), і певну функціональність (перемикання передач):

class Car {
 constructor(make, model) {
  this.make = make;
  this.model = model;
  this.userGears = ['P', 'N', 'R', 'D'];
  this.userGear = this.userGears[0];
 }
 shift(gear) {
  if(this.userGears.indexOf(gear) < 0)
   throw new Error(`Invalid gear: ${gear}`);
  this.userGear = gear;
 }
}

Тут ключове слово this використовується за його призначенням: щоб послатись на той примірник на якому метод було викликано. Ви можете думати про це як про позначку: коли ви пишете свій клас, який є абстракцією, ключове слово this позначає конкретний примірник цього класу, який буде відомим під час виклику метода. Цей конструктор дозволяє нам вказати виробника і модель, а також встановлює деякі параметри по замовчанню: доступні передачі і поточну передачу, яку ми ініціалізуємо першою з доступних. (Я вирішив назвати це користувацькими передачами, бо якщо цей автомобіль має автоматичну трансмісію, коли він їде, там наявні механічні передачі, які можуть відрізнятись.) Додатково до конструктора, який викликається неявно коли ми створюємо новий об’єкт, ми також створили метод shift, який дозволяє переключати передачі. Давайте подивимось як це діє:

const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
car1.shift('D');
car2.shift('R');

У цьому прикладі, ми викликали car1.shift('D'), this прив’язаний до car1. Аналогічно, у car2.shift('R'), він прив’заний до car2. ми можемо перевірити, що car1 на передачі (D) і car2 на задньому ходу (R):

> car1.userGear // "D"
> car2.userGear // "R"

Динамічні властивості

Те, що метод shift запобігає ненавмисному встановленню недійсної передачі, може видатись дуже розумним. Однак, цей захист обмежений, тому що ніщо не зупиняє користувача проти встановлення передачі напряму: car1.userGear = 'X'. Більшість ОО-мов вдаються до значних зусиль, щоб забезпечити механізми, які запобігають подібному зловживанню через надання можливості вказувати рівень доступу до методів і властивостей. JavaScript не має такого механізму, що часто згадають в критиці мови.

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

class Car {
 constructor(make, model) {
  this.make = make;
  this.model = model;
  this._userGears = ['P', 'N', 'R', 'D'];
  this._userGear = this._userGears[0];
 }
 get userGear() { return this._userGear; }
 set userGear(value) {
  if(this._userGears.indexOf(value) < 0)
   throw new Error(`Invalid gear: ${value}`);
  this._userGear = vaule;
 }
 shift(gear) { this.userGear = gear; }
}

Проникливий читач зауваже, що ми не позбавились проблеми оскільки ми все ще можемо використати _userGear напряму: car1._userGear = 'X'. У цьому прикладі, ми використовуємо “китайську копію обмеження доступу”–вважаємо властивості, що починаються з підкреслювання приватними. Це захист лише за домовленістю, який дозволяє швидко виявити те, що код доступається до властивостей до яких не мав би.

Якщо ви дійсно потребуєте повної приватності, то ви можете використати примірник WeakMap яка захищена областю видимості (якби ми використали не WeakMap, наші приватні властивості ніколи не покинуть область видимості навіть якщо те, на що вони посилаються покине). Ось як ми можемо змінити клас Car, щоб зробити використовувану властивість поточної передачі дійсно приватною:

const Car = (function() {
 const carProps = new WeakMap();
 class Car {
  constructor(make, model) {
  this.make = make;
  this.model = model;
  this._userGears = ['P', 'N', 'R', 'D'];
  carProps.set(this, { userGear: this._userGears[0] });
 }
 get userGear() { return carProps.get(this).userGear; }
  set userGear(value) {
   if(this._userGears.indexOf(value) < 0)
    throw new Error(`Invalid gear: ${value}`);
   carProps.get(this).userGear = value;
  }
  shift(gear) { this.userGear = gear; }
 }
 return Car;
})();

Тут ми використали вираз негайно викликаної функції щоб приховати нашу WeakMap у замиканні, до якого не можна доступитись зовні класу.

Інший підхід полягає у використанні символів як імен властивостей; вони також надають певний захист від випадкового використання, але до символьних властивостей можна доступитись, тобто і цей підхід можна обійти.

Класи – це функції

До того як ключове слово class було впроваджене у ES6, щоб створити клас вам довелось би використати функцію як конструктор цього класу. Хоча синтаксис class набагато юільш інтуїтивний і прямолінійний, природа класів у Javascript не змінилась (class лише додає певний синтаксичний цукор), отже, важливо розуміти як класи представлені у Javascript.

Насправді клас – це просто функція. У ES5, ми б розпочали наш клас Car отак:

function Car(make, model) {
 this.make = make;
 this.model = model;
 this._userGears = ['P', 'N', 'R', 'D'];
 this._userGear = this.userGears[0];
}

Ми все ще можемо так робити у ES6: у висліді матимемо те саме (ми підійдемо до методів дуже скоро). Ми можемо перевірити спробувавши обидва підходи:

class Es6Car {} // ми опустимо констуктор заради краткості
function Es5Car {}
> typeof Es6Car // "function"
> typeof Es5Car // "function"

Отже, нічого дійсно нового у ES6; ми просто отримали зручний новий синтаксис.

Advertisements
Категорії:Javascript Позначки:,
  1. Коментарів ще немає.
  1. No trackbacks yet.

Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: