Лучший способ избежать потери контекста this – не использовать this. Однако, это не всегда возможно. Например, мы работаем с чужим кодом или библиотекой, которые используют this.
Литерал объекта, функция-конструктор, конструктор объектов класса в системе прототипов. Псевдопараметр this используется в системе прототипирования для того, чтобы дать доступ к свойствам объекта.
Давайте рассмотрим несколько случаев.
Вложенные функции (Nested Functions)
this теряет ссылку на контекст внутри вложенных функций.
class Service {
constructor(){
this.numbers = [1,2,3];
this.token = "token";
}
doSomething(){
setTimeout(function doAnotherThing(){
this.numbers.forEach(function log(number){
//Cannot read property 'forEach' of undefined
console.log(number);
console.log(this.token);
});
}, 100);
}
}
let service = new Service();
service.doSomething();
У метода doSomething() две вложенные функции: doAnotherthing() и log(). При вызове service.doSomething(), this теряет ссылку на контекст во вложенной функции.
bind()
Один из способов решения проблемы – метод bind(). Взгляните на следующий код:
doSomething(){
setTimeout(function doAnotherThing(){
this.numbers.forEach(function log(number){
console.log(number);
console.log(this.token);
}.bind(this));
}.bind(this), 100);
}
bind() создает новую версию функции, которая при вызове уже имеет определенное значение this.
function doAnotherThing(){ /*…*/}.bind(this) создает версию функции doAnotherThing(), которая берет значение this из doSomething().
that/self
Другой вариант – объявить и использовать новую переменную that/self, которая будет хранить значение this из метода doSomething().
doSomething(){
let that = this;
setTimeout(function doAnotherThing(){
that.numbers.forEach(function log(number){
console.log(number);
console.log(that.token);
});
}, 100);
}
Мы должны объявлять let that = this во всех методах, использующих this во вложенных функциях.
Стрелочные функции (Arrow function)
Стрелочная функция даёт нам еще один способ решения этой проблемы.
doSomething(){
setTimeout(() => {
this.numbers.forEach(number => {
console.log(number);
console.log(this.token);
});
}, 100);
}
Стрелочные функции не создают собственный контекст для this, а используют значение this окружающего контекста. В примере выше она использует значение this родительской функции.
Недостаток этого способа в том, что мы не можем задать имя стрелочной функции. Имя функции играет важную роль, так как повышает читабельность кода и описывает её назначение.
Ниже представлен тот же код с функцией, выраженной через имя переменной:
doSomething(){
let log = number => {
console.log(number);
console.log(this.token);
}
let doAnotherThing = () => {
this.numbers.forEach(log);
}
setTimeout(doAnotherThing, 100);
}
Функции обратного вызова (Method as callback)
this теряет ссылку на контекст при использовании метода в качестве функции обратного вызова. Посмотрим на следующий класс:
class Service {
constructor(){
this.token = "token";
}
doSomething(){
console.log(this.token);//undefined
}
}
let service = new Service();
Давайте разберем ситуации, в которых метод service.doSomething() используется как коллбэк-функция.
//callback on DOM event
$("#btn").click(service.doSomething);
//callback for timer
setTimeout(service.doSomething, 0);
//callback for custom function
run(service.doSomething);
function run(fn){
fn();
}
Во всех случаях выше this теряет ссылку на контекст.
bind()
Мы можем использовать bind() для решения этой проблемы. Ниже приведен код этого варианта:
//callback on DOM event
$("#btn").click(service.doSomething.bind(service));
//callback for timer
setTimeout(service.doSomething.bind(service), 0);
//callback for custom function
run(service.doSomething.bind(service));
Стрелочная функция
Еще один способ – создание стрелочной функции, которая вызывает service.doSomething().
//callback on DOM event
$("#btn").click(() => service.doSomething());
//callback for timer
setTimeout(() => service.doSomething(), 0);
//callback for custom function
run(() => service.doSomething());
React-компоненты (React Components)
В компонентах this теряет ссылку на контекст, когда методы используются в качестве коллбэков для событий.
class TodoAddForm extends React.Component {
constructor(){
super();
this.todos = [];
}
componentWillMount() {
this.setState({desc: ""});
}
add(){
let todo = {desc: this.state.desc};
//Cannot read property 'state' of undefined
this.todos.push(todo);
}
handleChange(event) {
//Cannot read property 'setState' of undefined
this.setState({desc: event.target.value});
}
render() {
return <form>
<input onChange={this.handleChange} value={this.state.desc} type="text"/>
<button onClick={this.add} type="button">Save</button>
</form>;
}
}
ReactDOM.render(
<TodoAddForm />,
document.getElementById('root'));
В качестве решения мы можем создать новые функции в конструкторе, которые будут использовать bind(this).
constructor(){
super();
this.todos = [];
this.handleChange = this.handleChange.bind(this);
this.add = this.add.bind(this);
}
Не использовать “this"
Нет this — нет проблем с потерей контекста. Объекты могут создаваться с помощью фабричных функций (factory functions). Посмотрите на этот пример:
function Service() {
let numbers = [1,2,3];
let token = "token";
function doSomething(){
setTimeout(function doAnotherThing(){
numbers.forEach(function log(number){
console.log(number);
console.log(token);
});
}, 100);
}
return Object.freeze({
doSomething
});
}
Контекст остается если использовать метод в качестве коллбэка.
let service = Service();
service.doSomething();
//callback on DOM event
$("#btn").click(service.doSomething);
//callback for timer
setTimeout(service.doSomething, 0);
//callback for custom function
run(service.doSomething);
Заключение
this теряет ссылку на контекст в различных ситуациях.
bind(), использование переменной that/self и стрелочные функции — это способы решения проблем с контекстом.
Фабричные функции дают возможность создавать объекты без использования this.
Комментарии (21)
aamonster
31.08.2018 19:09+1Ещё один не разобравшийся в JavaScript пишет свои туториалы…
this теряется совсем не потому, что это вложенная функция — просто реализация setTimeout вызывает её с другим this.
Zenitchik
31.08.2018 21:28+3Абсурд. this — это и есть контекст, в котором вызвана функция.
Если функция вызывается не в том контексте — значит что-то с руками у того, кто её вызывает. Выпрямляйте руки.mayorovp
02.09.2018 09:24Ну так и весь пост — это и есть инструкция по выпрямлению рук. В чем тут абсурд-то?
Myateznik
01.09.2018 01:33В случае с вложенными функциями (Nested Functions) для решения «проблемы смены контекста» лучше всего использовать стрелочные функции ведь это одна из причин существования стрелочных функций.
В случае с функциями обратного вызова (Method as callback) и особенно React-компонентами есть один очень интересный способ решения (правда он «ESNEXT candidate stage-3») — указание метода класса через поле экземпляра класса (instance class field) и стрелочной функции как в решении с Nested Functions:
class Service { constructor(){ this.token = "token"; } doSomething: () => console.log(this.token) } // Равносильно: class Service { constructor(){ this.token = "token"; this.doSomething = () => console.log(this.token); } } // Или: function Service() { this.token = "token"; this.doSomething = () => console.log(this.token); }
mamont80
01.09.2018 11:40Для React я использую npm пакет react-autobind. Вызывается 1 раз в конструкторе и все методы в объекте будут приколочены, потом можно не париться.
constructor() { super(); autoBind(this); }
Valery4
02.09.2018 21:29В текущей версии Реакт с актуальными средствами транспилинга — вообще редко когда нужен конструктор.
State можно объявить сразу напрямую, как объект, это и будет по сути сахар для конструктора. А методы можно тоже через стрелочные функции создавать.
class MyComponent extends PureComponent { state = {counter: 0}; myMethod = () => doSomething... render() {...} }
Vahman
01.09.2018 23:26this не умеет терять ссылку на контекст. Многие программисты умеют неправильно готовить js, ибо this в javascript и является контекстом исполнения функции, зависящим от того, как она была вызвана. Рекомендую к прочтению книгу "you don't know JS", раздел про this.
keslo
02.09.2018 00:02Тут теряется не this, а разработчик при работе с ним. Прочитав статью, человек как не разбирался с this, так и не будет разбираться. Хорошо было бы указать почему меняется this и какой его значение при вызове в примерах.
К переводчику никаких претензий.
Mikluho
02.09.2018 20:18+2Нда… Я не знаю, почему автор назвал статью именно так, но многих это может ввести в заблуждение, ибо…
this не может потерять контекст, так как this и есть ссылка на контекст!
Правильнее говорить о том, что делать, если контекст не тот, который ожидал разработчик.
И после этого всё встаёт на свои места.
this можно использовать в инициализаторах и в тех местах, где конкретный контекст гарантирован. А если уж есть готовый метод, который использует this — ему всегда можно подсунуть то, что нам нужно.
Free_ze
Есть объективная причина делать не
const that = this;
?ptzoo Автор
Для более строгой и чёткой логики(best practice) лучше использовать const везде, где это возможно. Однако в оригинале статьи приведены именно такие примеры и они являются ознакомительными. Использования let и const всегда остаются на усмотрение разработчика, ведь этот момент затрагивает не только логический, но и семантический аспект.
mayorovp
В случае that или self заведомо никогда меняться не будет, так что семантика у const правильная. Даже taujavarob не сможет ничего возразить :-)
ptzoo Автор
Никаких возражений, все абсолютно верно.
Vindicar
А в JS const — это константность ссылки на объект, а не константность значения объекта?
Т. е. не как в C++, где для const объекта можно вызывать только const методы, не изменяющие его состояния?
mayorovp
В JS const — это неизменяется переменная. Объект на который она указывает можно изменять.
JiLiZART
Вообще луше вместо that использовать переменные которые конкретно нужны функции.
const numbers = this.numbers;
Зачем затруднять сборку мусора и плодить утечки памяти.
Valery4
Не факт что в данном случае это более удобно, но часто для таких случаев использую
Более реальный пример: