Методы объектов, ключевое слово this

На предыдущем занятии мы с вами познакомились с объектами и их свойствами. Например, объект автомобиль:

let car = {
    model: "toyota",
    color: "black",
};

Здесь заданы два свойства (model, color) для характеристики модели и цвета машины. То есть, свойства – это данные. Но автомобиль обладает не только какими-то характеристиками (максимальной скоростью, типом трансмиссии, типом кузова и т.п.) он способен к действию! И самое распространенное его действие – ехать. Как описать действие? Помните, когда мы говорили о функциях, то упоминали, что они реализуют определенные действия? Так вот, ровно такие же функции можно помещать в объекты. Зададим свойство go объекта car, которое будет являться ссылкой на функцию:

let car = {
    model: "toyota",
    color: "black",
    go: function() {
          console.log("машина едет");
    }
};

Такое свойство называется методом объекта. То есть, любое свойство, которое является ссылкой на функцию, называют методом объекта. А название метода должно отвечать на вопрос «что делает метод?» и быть глаголом. В нашем случае как раз так и есть: go – это глагол, означающий идти, ехать и все в таком духе.

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

car.go();

Опять же, вспоминая занятия по функциям, мы говорили, что для вызова функции нужно указать ее имя и круглые скобки. Здесь ровно так все и записано. Отлично, теперь наша машина едет!

Давайте, теперь остановим ее. Добавим в объект car еще один метод stop, который будет это делать. Пропишем его следующим образом:

car.stop = function () {
    console.log("машина остановлена");
}

Смотрите, мы здесь используем объявление функции по синтаксису Function Experssion. В результате в car добавляется новый метод stop. Вызовем его:

car.stop();

Теперь наш автомобиль умеет еще и останавливаться.

Методы, как и любая функция могут принимать аргументы. Например, добавим аргумент методу go, передавая ему имя водителя:

let car = {
    model: "toyota",
    color: "black",
    go: function (driverName) {
          console.log("Водитель " + driverName + ": машина едет");
    },
    stop: function () {
          console.log("машина остановлена");
    }
};

И вызовем этот метод так:

car.go("Федор");

Теперь мы знаем не только, что машина едет, но и кто ее ведет. Кстати, новый стандарт ES6 допускает записывать методы в более краткой форме, вот так:

let car = {
    model: "toyota",
    color: "black",
    go(driverName) {
        console.log("Водитель " + driverName + ": машина едет");
    },
    stop() { 
        console.log("машина остановлена");
    }
};

Мы здесь убрали двоеточие и слово function, оставили только круглые скобки. Такая запись смотрится естественнее и по смыслу абсолютно такая же, что и в предыдущем случае.

Ключевое слово this

Предположим, что мы хотим добавить еще один метод, который бы возвращал марку машины. Если записать его вот так:

    getModel() {
        return model;
    }

то при вызове этого метода:

console.log( car.getModel() );

возникнет ошибка: переменная model не существует. И действительно, такой переменной нет, есть свойство model. Чтобы обратиться к этому свойству внутри объекта, используется ключевое слово this:

    getModel() {
         return this.model;
    }

Фактически, this – это ссылка на текущий объект. И через эту ссылку можно обращаться к любому свойству и методу внутри этого объекта. Теперь, если мы запустим скрипт, то убедимся, что все работает.

В JavaScript ключевое слово «this» ведёт себя иначе, чем в большинстве других языков программирования. И для примера мы посмотрим на вот такую программу:

let car1 = {model: "toyota"};
let car2 = {model: "opel"};
 
car1.getModel = getModel;
car2.getModel = getModel;
 
console.log( car1.getModel() );
console.log( car2.getModel() );
 
function getModel() {
    return this.model;
}

Смотрите, здесь два разных объекта car1 и car2, в которых есть свойство model. В низу прописана функция getModel, которая возвращает это свойство. Обратите внимание, в ее теле присутствует ключевое слово this. Казалось бы, к чему оно должно относиться? Ведь функция не прописана в объекте? Но в JavaScript ссылка this динамически вычисляется в момент выполнения скрипта, в данном случае this будет вычисляться только в момент вызова этой функции. Далее, в объекты car1 и car2 добавляем методы getModel, которые являются ссылками на одну и ту же функцию. Теперь, когда идет вызов функции getModel через ссылку первого объекта car1, this указывает на объект car1. Когда функция вызывается через ссылку из объекта car2, this указывает на этот второй объект. Вот такое нетривиальное поведение ссылки this существует в JavaScript.

Конечно, если мы просто попытаемся вызвать функцию

getModel();

то получим ошибку выполнения, так как this в ней будет иметь значение undefined (при включенном режиме "use strict") и никакого доступа к свойству model мы здесь не получим. Если у вас не включен строгий режим, то в браузерах ссылка this будет указывать на глобальный объект window. Но, обычно, если программист задает такую функцию, то ожидается, что this будет ссылаться на какой-либо объект из контекста вызова этого метода.

Если вы до этого изучали другие языки программирования, то, скорее всего, привыкли к идее "фиксированного this" – когда методы, определённые внутри объекта, всегда сохраняют в качестве значения this ссылку на свой объект (в котором был определён метод).

В JavaScript this является «свободным», его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а зависит от того, какой объект вызывает метод (какой объект стоит «перед точкой»).

Эта идея вычисления this в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок. Например, посмотрите на такую программу:

let car = {
    model: "toyota",
    getModel() {
        return this.model;
    }         
};
 
let get = car.getModel();
 
console.log( car.getModel() );
console.log( get() );

На первый взгляд может показаться, что здесь все в порядке. Но на самом деле, вот этот последний вызов get() завершится ошибкой. Почему? Дело в том, что мы создали ссылку get на функцию getModel объекта car. И когда вызывается get(), то это эквивалентно вызову getModel вне объекта и this становится равным undefined. Из-за этого возникает ошибка. Вот на такие моменты при работе с ключевым словом this следует обращать внимание.

Видео по теме