То, что я хотел бы знать, когда начал изучать this

Ключевое слово this является причиной многих недоразумений в JavaScript. Это также достаточно важно, чтобы этого трудно избежать. Например, если вы используете React или Angular, вы, вероятно, будете использовать классы JavaScript, где ключевое слово this почти необходимо.

Но даже в простом старом ванильном JavaScript вы можете столкнуться с this - или его побочными эффектами - чаще, чем вы думаете. Например, синтаксис так называемой «толстой стрелки» в ES6 автоматически принимает привязку this из охватывающей области
. Это полезно при создании новых объектов из прототипов и написании обычных функций JavaScript.

Настоящая статья предназначена для разработчиков, которые, возможно, использовали ключевое слово this, но не уверены, что хорошо разбираются в его поведении. Я надеюсь, что - если вы попробуете эти примеры - вы закончите статью, получив более четкое представление о том, что this делает и почему. Для тех, кто хочет пойти дальше, я включил некоторые ресурсы внизу.

если (это === окно) вернуть «глобальный»;

this может показаться сложным, потому что его значение меняется в зависимости от контекста, в котором он используется. Мы начнем с рассмотрения самого основного контекста.

Хотя this в основном встречается в объектах, функциях и классах, мы также можем называть его глобально, например:

console.log(this);

Использование this без более конкретного контекста эквивалентно вызову глобального объекта: в веб-браузере это объект window.

Если это тот результат, который нам нужен, мы также можем вызвать self, frames или globalThis. Но они всегда относятся к объекту window: другими словами, им не хватает гибкости, которая делает this полезным.

Node.js

Не каждая программа JavaScript работает в браузере. Вы должны знать, что в средах без окон глобальным значением this не будет window объект, потому что его не существует!

Обратите внимание, что при глобальном использовании в Node.js this представляет собой пустой объект:

console.log(this);    // {}
this.foo = 'bar' ;
console.log(this);    // { foo: 'bar' }

Объем против контекста

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

Это потому, что this зависит от контекста, а не от объема. В предыдущем разделе мы говорили о глобальном контексте, а не о глобальной области. Так получилось, что приведенный выше код имел и глобальный контекст, и глобальную область видимости, так в чем же разница?

Что такое объем?

Область видимости контролирует контекст, в котором доступны переменные. Вот классический пример:

function foo() {
  const bar = true; 
  return bar;
};
console.log(foo()); // true
console.log(bar);   // ReferenceError: bar is not defined

Переменная bar доступна только внутри нашей функции foo. Если мы вызовем foo(), мы получим значение bar. Но если мы попытаемся использовать bar вне foo, мы получим ReferenceError. Это называется локальной областью или функциональной областью.

Заманчиво представить, что this работает таким же образом. Но это недоразумение, вероятно, является причиной большинства ошибок новичков!

Что такое контекст?

Чтобы понять контекст, давайте перепишем нашу функцию foo, чтобы установить значение this.bar:

function foo() {
  this.bar = true; 
  return bar;
};
console.log(foo()); // true
console.log(bar);   // true

Новички часто предполагают, что в приведенном выше примере this относится к функции foo. Но когда мы пытаемся получить доступ к bar, мы можем!

Это потому, что выражение this.bar = true фактически записывает глобальную переменную bar. Это потому, что мы не установили конкретный контекст, и поэтому, как и в первом примере этой статьи, this относится к глобальному объекту. Чтобы дать this контекст, вам нужно использовать такие методы, как call, apply или bind.

Определение контекста для «этого»

Теперь мы понимаем, что область действия и контекст разные, но как определить контекст для this?

Начнем с самого простого примера:

function func() { return this };

Если мы вызовем func(), мы получим глобальный объект window, как и раньше. Но есть три метода, которые мы можем использовать для изменения значения this, когда мы его называем: call, apply и bind.

Допустим, мы хотим, чтобы this представляла строку 'Hello World!', мы можем сделать это несколькими способами:

func.call('Hello World');
func.apply('Hello World');
func.bind('Hello World')();

