2 березня 2024 р.

Методи об’єкта, "this"

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

let user = {
  name: "Іван",
  age: 30
};

І в реальному світі користувач може діяти: вибрати щось із кошика для покупок, авторизуватися, виходити із системи тощо.

Дії представлені в JavaScript функціями у властивостях об’єкта.

Приклади методів

Для початку навчімо user вітатися:

let user = {
  name: "Іван",
  age: 30
};

user.sayHi = function() {
  alert("Привіт!");
};

user.sayHi(); // Привіт!

Тут ми щойно використали Function Expression (функціональний вираз) для створення функції та присвоїли її властивості user.sayHi об’єкта.

Потім ми викликали її завдяки user.sayHi(). Користувач тепер може говорити!

Функція, яка є властивістю об’єкта, називається його методом.

Отже, ми отримали метод sayHi об’єкта user.

Звичайно, ми могли б використовувати попередньо оголошену функцію як метод, наприклад:

let user = {
  // ...
};

// спочатку створимо функцію
function sayHi() {
  alert("Привіт!");
}

// потім додамо її як метод
user.sayHi = sayHi;

user.sayHi(); // Привіт!
Object-oriented programming

Коли ми пишемо наш код, використовуючи об’єкти для представлення сутностей, це називається об’єктно-орієнтоване програмування, скорочено: “ООП”.

ООП є великою предметною областю і цікавою наукою саме по собі. Як правильно обрати сутності? Як організувати взаємодію між ними? Це архітектура, і на цю тему є чудові книги, такі як “Шаблони проєктування: елементи багаторазового об’єктно-орієнтованого програмного забезпечення” Е. Гамми, Р. Хелма, Р. Джонсона, Дж. Віссідеса або “Об’єктно-орієнтований аналіз та дизайн з застосунками” Г. Буча та ін.

Скорочений запис методу

Існує коротший синтаксис для методів в літералі об’єкта:

// цей об’єкт робить те ж саме

user = {
  sayHi: function() {
    alert("Привіт!");
  }
};

// скорочений метод виглядає краще, чи не так?
user = {
  sayHi() { // те ж саме що й "sayHi: function(){...}"
    alert("Привіт!");
  }
};

Як було показано, ми можемо опустити "function" і написати просто sayHi().

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

“this” в методах

Як правило, метод об’єкта повинен отримувати доступ до інформації, що зберігається в об’єкті, для виконання своєї роботи.

Наприклад, коду всередині user.sayHi() може знадобитися ім’я, що зберігається в об’єкті user.

Для доступу до інформації всередині об’єкта метод може використовувати ключове слово this.

Значенням this є об’єкт “перед крапкою”, який використовується для виклику методу.

Наприклад:

let user = {
  name: "Іван",
  age: 30,

  sayHi() {
    // "this" -- це "поточний об’єкт"
    alert(this.name);
  }

};

user.sayHi(); // Іван

Тут під час виконання коду user.sayHi(), значенням this буде user.

Також можна отримати доступ до об’єкта без цього, посилаючись на нього через зовнішню змінну:

let user = {
  name: "Іван",
  age: 30,

  sayHi() {
    alert(user.name); // використовуємо змінну "user" замість "this"
  }

};

…Але такий код ненадійний. Якщо ми вирішимо скопіювати user в іншу змінну, напр. admin = user перезаписати user чимось іншим, тоді цей код отримає доступ до неправильного об’єкта.

Це продемонстровано нижче:

let user = {
  name: "Іван",
  age: 30,

  sayHi() {
    alert( user.name ); // призводить до помилки
  }

};


let admin = user;
user = null; // перезапишемо значення змінної для наочності

admin.sayHi(); // TypeError: Cannot read property 'name' of null

Якщо ми використовуємо this.name замість user.name всередині alert, тоді цей код буде працювати.

“this” не є фіксованим

В JavaScript, ключове слово this поводить себе не так, як в більшості мов програмування.

В цьому коді немає синтаксичної помилки:

function sayHi() {
  alert( this.name );
}

Значення this обчислюється під час виконання і залежить від контексту.

Наприклад, тут одна й та ж функція призначена двом різним об’єктам і має різний “this” при викликах:

let user = { name: "Іван" };
let admin = { name: "Адмін" };

function sayHi() {
  alert( this.name );
}

// використовуємо одну і ту ж функцію у двох об’єктах
user.f = sayHi;
admin.f = sayHi;

// виклики функцій, приведені нижче, мають різні this
// "this" всередині функції є посиланням на об’єкт "перед крапкою"
user.f(); // Іван  (this == user)
admin.f(); // Адмін  (this == admin)

admin['f'](); // Адмін (неважливо те, як звертатися до методу об’єкта -- через крапку чи квадратні дужки)

Правило просте: якщо obj.f() викликано, то this це obj під час виконання f. Так що в даному прикладі це user або admin.

