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

—

от автора

Привет, Хабр! Представляю вашему вниманию перевод статьи «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.


ссылка на оригинал статьи https://habr.com/post/421959/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *