Как стать автором
Обновить

Что делать, когда “this” теряет ссылку на контекст

Время на прочтение4 мин
Количество просмотров19K
Привет, Хабр! Представляю вашему вниманию перевод статьи «What to do when “this” loses context» автора Cristi Salcescu.

Лучший способ избежать потери контекста 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.
Теги:
Хабы:
Всего голосов 23: ↑16 и ↓7+9
Комментарии24

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань