В языке JavaScript есть такая вещь как this. Часто взаимодействие с ним вызывает трудности у начинающих разработчиков. Надеюсь, после прочтения этой заметки вам никогда больше не придётся сталкиваться с неожиданным поведением ваших функций. Здесь рассматривается сущность this и управление его поведением в часто встречающихся сценариях.
Мы используем this так же, как местоимение в языках подобных английскому или русскому. Не всегда называем имя или предмет, а говорим применительно к нему «этот». То же самое происходит при использовании ключевого слова this в языках программирования. This всегда является ссылкой на свойство объекта, но что это за объект зависит от контекста.
var Person = {
firstName: "Jack",
lastName: "Sparrow",
fullName: function() {
// this означает "этот"
return this.firstName + ' ' + this.lastName;
// аналогично такой записи:
return Person.firstName + ' ' + Person.lastName;
}
};
console.log(Person.fullName());
//-> Jack Sparrow
почему this
Если вместо this обращаться к объекту через его имя (Person), мы рискуем вызвать метод совсем не того объекта. Ведь код может быть написан не одним человеком. Вы уверены, что в скриптах, которые писали не вы, не будет глобальной переменной с таким же именем?
Объекты, как и функции, имеют свойства. Когда функция выполняется, она получает
свойство this того объекта, который её вызвал. This обычно используется внутри
функции, но при вызове в глобальной области видимости его значение будет
зависеть от окружения. Например, для браузера глобальным объектом будет window,
и код console.log(this.innerHeight);
вернёт высоту окна.
При использовании строгого режима (strict mode) это не сработает: this не будет связан с глобальным объекто м и код вызовет ошибку. Такое же поведение ожидаемо с анонимными функциями, ведь они не привязаны к конкретному объекту.
Небольшой пример того, как работает this с jQuery:
$('button').click(function(event) {
// $(this) указывает на $('button') потому что объект $('button')
// запускает метод click, в котором определён this
console.log( $(this).prop('name') );
});
Когда что-то может пойти не так
Кажется, что всё просто как дважды два. Тогда почему this вызывает столько вопросов и ведёт себя не так, как мы ожидаем?
Метод как функция обратного вызова
Объект Boxer
содержит некие данные и метод clickHandler
. Мы ищем объект-DOM
с классом .boxer
. Добавляем обработчик события и в качестве функции пытаемся
передать ему clickHandler
, который будет рандомно отображать в консоли имя
одного из боксёров.
var Boxer = {
data: [
{ name: 'M.Tyson', age: 49},
{ name: 'M.Ali', age: 74}
],
clickHandler: function() {
var randomNum = Math.random() * 2|0;
console.log(this.data[randomNum].name + ' ' + this.data[randomNum].age);
}
};
document.querySelector('.boxer').addEventListener('click', Boxer.clickHandler);
// -> Cannot read property '1' of undefined
Ничего не вышло. Всё дело в том, что функцию в данном контексте вызывает
DOM-объект .boxer
. Наш Boxer.clickHandler
пытается быть выполненным в контексте
.boxer
.
Решение: явно привязать функцию обратного вызова к объекту через метод bind()
.
// передаём нужный объект методу bind()
document.querySelector('.boxer').
addEventListener('click', Boxer.clickHandler.bind(Boxer));
Теперь всё в порядке.
this внутри замыкания
Другой случай, когда поведение this может поставить в тупик: если он находится внутри замыкания. Тогда мы не можем получить доступ к this внешней функции. Перепишем код примера выше так, чтобы он удовлетворял новому условию.
var Boxer = {
'type of sport': 'boxing',
data: [
{ name: 'M.Tyson', age: 49 },
{ name: 'M.Ali', age: 74 }
],
clickHandler: function() {
// ссылается на Boxer
this.data.forEach(function(person) {
// теперь мы внутри data
console.log(person.name +
'. His type of sport ' + this['type of sport']);
});
}
};
Boxer.clickHandler();
//-> M.Tyson. His type of sport undefined
Раз this из замыкания не имеет доступа к this внешних функций, он будет привязан к глобальному объекту (если не включен строгий режим).
Решение: передать значение this другой переменной перед тем как создать замыкание. Такую переменную принято называть that.
clickHandler: function() {
// передаём значение текущего this
var that = this;
this.data.forEach(function(person) {
console.log(person.name +
'. His type of sport ' + that['type of sport']);
console.log(that.data[0]['age']);
});
}
// M.Tyson. His type of sport boxing
// 49
Хорошие новости. В ES6 делать этого не нужно. Спокойно пользуйтесь стрелочными функциями, они не имеют своего this и просто берут тот, что снаружи.
var Boxer = {
'type of sport': 'boxing',
data: [
{ name: 'M.Tyson', age: 49 },
{ name: 'M.Ali', age: 74 },
],
clickHandler() {
this.data.forEach(person => console.log(
`${person.name}. His sport ${this['type of sport']}`)
);
},
};
Boxer.clickHandler();
метод объекта передан в переменную
Вернёмся к нашим боксёрам. Добавим пару чемпионов.
var data = [
{ name: 'Joe Louis' },
{ name: 'Lennox Lewis' }
];
var Boxer = {
data: [
{ name: 'Mike Tyson', age: 49 },
{ name: 'Muhammad Ali', age: 74 }
],
clickHandler: function() {
console.log (this.data[0].name);
}
};
var showBoxer = Boxer.clickHandler;
console.log(data[0].name); //-> Joe Louis
console.log(showBoxer()); //-> Joe Louis
Вызов data
из глобальной области видимости и вызов showBoxer
оказался
одинаковым. Когда showBoxer
срабатывает, вызывается анонимная функция:
function() {
console.log (this.data[0].name);
}
Всё верно, этот кусок кода был «заморожен» в clickHandler
. Однако,
сейчас он выполнился в контексте глобального объекта.
Решение: снова явно привязать метод к объекту.
var showBoxer = Boxer.clickHandler.bind(Boxer);
console.log(showBoxer());
//-> Mike Tyson
заимствованные методы
Четвёртый и последний случай: имеем два объекта, у одного из них хотим вызвать метод другого. Допустим, рассчитать среднее количество очков на раунд игры.
Ниже используется метод reduce
. Он принимает значения:
- последний (он же промежуточный) результат (prev)
- текущий элемент массива (cur)
- номер текущего элемента (index)
- передаваемый массив (array)
В getTotal
передаём переменной сумму всех очков, затем делим их на количество
раундов.
// первая игра
var angryBirds = {
// количество очков, 4 раунда
scores: [10, 5, 15, 30],
// среднее значение ещё не рассчитано
average: null
};
// вторая игра
var angryCats = {
// количество очков, 5 раундов
scores: [25, 32, 16, 43, 56],
average: null,
getTotal: function () {
var sumOfScores = this.scores.reduce(function (prev, cur, index, array) {
return prev + cur;
});
this.average = sumOfScores / this.scores.length;
}
};
angryBirds.average = angryCats.getTotal();
console.log(angryBirds.average);
Но такой фокус не пройдёт. Метод getTotal
вызвается объектом angryCats.
Решение: apply
позволяет вызывать метод одного объекта в контексте другого.
angryCats.getTotal.apply(angryBirds, angryBirds.scores);
console.log(angryBirds.average);
Объект angryBirds «одолжил» метод getTotal
у angryCats. This передана
angryBirds, потому что он указан первым параметром метода apply
.