Методы объекта и this

Объект собирает данные об одной сущности: у игрока есть имя и счёт. Действия над этими данными обычно живут отдельно – в функции, которой объект передают:

add-score-function.jsjs
const player = {  name: 'Ира',  score: 0,};function addScore(player, points) {  player.score += points;}addScore(player, 10);console.log(player.score);
Консоль
10

Такой код работает, но addScore относится именно к игроку: без объекта player эта функция почти не имеет смысла. Данные лежат в объекте, действие над ними – рядом в файле, и связывает их только договорённость, что в функцию передадут игрока.

JavaScript позволяет хранить действие рядом с данными – прямо в объекте:

add-score-method.jsjs
const player = {  name: 'Ира',  score: 0,  addScore(points) {    this.score += points;  },};player.addScore(10);console.log(player.score);
Консоль
10

В объекте player рядом с name и score появилась третья запись – addScore, и в ней лежит функция. Вызывают её через сам объект: player.addScore(10). Имя player внутри функции больше не нужно: this.score обращается к данным того объекта, через который метод вызван.

Объект теперь хранит не только данные, но и действия над ними: он знает, какие у него данные и что с ними можно сделать. Функцию, которая хранится в объекте и вызывается через него, называют методом.

user name : 'Ира' sayHi : функция
Значением свойства может быть и функция.

Функция как значение свойства

Запись метода выглядит как отдельная конструкция, но ничего нового в объекте не появилось. Значением свойства бывает строка, число, другой объект. Функция в JavaScript – тоже значение: её можно записать в переменную и передать в другую функцию. Значит, функцию можно положить и в свойство:

say-hi-property.jsjs
function sayHi() {  console.log(`Привет, ${this.name}!`);}const user = {  name: 'Ира',  sayHi: sayHi,};user.sayHi();
Консоль
Привет, Ира!

Свойство sayHi записано как любое другое: слева ключ, справа значение. Значением стала функция, объявленная выше. Обращение user.sayHi читает свойство и даёт функцию, а скобки в user.sayHi() её вызывают.

Имя свойства и имя функции совпадают, поэтому работает короткая запись свойства:

полная запись
const user = {  name: 'Ира',  sayHi: sayHi,};
короткая запись
const user = {  name: 'Ира',  sayHi,};

Объявлять функцию заранее тоже не обязательно. Если она нужна только этому объекту, её пишут прямо в литерале, на месте значения свойства – для этого есть запись метода: имя, скобки параметров, тело и запятая после закрывающей скобки, как после любого свойства:

say-hi-method.jsjs
const user = {  name: 'Ира',  sayHi() {    console.log(`Привет, ${this.name}!`);  },};user.sayHi();
Консоль
Привет, Ира!

Все три записи дают одно и то же: в свойстве sayHi лежит функция, а user.sayHi() её вызывает. Метод – не отдельный вид значения и не особая часть объекта, а свойство, в котором лежит функция.

Как записать функцию в объекте

Функция попадает в свойство объекта тремя записями. Первая – короткая запись метода, вторая – свойство с обычной функцией, третья – свойство со стрелочной функцией:

three-forms.jsjs
// короткая запись методаconst a = {  sayHi() {    console.log('Привет!');  },};// свойство с обычной функциейconst b = {  sayHi: function () {    console.log('Привет!');  },};// свойство со стрелочной функциейconst c = {  sayHi: () => {    console.log('Привет!');  },};

Все три кладут функцию в свойство sayHi, и вызывается она одинаково – user.sayHi(). А вот с this записи ведут себя не одинаково: короткая запись и обычная function подходят методам, которым нужен this, а стрелочная функция собственного this не получает.

«Свой this» означает, что при вызове через объект функция получает этот объект: в user.sayHi() внутри метода this – это user, и this.name читает user.name. Стрелочная функция такого this не получает – её this приходит из кода вокруг объекта, и вызов через user его не меняет.

Короткая запись – sayHi() { … } – встречается чаще остальных, поэтому дальше методы записаны ею.

Зачем нужен this

Метод лежит внутри объекта, и данные этого объекта нужны ему для работы. Добраться до них можно самым прямым способом – по имени переменной, в которой объект хранится:

add-score-by-name.jsjs
const player = {  name: 'Ира',  score: 0,  addScore(points) {    player.score += points;  },};player.addScore(10);console.log(player.score);
Консоль
10

Вызов player.addScore(10) увеличил score до 10 – код работает. Пока в программе один объект и одно имя, запись player.score внутри метода ничем не хуже любой другой, и this выглядит лишним.

Где запись по имени ломается

Разница видна, когда одну и ту же функцию кладут в два разных объекта. Свойство addScore в обоих объектах хранит одно значение – функцию, объявленную выше:

