Методы объекта и this
Объект собирает данные об одной сущности: у игрока есть имя и счёт. Действия над этими данными обычно живут отдельно – в функции, которой объект передают:
const player = { name: 'Ира', score: 0,};function addScore(player, points) { player.score += points;}addScore(player, 10);console.log(player.score);Такой код работает, но addScore относится именно к игроку: без объекта player эта функция почти не имеет смысла. Данные лежат в объекте, действие над ними – рядом в файле, и связывает их только договорённость, что в функцию передадут игрока.
JavaScript позволяет хранить действие рядом с данными – прямо в объекте:
const player = { name: 'Ира', score: 0, addScore(points) { this.score += points; },};player.addScore(10);console.log(player.score);В объекте player рядом с name и score появилась третья запись – addScore, и в ней лежит функция. Вызывают её через сам объект: player.. Имя player внутри функции больше не нужно: this. обращается к данным того объекта, через который метод вызван.
Объект теперь хранит не только данные, но и действия над ними: он знает, какие у него данные и что с ними можно сделать. Функцию, которая хранится в объекте и вызывается через него, называют методом.
Запись метода выглядит как отдельная конструкция, но ничего нового в объекте не появилось. Значением свойства бывает строка, число, другой объект. Функция в JavaScript – тоже значение: её можно записать в переменную и передать в другую функцию. Значит, функцию можно положить и в свойство:
function sayHi() { console.log(`Привет, ${this.name}!`);}const user = { name: 'Ира', sayHi: sayHi,};user.sayHi();Свойство sayHi записано как любое другое: слева ключ, справа значение. Значением стала функция, объявленная выше. Обращение user. читает свойство и даёт функцию, а скобки в user. её вызывают.
Имя свойства и имя функции совпадают, поэтому работает короткая запись свойства:
const user = { name: 'Ира', sayHi: sayHi,};const user = { name: 'Ира', sayHi,};Объявлять функцию заранее тоже не обязательно. Если она нужна только этому объекту, её пишут прямо в литерале, на месте значения свойства – для этого есть запись метода: имя, скобки параметров, тело и запятая после закрывающей скобки, как после любого свойства:
const user = { name: 'Ира', sayHi() { console.log(`Привет, ${this.name}!`); },};user.sayHi();Все три записи дают одно и то же: в свойстве sayHi лежит функция, а user. её вызывает. Метод – не отдельный вид значения и не особая часть объекта, а свойство, в котором лежит функция.
Функция попадает в свойство объекта тремя записями. Первая – короткая запись метода, вторая – свойство с обычной функцией, третья – свойство со стрелочной функцией:
// короткая запись методаconst a = { sayHi() { console.log('Привет!'); },};// свойство с обычной функциейconst b = { sayHi: function () { console.log('Привет!'); },};// свойство со стрелочной функциейconst c = { sayHi: () => { console.log('Привет!'); },};Все три кладут функцию в свойство sayHi, и вызывается она одинаково – user.. А вот с this записи ведут себя не одинаково: короткая запись и обычная function подходят методам, которым нужен this, а стрелочная функция собственного this не получает.
«Свой this» означает, что при вызове через объект функция получает этот объект: в user. внутри метода this – это user, и this. читает user.. Стрелочная функция такого this не получает – её this приходит из кода вокруг объекта, и вызов через user его не меняет.
Короткая запись – sayHi() { … } – встречается чаще остальных, поэтому дальше методы записаны ею.
Метод лежит внутри объекта, и данные этого объекта нужны ему для работы. Добраться до них можно самым прямым способом – по имени переменной, в которой объект хранится:
const player = { name: 'Ира', score: 0, addScore(points) { player.score += points; },};player.addScore(10);console.log(player.score);Вызов player. увеличил score до 10 – код работает. Пока в программе один объект и одно имя, запись player. внутри метода ничем не хуже любой другой, и this выглядит лишним.
Где запись по имени ломается
Разница видна, когда одну и ту же функцию кладут в два разных объекта. Свойство addScore в обоих объектах хранит одно значение – функцию, объявленную выше:
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);Оба вызова изменили player.: сначала на 10, затем ещё на 50 – вышло 60. У enemy осталось 100, хотя вызов был enemy..
Дело в строке player.: имя player записано в функции жёстко. Куда бы функцию ни положили, она идёт к объекту с этим именем, а объект, через который её вызвали, на результат не влияет.
Что меняет this
Чтобы функция работала с объектом, через который её вызвали, вместо имени переменной пишут this:
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);В функции изменилось одно слово, а результат другой: player. стал 10, enemy. – 150. Каждый вызов пришёл к своему объекту.
this позволяет функции не знать имя объекта заранее. Вместо «измени player.» в ней записано «измени score того объекта, через который меня вызвали».
function addScore(points) { this.score += points;}function addScore(points) { player.score += points;}this функция меняет объект, через который её вызвали; с именем player – всегда один и тот же объект.this определяется вызовом
this не нужно передавать параметром. Для обычной функции его значение определяется тем, как функцию вызвали.
В player. функция вызвана через player, поэтому внутри этого вызова this – это player. В enemy. та же функция вызвана через enemy, поэтому this – это enemy.
Отсюда рабочее правило для обычных методов: в вызове object. значением this внутри метода становится object.
this.this.this зависит от вызова
Значение this не закрепляется за функцией в момент, когда её создали. Одна функция showName в двух объектах печатает разные имена:
function showName() { console.log(this.name);}const user = { name: 'Ира', showName,};const admin = { name: 'Макс', showName,};user.showName();admin.showName();this тоже разный.Функция здесь одна: в свойстве showName у user и у admin лежит одно и то же значение. Разные у них вызовы – а вместе с вызовом разный и this.
this – не сокращение для имени объекта, а значение, которое определяется способом вызова.
Раз this даёт доступ к объекту вызова, метод может работать с ним так же, как любой другой код: читать свойства, изменять их и использовать в вычислениях:
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);Метод может возвращать значение
Метод не обязан менять объект. Здесь total() только читает price и quantity, вычисляет результат и возвращает его:
const order = { price: 350, quantity: 3, total() { return this.price * this.quantity; },};const sum = order.total();console.log(sum);Почему для методов с this не берут стрелку
В свойство можно положить и стрелочную функцию. Запись выглядит привычно, и вызов ничем не отличается от вызова любого другого метода. Проверим, чем окажется this в момент такого вызова:
const outside = this;const user = { name: 'Ира', sayHi: () => { console.log(this === user); console.log(this === outside); },};user.sayHi();Вызов user. выполнился, но this внутри стрелки – не user: первая строка вывела false.
Стрелочная функция не получает this от вызова, а берёт его из кода вокруг объекта – поэтому вторая строка выводит true: внутри стрелки this тот же, что в outside. Что именно в нём лежит, зависит от того, где выполняется программа, но объектом user он не становится.
const user = { name: 'Ира', sayHi() { console.log(this.name); },};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 меняет состояние конкретной корзины: обнуляет её items и total. Это действие имеет смысл только вместе с этими данными, поэтому и записано рядом с ними.
Состояние и поведение
У многих сущностей в программе есть и данные, и действия над этими данными. Данные называют состоянием объекта, действия – его поведением. Когда есть и то и другое, объект держит их вместе:
Такая сборка группирует код по смыслу: свойства и методы одной сущности лежат в одном месте, а снаружи достаточно знать названия действий – player., cart. – и не разбираться в их устройстве.
На объединении состояния и действий в один объект построено объектно-ориентированное программирование.
Полный пример: объект игрока хранит своё состояние и сам его меняет.
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());Свойства name, health и score – состояние игрока. Методы takeDamage, addScore и isAlive – действия над этим состоянием, и каждый из них работает со своим объектом через this.
Код снаружи не считает здоровье сам: он вызывает player., а как применяется урон и почему здоровье не уходит в минус – дело метода. Правило записано в одном месте, и в большой программе это заметно: операции над одними и теми же данными не разбросаны по проекту, а лежат рядом с ними.
// теперь вы знаете
- Метод – функция в свойстве объекта:
sayHi() { … }в литерале, вызовuser.sayHi() - В вызове
object.method() thisвнутри метода – объект, через который вызван метод;this.вname user.– этоsayHi() user.name thisопределяется вызовом, а не местом записи: одна функция в двух объектах даёт разныеthis- Через
thisметод читает и меняет данные своего объекта, параметры передают данные извне, а результат можно вернуть:order.– выражение со значениемtotal() - Короткая форма
sayHi() {}иsayHi: function () {}для обычных методов работают одинаково; у стрелочной функции своегоthisнет - Метод нужен там, где действие относится к данным объекта:
formatPrice(price)остаётся отдельной функцией,cart.– метод корзиныclear() - Свойства хранят состояние объекта, методы описывают поведение – данные и операции над ними лежат рядом
Что появится в консоли?
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.. Верно ли, что при вызове user. внутри 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;}