Виклик без об’єкта: this == undefined

Ми можемо навіть викликати функцію взагалі без об’єкта:

function sayHi() {
  alert(this);
}

sayHi(); // undefined

В такому випадку this є undefined в суворому режимі ("use strict"). Якщо ми спробуємо звернутися до this.name трапиться помилка.

У несуворому режимі значенням this в такому випадку буде глобальний об’єкт (window у браузері, ми дійдемо до нього пізніше в главі Глобальний об’єкт). Це – поведінка, яка склалася історично та виправляється завдяки використанню суворого режиму ("use strict").

Зазвичай такий виклик є помилкою програмування. Якщо всередині функції є this, вона очікує виклику в контексті об’єкта.

Наслідки вільного this

Якщо ви прийшли з іншої мови програмування, то ви, мабуть, звикли до ідеї “зв’язаного this”, де методи, визначені в об’єкті, завжди мають this, що посилається на цей об’єкт.

В JavaScript this є “вільним”, його значення обчислюється під час виклику і не залежить від того, де метод був оголошений, а від того, який об’єкт “перед крапкою”.

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

Тут наша позиція полягає не в тому, щоб судити, добре чи погане таке рішення щодо дизайну мови. Ми зрозуміємо, як з цим працювати, як отримати переваги та уникнути проблем.

Стрілкові функції не мають “this”

Стрілкові функції особливі: у них немає “свого” this. Якщо ми посилаємось на this з такої функції, його значення береться із зовнішньої “нормальної” функції.

Наприклад, тут arrow() використовує this із зовнішнього user.sayHi() методу:

let user = {
  firstName: "Ілля",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ілля

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

Підсумки

  • Функції, які зберігаються у властивостях об’єкта, називаються “методами”.

  • Методи дозволяють об’єктам “діяти” подібно до object.doSomething().

  • Методи можуть посилатися на об’єкт завдяки this.

  • Значення this визначається під час виконання.

  • Коли функція оголошена, вона може використовувати this, але саме this не має значення, доки функція не буде викликана.

  • Функцію можна копіювати між об’єктами.

  • Коли функція викликається в синтаксисі “методу”: object.method(), значення this під час виклику є object – об’єкт перед крапкою.

Зверніть увагу, що стрілкові функції є особливими: у них немає this. Коли всередині стрілкової функції звертаються до this, то його значення береться ззовні.

Завдання

важливість: 5

Тут функція makeUser повертає об’єкт.

Який результат доступу до його поля ref? Чому?

function makeUser() {
  return {
    name: "Іван",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Який результат?

Відповідь: помилка.

Спробуйте це:

function makeUser() {
  return {
    name: "Іван",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Error: Cannot read property 'name' of undefined

Це тому, що правила, які встановлюють this, не розглядають оголошення об’єкта. Важливий лише момент виклику метода.

Тут значення this всередині makeUser() є undefined, оскільки воно викликається як функція, а не як метод із синтаксисом “через крапку”.

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

Отже, ref: this дійсно бере значення this функції.

Ми можемо переписати функцію і повернути те саме this зі значеннямundefined:

function makeUser(){
  return this; // цього разу немає літерала об’єкта
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

Як бачите, результат alert( makeUser().name ) збігається з результатом alert( user.ref.name ) з попереднього прикладу.

Ось протилежний випадок:

function makeUser() {
  return {
    name: "Іван",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // Іван

Зараз це працює, оскільки user.ref() – це метод. І значення this встановлюється для об’єкта перед крапкою ..

важливість: 5

Створіть об’єкт calculator з трьома методами:

  • read() запитує два значення та зберігає їх як властивості об’єкта з іменами a та b відповідно.
  • sum() повертає суму збережених значень.
  • mul() множить збережені значення і повертає результат.
let calculator = {
  // ... ваш код ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Запустити демонстрацію

Відкрити пісочницю з тестами.

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Відкрити рішення із тестами в пісочниці.

важливість: 2

Існує об’єкт ladder, що дозволяє підійматися вгору-вниз:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // показує поточний крок
    alert( this.step );
  }
};

Тепер, якщо нам потрібно зробити кілька викликів послідовно, можна зробити це так:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
ladder.down();
ladder.showStep(); // 0

Змініть код up, down і showStep так, щоб зробити доступним ланцюг викликів, наприклад:

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

Такий підхід широко використовується в бібліотеках JavaScript.

Відкрити пісочницю з тестами.

Рішення полягає у поверненні самого об’єкта з кожного виклику функції.

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

ladder.up().up().down().showStep().down().showStep(); // покаже 1, потім 0

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

ladder
  .up()
  .up()
  .down()
  .showStep() // 1
  .down()
  .showStep(); // 0

Відкрити рішення із тестами в пісочниці.

Навчальна карта