two-objects-by-name.jsjs
function addScore(points) {  player.score += points;}const player = {  name: 'Ира',  score: 0,  addScore,};const enemy = {  name: 'Макс',  score: 100,  addScore,};player.addScore(10);enemy.addScore(50);console.log(player.score);console.log(enemy.score);
Консоль
60
100

Оба вызова изменили player.score: сначала на 10, затем ещё на 50 – вышло 60. У enemy осталось 100, хотя вызов был enemy.addScore(50).

Дело в строке player.score += points;: имя player записано в функции жёстко. Куда бы функцию ни положили, она идёт к объекту с этим именем, а объект, через который её вызвали, на результат не влияет.

Что меняет this

Чтобы функция работала с объектом, через который её вызвали, вместо имени переменной пишут this:

two-objects-this.jsjs
function addScore(points) {  this.score += points;}const player = {  name: 'Ира',  score: 0,  addScore,};const enemy = {  name: 'Макс',  score: 100,  addScore,};player.addScore(10);enemy.addScore(50);console.log(player.score);console.log(enemy.score);
Консоль
10
150

В функции изменилось одно слово, а результат другой: player.score стал 10, enemy.score – 150. Каждый вызов пришёл к своему объекту.

this позволяет функции не знать имя объекта заранее. Вместо «измени player.score» в ней записано «измени score того объекта, через который меня вызвали».

работает с объектом вызова
function addScore(points) {  this.score += points;}
привязано к имени player
function addScore(points) {  player.score += points;}
С this функция меняет объект, через который её вызвали; с именем player – всегда один и тот же объект.

this определяется вызовом

this не нужно передавать параметром. Для обычной функции его значение определяется тем, как функцию вызвали.

В player.addScore(10) функция вызвана через player, поэтому внутри этого вызова this – это player. В enemy.addScore(50) та же функция вызвана через enemy, поэтому this – это enemy.

Отсюда рабочее правило для обычных методов: в вызове object.method() значением this внутри метода становится object.

Объект, через который выполнен вызов, становится значением this.
this определяется при вызове
const user = {
Пока метод не вызван, мы ещё не знаем, на какой объект будет указывать this.
name: 'Ира',
sayHi() {
console.log(`Привет, ${this.name}!`);
},
};
user.sayHi();

this зависит от вызова

Значение this не закрепляется за функцией в момент, когда её создали. Одна функция showName в двух объектах печатает разные имена:

show-name.jsjs
function showName() {  console.log(this.name);}const user = {  name: 'Ира',  showName,};const admin = {  name: 'Макс',  showName,};user.showName();admin.showName();
Консоль
Ира
Макс
функция showName user.showName() this → user name → 'Ира' admin.showName() this → admin name → 'Макс'
Функция одна, вызовы разные – this тоже разный.

Функция здесь одна: в свойстве showName у user и у admin лежит одно и то же значение. Разные у них вызовы – а вместе с вызовом разный и this.

this – не сокращение для имени объекта, а значение, которое определяется способом вызова.

Метод меняет свой объект

Раз this даёт доступ к объекту вызова, метод может работать с ним так же, как любой другой код: читать свойства, изменять их и использовать в вычислениях:

counter.jsjs
const counter = {  count: 0,  increment() {    this.count += 1;  },  reset() {    this.count = 0;  },};counter.increment();counter.increment();console.log(counter.count);counter.reset();console.log(counter.count);
Консоль
2
0

Метод может возвращать значение

Метод не обязан менять объект. Здесь total() только читает price и quantity, вычисляет результат и возвращает его:

order.jsjs
const order = {  price: 350,  quantity: 3,  total() {    return this.price * this.quantity;  },};const sum = order.total();console.log(sum);
Консоль
1050

Почему для методов с this не берут стрелку

В свойство можно положить и стрелочную функцию. Запись выглядит привычно, и вызов ничем не отличается от вызова любого другого метода. Проверим, чем окажется this в момент такого вызова:

arrow-method.jsjs
const outside = this;const user = {  name: 'Ира',  sayHi: () => {    console.log(this === user);    console.log(this === outside);  },};user.sayHi();
Консоль
false
true

Вызов user.sayHi() выполнился, но this внутри стрелки – не user: первая строка вывела false.

Стрелочная функция не получает this от вызова, а берёт его из кода вокруг объекта – поэтому вторая строка выводит true: внутри стрелки this тот же, что в outside. Что именно в нём лежит, зависит от того, где выполняется программа, но объектом user он не становится.

короткая запись: this – объект вызова
const user = {  name: 'Ира',  sayHi() {    console.log(this.name);  },};
стрелка: this не приходит от вызова
const user = {  name: 'Ира',  sayHi: () => {    console.log(this.name);  },};
Обе функции лежат в свойстве sayHi и вызываются как user.sayHi(); различие – только в this.

