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

Комментарии (21)


  1. Free_ze
    31.08.2018 18:05

    Мы должны объявлять let that = this во всех методах, использующих this во вложенных функциях.

    Есть объективная причина делать не const that = this;?


    1. ptzoo Автор
      31.08.2018 18:29

      Для более строгой и чёткой логики(best practice) лучше использовать const везде, где это возможно. Однако в оригинале статьи приведены именно такие примеры и они являются ознакомительными. Использования let и const всегда остаются на усмотрение разработчика, ведь этот момент затрагивает не только логический, но и семантический аспект.


      1. mayorovp
        31.08.2018 19:04

        В случае that или self заведомо никогда меняться не будет, так что семантика у const правильная. Даже taujavarob не сможет ничего возразить :-)


        1. ptzoo Автор
          31.08.2018 19:31

          Никаких возражений, все абсолютно верно.


        1. Vindicar
          01.09.2018 16:43

          А в JS const — это константность ссылки на объект, а не константность значения объекта?

          Т. е. не как в C++, где для const объекта можно вызывать только const методы, не изменяющие его состояния?


          1. mayorovp
            01.09.2018 17:03

            В JS const — это неизменяется переменная. Объект на который она указывает можно изменять.


    1. JiLiZART
      01.09.2018 16:53

      Вообще луше вместо that использовать переменные которые конкретно нужны функции.
      const numbers = this.numbers;
      Зачем затруднять сборку мусора и плодить утечки памяти.


      1. Valery4
        02.09.2018 21:23

        Не факт что в данном случае это более удобно, но часто для таких случаев использую

        const { numbers } = this;

        Более реальный пример:
        const { propOne, propTwo } = this.props;


  1. aamonster
    31.08.2018 19:09
    +1

    Ещё один не разобравшийся в JavaScript пишет свои туториалы…
    this теряется совсем не потому, что это вложенная функция — просто реализация setTimeout вызывает её с другим this.


  1. sanchezzzhak
    31.08.2018 20:02
    +3

    Не разу не терял this за свою жизнь


    1. s-kozlov
      02.09.2018 09:36

      Ни разу не писал на JavaScript?
      image


  1. Zenitchik
    31.08.2018 21:28
    +3

    Абсурд. this — это и есть контекст, в котором вызвана функция.
    Если функция вызывается не в том контексте — значит что-то с руками у того, кто её вызывает. Выпрямляйте руки.


    1. mayorovp
      02.09.2018 09:24

      Ну так и весь пост — это и есть инструкция по выпрямлению рук. В чем тут абсурд-то?


  1. 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);
    }
    


  1. mamont80
    01.09.2018 11:40

    Для React я использую npm пакет react-autobind. Вызывается 1 раз в конструкторе и все методы в объекте будут приколочены, потом можно не париться.

    constructor() {
      super();
      autoBind(this);
    }
    


    1. Alexufo
      01.09.2018 16:01

      Говорят react это новый jQuery. :-)


      1. JiLiZART
        01.09.2018 16:52

        jQuery очень хорош, если уметь его "готовить". Что получается у меньшинства :(
        Абсолютно тоже самое можно сказать про React, но здесь полегче.


    1. Valery4
      02.09.2018 21:29

      В текущей версии Реакт с актуальными средствами транспилинга — вообще редко когда нужен конструктор.
      State можно объявить сразу напрямую, как объект, это и будет по сути сахар для конструктора. А методы можно тоже через стрелочные функции создавать.

      class MyComponent extends PureComponent {
        state = {counter: 0};
      
        myMethod = () => doSomething...
      
        render() {...}
      }


  1. Vahman
    01.09.2018 23:26

    this не умеет терять ссылку на контекст. Многие программисты умеют неправильно готовить js, ибо this в javascript и является контекстом исполнения функции, зависящим от того, как она была вызвана. Рекомендую к прочтению книгу "you don't know JS", раздел про this.


  1. keslo
    02.09.2018 00:02

    Тут теряется не this, а разработчик при работе с ним. Прочитав статью, человек как не разбирался с this, так и не будет разбираться. Хорошо было бы указать почему меняется this и какой его значение при вызове в примерах.

    К переводчику никаких претензий.


  1. Mikluho
    02.09.2018 20:18
    +2

    Нда… Я не знаю, почему автор назвал статью именно так, но многих это может ввести в заблуждение, ибо…
    this не может потерять контекст, так как this и есть ссылка на контекст!
    Правильнее говорить о том, что делать, если контекст не тот, который ожидал разработчик.
    И после этого всё встаёт на свои места.
    this можно использовать в инициализаторах и в тех местах, где конкретный контекст гарантирован. А если уж есть готовый метод, который использует this — ему всегда можно подсунуть то, что нам нужно.