Каждая строка приведенного выше кода вернет строку 'Hello World!'. Как аргумент функции, мы можем использовать любой тип данных с указанными выше методами. Чаще всего вы встретите предметы. Вот пример:

const a = 1;
const obj = { a: 2 };
function whatIsA() { return this.a };

Будет ли whatIsA() возвращать 1 или 2, будет зависеть от того, как мы это называем:

whatIsA();            // 1
whatIsA(obj);         // 1
whatIsA.call(obj);    // 2
whatIsA.apply(obj);   // 2
whatIsA.bind(obj)();  // 2

Обратите внимание, что передача obj в качестве аргумента whatIsA не влияет на результат this.

Так зачем беспокоиться ?!

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

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

function round() {
  return (this.price / 100).toFixed(2);
};
function toCurrency() {
  return '$' + round.call(this);
};
const sale = {
  price: 1299
};
const sale2 = {
  price: 3750
};
console.log(toCurrency.call(sale))    // $12.99
console.log(toCurrency.call(sale2))   // $37.50

В приведенном выше коде мы конвертируем целые числа в доллары в два этапа. Сначала округляем число до двух десятичных знаков. Затем мы добавляем символ доллара.

Конечно, желаемого эффекта можно было добиться и с помощью аргументов:

function round(obj) {
  return (obj.price / 100).toFixed(2);
};
function toCurrency(obj) {
  return '$' + round(obj);
};

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

Напротив, при использовании this мы можем быть уверены в точной форме, которую будет принимать наш контекст: это всегда будет именно то, что мы передаем в наш call или apply метод.

Позвоните и подайте заявку

Разница между call и apply просто семантическая: call() принимает список аргументов, а apply() принимает единственный массив аргументов.

Для демонстрации давайте расширим нашу функцию toCurrency, чтобы она позволяла использовать разные валюты и давала возможность заменять точку на запятую, чтобы мы могли получить такие результаты, как €12,99.

function round() {
  return (this.price / 100).toFixed(2);
};
function toCurrency(code, separator) {
  let symbol;
  let number = round.call(this);
  switch (code) {
    case 'EUR':
      symbol = '€';
      break;
    case 'JPY':
      symbol = '¥';
      break;
    case 'USD':
    default:
      symbol = '$';
      break;
};
if (separator) number = number.toString().replace('.', separator);
  return symbol + number;
};

Если мы хотим отображать цену в евро с запятой в качестве разделителя вместо точки, мы могли бы использовать:

toCurrency.call(sale, 'EUR', ',');        // €12,99

Или мы могли бы использовать:

toCurrency.apply(sale, ['EUR', ',']);     // €12,99

Связывать

До этого момента, когда мы хотели использовать конкретный контекст, нам приходилось указывать его в точке вызова нашей функции. Но что, если мы хотим связать функцию с определенным контекстом, чтобы выбранный нами контекст предполагался каждый раз, когда мы вызываем функцию? Вот тут-то и пригодится bind!

const o = {
  x: 10,
  getX: function () {
    return this.x;
  }
};
let y = o.getX;

Если мы позвоним y, каков будет результат? Без какого-либо конкретного контекста this примет значение x в глобальном контексте. В данном случае его нет, поэтому мы получим undefined.

Но мы можем привязать нашу функцию y к контексту o:

const o = {
  x: 10,
  getX: function () {
    return this.x;
  }
};
let y = o.getX;
y = y.bind(o);

Теперь, когда мы звоним y, мы получаем номер 10. Довольно полезно!

Использование привязки в методах класса

Если вы привыкли к такой среде, как React, которая интенсивно использует классы JavaScript, вы, вероятно, будете использовать для привязки своих методов внутри constructor метода вашего класса:

import React from 'react';
class Counter extends React.Component { 
  constructor(props) { 
    super(props); 
    this.state = { count: 0 }; 
    this.addOne = this.addOne.bind(this);
  };
  addOne(event){
    this.setState({ count: this.state.count + 1 });
  };
  
  render(){
    return (
      <button type="button" onClick={this.addOne}>
        Click Me
      </button>
    );
  };
};