Ориентир простой: если функция работает с объектом через this, для неё берут короткую запись метода.

Функция отдельно или метод?

Функция становится методом не потому, что такая запись современнее. Класть её в объект стоит тогда, когда действие относится к данным этого объекта. Вот два действия из одной программы:

отдельная функция
function formatPrice(price) {  return `${price} ₽`;}
метод объекта
const cart = {  items: [],  total: 0,  clear() {    this.items = [];    this.total = 0;  },};
formatPrice работает с любым числом, clear – с данными своей корзины.

formatPrice получает число и возвращает строку. Собственные данные ей не нужны, и никакой объект она не меняет – такая функция живёт сама по себе и одинаково работает с любой ценой.

clear меняет состояние конкретной корзины: обнуляет её items и total. Это действие имеет смысл только вместе с этими данными, поэтому и записано рядом с ними.

Состояние и поведение

У многих сущностей в программе есть и данные, и действия над этими данными. Данные называют состоянием объекта, действия – его поведением. Когда есть и то и другое, объект держит их вместе:

Состояние – свойства, поведение – методы.

Такая сборка группирует код по смыслу: свойства и методы одной сущности лежат в одном месте, а снаружи достаточно знать названия действий – player.takeDamage(10), cart.clear() – и не разбираться в их устройстве.

На объединении состояния и действий в один объект построено .

Данные и действия в одном объекте

Полный пример: объект игрока хранит своё состояние и сам его меняет.

player.jsjs
const player = {  name: 'Ира',  health: 100,  score: 0,  takeDamage(damage) {    this.health -= damage;    // здоровье не опускается ниже нуля    if (this.health < 0) {      this.health = 0;    }  },  addScore(points) {    this.score += points;  },  isAlive() {    return this.health > 0;  },};player.takeDamage(30);player.addScore(100);console.log(player.health);console.log(player.score);console.log(player.isAlive());
Консоль
70
100
true

Свойства name, health и score – состояние игрока. Методы takeDamage, addScore и isAlive – действия над этим состоянием, и каждый из них работает со своим объектом через this.

Код снаружи не считает здоровье сам: он вызывает player.takeDamage(30), а как применяется урон и почему здоровье не уходит в минус – дело метода. Правило записано в одном месте, и в большой программе это заметно: операции над одними и теми же данными не разбросаны по проекту, а лежат рядом с ними.

// теперь вы знаете

  1. Метод – функция в свойстве объекта: sayHi() { … } в литерале, вызов user.sayHi()
  2. В вызове object.method() this внутри метода – объект, через который вызван метод; this.name в user.sayHi() – это user.name
  3. this определяется вызовом, а не местом записи: одна функция в двух объектах даёт разные this
  4. Через this метод читает и меняет данные своего объекта, параметры передают данные извне, а результат можно вернуть: order.total() – выражение со значением
  5. Короткая форма sayHi() {} и sayHi: function () {} для обычных методов работают одинаково; у стрелочной функции своего this нет
  6. Метод нужен там, где действие относится к данным объекта: formatPrice(price) остаётся отдельной функцией, cart.clear() – метод корзины
  7. Свойства хранят состояние объекта, методы описывают поведение – данные и операции над ними лежат рядом

Проверьте себя

Что появится в консоли?

function voice() {
console.log(`${this.name}: мяу`);
}
const cat = { name: 'Мурка', voice };
const dog = { name: 'Рекс', voice };
dog.voice();

Верно ли, что код выведет 2?

const bell = {
rings: 0,
ring() {
this.rings += 1;
},
};
bell.ring;
bell.ring();
console.log(bell.rings);

Что появится в консоли?

const counter = {
count: 0,
increment() {
this.count += 1;
},
};
const same = counter;
same.increment();
counter.increment();
console.log(counter.count);

Сколько раз код напечатает true?

const user = {
a() {
console.log(this === user);
},
b: function () {
console.log(this === user);
},
c: () => {
console.log(this === user);
},
};
user.a();
user.b();
user.c();

Что появится в консоли?

const wallet = {
balance: 100,
add(amount) {
this.balance += amount;
},
};
wallet.add(50);
wallet.add(25);
console.log(wallet.balance);

Метод записан стрелочной функцией: hi: () => console.log(this.name). Верно ли, что при вызове user.hi() внутри this станет user?

Состояние объекта account – свойства owner и balance. Какую из функций стоит сделать методом этого объекта?

const account = {
owner: 'Ира',
balance: 5000,
};
function formatMoney(sum) {
return `${sum} ₽`;
}
function withdraw(account, amount) {
account.balance -= amount;
}
function isValidAmount(amount) {
return amount > 0;
}
function percent(sum, rate) {
return sum * rate / 100;
}