JavaScript

К освоению JavaScript лежит долгий путь, на котором вам может встретиться такое выражение, как this. Первый раз я встретил его в процессе работы с eventListeners и jQuery, а в дальнейшем часто применял с React.

Овладение концепцией использования this и умение применять его со знанием дела не представляют особых трудностей.

Основы this

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

this тесно связано с тем, в каком контексте вы находитесь в программе. Начнем с самого начала. При наборе this консоль выдает объект window, самый внешний контекст JavaScript. При вводе следующего кода в Node.js:

console.log(this)

мы получаем пустой объект {}. Это немного странно, но, скорее всего, это особенность Node.js. Однако при вводе следующего:

(function() {
console.log(this);
})();

получаем объект global, относящийся к самому внешнему контексту. В этом контексте сохранены setTimeout иsetInterval. Не бойтесь немного поэкспериментировать с ними, чтобы узнать все их возможности. С этого момента между Node.js и браузером почти нет никаких различий. Мы будем использовать объект window. Не забывайте, что в Node.js это будет объект global, хотя это не играет особой роли.

Запомните: Контекст имеет смысл только внутри функций.

Представьте, что вы пишите программу с функциями без вложений. Вы просто пишите одну строку за другой, не создавая никаких структур. Это значит, что нет необходимости следить за тем, где вы находитесь, поскольку вы остаетесь на одном уровне.

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

Отслеживание объекта caller

Рассмотрим изменения ключевого слова this в зависимости от контекста:

const coffee = {
  strong: true,
  info: function() {
   console.log(`The coffee is ${this.strong ? '' : 'not '}strong`)
 },
}
coffee.info() // The coffee is strong

Поскольку был совершен вызов функции, объявленной внутри объекта coffee, контекст меняется именно на этот объект. Теперь можно получить доступ ко всем свойствам этого объекта с помощью this. В приведенном выше примере можно получить прямую ссылку на объект с помощью coffee.strong. Становится интереснее, когда мы не знаем, в каком контексте и в каком объекте мы находимся, или в какой момент структура начинает усложняться. Взгляните на следующий пример:

const drinks = [
    {
        name: 'Coffee',
        addictive: true,
        info: function() {
            console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`)
        },
    },
    {
        name: 'Celery Juice',
        addictive: false,
        info: function() {
            console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`)
        },
    },
]

function pickRandom(arr) {
    return arr[Math.floor(Math.random() * arr.length)]
}

pickRandom(drinks).info()

Классы и экземпляры классов

Классы используются, чтобы делать код более абстрактным и разделять их поведение. Не стоит объявлять функцию info несколько раз, как было указано в последнем примере. Поскольку классы и их экземпляры находятся в фактических объектах, то и действуют они одинаково. Однако следует упомянуть, что объявление this в конструкторе представляет собой предсказание на будущее, когда появится экземпляр.

Рассмотрим пример:

class Coffee {
  constructor(strong) {
   this.strong = !!strong
  }
  info() {
    console.log(`This coffee is ${this.strong ? '' : 'not '}strong`)
  }
}
const strongCoffee = new Coffee(true)
const normalCoffee = new Coffee(false)
strongCoffee.info() // This coffee is strong
normalCoffee.info() // This coffee is not strong

Ошибка: вложенные вызовы функции

Иногда мы оказываемся не в том контексте, которого мы ожидали. Это может произойти при вызове функции внутри контекста другого объекта. Самый распространенный пример при использовании setTimeout или setInterval :

// BAD EXAMPLE
const coffee = {
  strong: true,
  amount: 120,
  drink: function() {
    setTimeout(function() {
      if (this.amount) this.amount -= 10
    }, 10) 
  },
}
coffee.drink()

Угадайте, какое значение имеет coffee.amount?

..

.

Все еще 120. Изначально мы находились внутри объекта coffee, поскольку метод drink объявлен внутри него. Мы просто выполнили setTimeout и ничего больше. Дело именно в этом.

Как было сказано ранее, метод setTimeout на самом деле объявлен в объекте window. При вызове этого метода мы возвращаемся к контексту window. Это означает, что инструкции пытались изменить window.amount, но ничего не произошло из-за оператора if. Чтобы это исправить, нужно связать (bind) функции (пример ниже).

React