Причина такого длинного выражения this.addOne = this.addOne.bind(this) заключается в том, что мы всегда хотим, чтобы метод addOne выполнялся в контексте текущего класса React. В противном случае наше значение this будет не таким, как мы ожидали. Классы React работают в «строгом режиме», поэтому вместо this по умолчанию window он возвращаетundefined: наш addOne метод попытается использовать undefined.setState, которого, конечно же, не существует!

Использование метода bind означает, что всякий раз, когда мы вызываем addOne, он будет использовать метод setState, который является частью нашего класса.

Неявное связывание с функциями стрелок

Более опытные разработчики React могут предпочесть избегать написания выражения bind с каждым новым методом. Вместо этого компонент Counter, указанный выше, можно было бы переписать следующим образом:

import React from 'react';
class Counter extends React.Component { 
  state = { count: 0 }; 
  addOne = (event) => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render(){
    return (
      <button type="button" onClick={this.addOne}>
        Click Me
      </button>
    );
  };
};

Это работает, потому что функция стрелки автоматически принимает привязку this из охватывающей области - в этом классе Counter.

это в объектах

Наконец, стоит отметить, что когда this используется в методе объекта, его контекст - если не определено иное - будет контекстом самого непосредственного объекта.

function func() { return this };
let obj = {func, a: 10}

Возьмите код выше. Если мы вызовем func(), мы получим глобальный объект. Но если мы позвоним obj.func(), мы получим сам obj.

Загадки

Если вы хотите проверить свое понимание this, вот 3 небольших загадки. Все они должны работать, если вы вставляете их прямо в консоль браузера, поэтому я рекомендую попробовать их самостоятельно, как только вы попробуете ответ.

Головоломка 1: масштаб и контекст

Теперь, когда вы понимаете разницу между масштабом и контекстом, вот вам загадка. На какой номер вернется звонящий doThing?

const a = 1;
function doThing() {
  const a = 2;
  this.doIt = function() {
    return a;
  }
  return doIt();
}

Головоломка 2: неявное связывание с функциями стрелок

Учитывая, что стрелочные функции автоматически принимают привязку this из охватывающей области, как вы думаете, что произойдет, когда мы вызовем obj.func() ниже?

const func = () => { return this };
let obj = {func, a: 10}

Головоломка 3: Вложенные объекты

Исходя из приведенного ниже кода, если мы вызовем outerObj.innerObj.val, что мы получим?

this.val = 10;
const outerObj = {
  val: 50,
  innerObj: {
    val: 100,
    func: function () { return this.val }
  }
};
const func = outerObj.innerObj.func;

Ответ 1

Ответ 2. doIt определен глобально, но из-за того, где он определен (в пределах функциональной области doThing), он имеет доступ к var a = 2!

Поначалу это может показаться запутанным, но как только мы осознаем, что объем и контекст различны, это должно иметь смысл!

Ответ 2

Получаем глобальный объект window!

Это нельзя изменить с помощью call, apply или bind. Если вам нужно изменить привязку, используйте обычную функцию.

Ответ 3

Контекст this - это наш innerObj, поэтому мы получаем val это 100.

Если бы мы хотели получить 50, нам пришлось бы изменить наш контекст на outerObj, вызвав outerObj.innerObj.func.call(outerObj).

И если бы мы хотели получить 10, мы могли бы позвонить outObj.innerObj.func.call(this).

Заключение и дальнейшее чтение

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

Если вам нужны еще несколько примеров this в действии или вы хотите копнуть немного глубже, я рекомендую следующее.

1. Сеть разработчиков Mozilla, this

Как всегда, MDN - чрезвычайно полезный ресурс с множеством примеров this в действии.



2. Тодд Девиз, Ключевые слова this в JavaScript

В сети есть много статей о this, но эта бросилась мне в глаза из-за своих простых и понятных примеров!



3. Кайл Симпсон, Вы не знаете JS - this и прототипы объектов

Фантастический подробный ресурс о this его связи с прототипами объектов.