Объекты обычно создаются, чтобы представлять сущности реального мира, будь то пользователи, заказы и так далее:
// Объект пользователя
let user = {
name: "John",
age: 30
};
И так же, как и в реальном мире, пользователь может совершать действия: выбирать что-то из корзины покупок, авторизовываться, выходить из системы, оплачивать и т.п.
Такие действия в JavaScript представлены функциями в свойствах.
Примеры методов
Для начала давайте научим нашего пользователя user
здороваться:
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("Привет!");
};
user.sayHi(); // Привет!
Здесь мы просто использовали Function Expression (функциональное выражение), чтобы создать функцию приветствия, и присвоили её свойству user.sayHi
нашего объекта.
Затем мы можем вызвать ee как user.sayHi()
. Теперь пользователь может говорить!
Функцию, которая является свойством объекта, называют методом этого объекта.
Итак, мы получили метод sayHi
объекта user
.
Конечно, мы могли бы использовать заранее объявленную функцию в качестве метода, вот так:
let user = {
// ...
};
// сначала, объявляем
function sayHi() {
alert("Привет!");
}
// затем добавляем в качестве метода
user.sayHi = sayHi;
user.sayHi(); // Привет!
Когда мы пишем наш код, используя объекты для представления сущностей реального мира, – это называется объектно-ориентированным программированием или сокращённо: «ООП».
ООП является большой предметной областью и интересной наукой самой по себе. Как выбрать правильные сущности? Как организовать взаимодействие между ними? Это – создание архитектуры, и на эту тему есть отличные книги, такие как «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» авторов Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес или «Объектно-ориентированный анализ и проектирование с примерами приложений» Гради Буча, а также ещё множество других книг.
Сокращённая запись метода
Существует более короткий синтаксис для методов в литерале объекта:
// эти объекты делают одно и то же
user = {
sayHi: function() {
alert("Привет");
}
};
// сокращённая запись выглядит лучше, не так ли?
user = {
sayHi() { // то же самое, что и "sayHi: function(){...}"
alert("Привет");
}
};
Как было показано, мы можем пропустить ключевое слово "function"
и просто написать sayHi()
.
Нужно отметить, что эти две записи не полностью эквивалентны. Есть тонкие различия, связанные с наследованием объектов (что будет рассмотрено позже), но на данном этапе изучения это неважно. Почти во всех случаях сокращённый синтаксис предпочтителен.
Ключевое слово «this» в методах
Как правило, методу объекта обычно требуется доступ к информации, хранящейся в объекте, для выполнения своей работы.
Например, коду внутри user.sayHi()
может потребоваться имя пользователя, которое хранится в объекте user
.
Для доступа к информации внутри объекта метод может использовать ключевое слово this
.
Значение this
– это объект «перед точкой», который используется для вызова метода.
Например:
let user = {
name: "John",
age: 30,
sayHi() {
// "this" - это "текущий объект".
alert(this.name);
}
};
user.sayHi(); // John
Здесь во время выполнения кода user.sayHi()
значением this
будет являться user
(ссылка на объект user
).
Технически также возможно получить доступ к объекту без ключевого слова this
, обратившись к нему через внешнюю переменную (в которой хранится ссылка на этот объект):
let user = {
name: "John",
age: 30,
sayHi() {
alert(user.name); // "user" вместо "this"
}
};
…Но такой код ненадёжен. Если мы решим скопировать ссылку на объект user
в другую переменную, например, admin = user
, и перезапишем переменную user
чем-то другим, тогда будет осуществлён доступ к неправильному объекту при вызове метода из admin
.
Это показано ниже:
let user = {
name: "John",
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: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert( this.name );
}
// используем одну и ту же функцию в двух объектах
user.f = sayHi;
admin.f = sayHi;
// эти вызовы имеют разное значение this
// "this" внутри функции - это объект "перед точкой"
user.f(); // John (this == user)
admin.f(); // Admin (this == admin)
admin['f'](); // Admin (нет разницы между использованием точки или квадратных скобок для доступа к объекту)
Правило простое: если вызывается obj.f()
, то во время вызова f
, this
– это obj
. Так что, в приведённом выше примере это либо user
, либо admin
.
this == undefined
Мы даже можем вызвать функцию вообще без объекта:
function sayHi() {
alert(this);
}
sayHi(); // undefined
В строгом режиме ("use strict"
) в таком коде значением this
будет являться undefined
. Если мы попытаемся получить доступ к this.name
– это вызовет ошибку.
В нестрогом режиме значением this
в таком случае будет глобальный объект (window
в браузерe, мы вернёмся к этому позже в главе Глобальный объект). Это – исторически сложившееся поведение this
, которое исправляется использованием строгого режима ("use strict"
).
Обычно подобный вызов является ошибкой программирования. Если внутри функции используется this
, тогда она ожидает, что будет вызвана в контексте какого-либо объекта.
this
Если вы до этого изучали другие языки программирования, то вы, вероятно, привыкли к идее «фиксированногоthis
» – когда методы, определённые в объекте, всегда имеют this
, ссылающееся на этот объект.
В JavaScript this
является «свободным», его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а скорее от того, какой объект вызывает метод (какой объект стоит «перед точкой»).
Эта концепция вычисления this
в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок.
Здесь наша позиция заключается не в том, чтобы судить, является ли это архитектурное решение в языке хорошим или плохим. Скоро мы поймем, как с этим работать, как получить выгоду и избежать проблем.
У стрелочных функций нет «this»
Стрелочные функции особенные: у них нет своего «собственного» this
. Если мы ссылаемся на this
внутри такой функции, то оно берётся из внешней «нормальной» функции.
Например, здесь arrow()
использует значение this
из внешнего метода user.sayHi()
:
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Ilya
Это особенность стрелочных функций. Она полезна, когда мы на самом деле не хотим иметь отдельное this
, а скорее хотим взять его из внешнего контекста. Позже в главе Повторяем стрелочные функции мы увидим больше примеров на эту тему.
Итого
- Функции, которые находятся в свойствах объекта, называются «методами».
- Методы позволяют объектам «действовать»:
object.doSomething()
. - Методы могут ссылаться на объект через
this
.
Значение this
определяется во время исполнения кода.
- При объявлении любой функции в ней можно использовать
this
, но этотthis
не имеет значения до тех пор, пока функция не будет вызвана. - Функция может быть скопирована между объектами (из одного объекта в другой).
- Когда функция вызывается синтаксисом «метода» –
object.method()
, значениемthis
во время вызова являетсяobject
.
Также ещё раз заметим, что стрелочные функции являются особенными – у них нет this
. Когда внутри стрелочной функции обращаются к this
, то его значение берётся извне.