При работе с React подобные ситуации скоро останутся в прошлом, благодаря Hooks. Но на данный момент все еще необходимо связать (bind) все функции тем или иным способом (подробнее об этом позже). 

Рассмотрим два простых компонента класса React:

// BAD EXAMPLE
import React from 'react'

class Child extends React.Component {
    render() {
        return <button onClick={this.props.getCoffee}>Get some Coffee!</button>
    }
}

class Parent extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            coffeeCount: 0,
        }

        // change to turn into good example – normally we would do:
        // this._getCoffee = this._getCoffee.bind(this)
    }

    render() {
        return (
            <React.Fragment>
                <Child getCoffee={this._getCoffee} />
            </React.Fragment>
        )
    }

    _getCoffee() {
        this.setState({
            coffeeCount: this.state.coffeeCount + 1,
        })
    }
}

При нажатии на кнопку, вынесенную классом Child, происходит ошибка. Почему? Поскольку React изменил контекст при вызове метода _getCoffee.

Я предполагаю, что React вызывает метод render для компонентов в другом контексте, с помощью вспомогательных классов или аналогичным образом (хотя придется копать глубже, чтобы узнать наверняка). Тем не менее this.state не определен, а мы пытаемся получить доступ к this.state.coffeeCount. Программа выдает нечто подобное Cannot read property coffeeCount of undefined.

Чтобы решить эту проблему, необходимо связать (bind) методы в классах при передаче их из компонента, в котором они определены.

Рассмотрим еще один базовый пример:

// BAD EXAMPLE
class Viking {
    constructor(name) {
        this.name = name
    }

    prepareForBattle(increaseCount) {
        console.log(`I am ${this.name}! Let's go fighting!`)
        increaseCount()
    }
}

class Battle {
    constructor(vikings) {
        this.vikings = vikings
        this.preparedVikingsCount = 0

        this.vikings.forEach(viking => {
            viking.prepareForBattle(this.increaseCount)
        })
    }

    increaseCount() {
        this.preparedVikingsCount++
        console.log(`${this.preparedVikingsCount} vikings are now ready to fight!`)
    }
}

const vikingOne = new Viking('Olaf')
const vikingTwo = new Viking('Odin')

new Battle([vikingOne, vikingTwo])

Мы передаем increaseCount из одного класса в другой. При вызове метода increaseCount в Viking контекст уже изменен, а this действительно указывает на Viking. Это означает, что increaseCount не будет работать так, как ожидалось.

Решение — bind

Лучшее решение в этой ситуации — связать (bind) методы, которые будут переданы из первоначального объекта или класса. Есть несколько способов связывания функций, но самый распространенный (даже в React) — это связать их в конструкторе. Нужно добавить эту строку в конструктор Battle:

this.increaseCount = this.increaseCount.bind(this)

Можно привязать любую функцию к любому контексту. Необязательно привязывать функцию к контексту, в котором она объявлена (хотя это самый распространенный случай). Вместо этого, можно привязать ее к другому контексту. С помощью bind всегда устанавливается контекст для объявления функции. Это означает, что все вызовы для этой функции получат связанный контекст, как и в случае с this. Существует еще два способа установки контекста.

Стрелочные функции `() => {}` автоматически привязывают функцию к контексту объявления.

Функции apply и call

Они обе выполняют одно и то же действие, различается лишь их синтаксис. В обоих случаях контекст передается в качестве первого аргумента. apply охватывает массив других аргументов, а при использовании call можно просто разделить другие аргументы запятыми. И что же они выполняют? Оба метода устанавливают контекст для одного определенного вызова функции. При вызове функции без метода call контекст устанавливается по умолчанию (или даже связанный контекст). Пример:

class Salad {
  constructor(type) {
    this.type = type
  }
}
function showType() {
  console.log(`The context's type is ${this.type}`)
}
const fruitSalad = new Salad('fruit')
const greekSalad = new Salad('greek')
showType.call(fruitSalad) // The context's type is fruit
showType.call(greekSalad) // The context's type is greek
showType() // The context's type is undefined

Угадайте, какой контекст у последнего вызова showType()?

..

.

Вы правы, самый внешний контекст — window. Тем не менее type не определен ( undefined), а window.typeне существует

Вот и все. Надеюсь, вы разобрались в том, как использовать this в JavaScript.


Перевод статьи Lukas Gisder-Dubé: How to understand the keyword this and context in JavaScript