JavaScript: Что это значит?

Выяснить значение this в JavaScript может быть непросто, вот как это сделать…

JavaScript является this многих шуток, и это потому, что он довольно сложен. Однако я видел, как разработчики делали гораздо более сложные и специфичные для предметной области вещи, чтобы избежать этой this . Если вы не уверены в this , надеюсь, это поможет. Это мое this .

Я начну с самой конкретной ситуации и закончу наименее конкретной. Эта статья похожа на большое if (…) … else if () … else if (…) … , так что вы можете сразу перейти к первому разделу, который соответствует коду, который вы просматриваете.

  1. Если функция определена как стрелочная функция
  2. В противном случае, если функция/класс вызывается с new
  3. В противном случае, если функция имеет «привязку», this значение
  4. В противном случае, если this установлено во время вызова
  5. В противном случае, если функция вызывается через родительский объект ( parent.func() )
  6. В противном случае, если функция или родительская область находятся в строгом режиме
  7. В противном случае

Если функция определена как стрелочная функция:

const arrowFunction = () => {
  console.log(this);
};

В этом случае значение this всегда такое же this и в родительской области:

const outerThis = this;

const arrowFunction = () => {
  // Always logs `true`:
  console.log(this === outerThis);
};

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

Другие примеры

При использовании стрелочных функций значение this не может быть изменено с помощью bind :

// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();

При использовании стрелочных функций значение this нельзя изменить с помощью call или apply :

// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});

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

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();

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

// TypeError: arrowFunction is not a constructor
new arrowFunction();

«Связанные» методы экземпляра

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

class Whatever {
  someMethod = () => {
    // Always the instance of Whatever:
    console.log(this);
  };
}

Этот шаблон действительно полезен при использовании методов экземпляра в качестве прослушивателей событий в компонентах (таких как компоненты React или веб-компоненты).

Может показаться, что вышеизложенное нарушает правило « this будет то же самое, что и this в родительской области», но это начинает иметь смысл, если вы думаете о полях класса как о синтаксическом сахаре для настройки вещей в конструкторе:

class Whatever {
  someMethod = (() => {
    const outerThis = this;
    return () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  })();
}

// …is roughly equivalent to:

class Whatever {
  constructor() {
    const outerThis = this;
    this.someMethod = () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  }
}

Альтернативные шаблоны включают привязку существующей функции в конструкторе или назначение функции в конструкторе. Если по какой-то причине вы не можете использовать поля класса, разумной альтернативой является назначение функций в конструкторе:

class Whatever {
  constructor() {
    this.someMethod = () => {
      // …
    };
  }
}

В противном случае, если функция/класс вызывается с помощью new :

new Whatever();

Вышеупомянутое вызовет Whatever (или его функцию-конструктор, если это класс) с this набором результатов Object.create(Whatever.prototype) .

class MyClass {
  constructor() {
    console.log(
      this.constructor === Object.create(MyClass.prototype).constructor,
    );
  }
}

// Logs `true`:
new MyClass();

То же самое справедливо и для конструкторов старого стиля:

function MyClass() {
  console.log(
    this.constructor === Object.create(MyClass.prototype).constructor,
  );
}

// Logs `true`:
new MyClass();

Другие примеры

При вызове с помощью new значение this не может быть изменено с помощью bind :

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

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

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

В противном случае, если функция имеет «привязку» this значению:

function someFunction() {
  return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

Всякий раз, когда boundFunction , this значение будет объектом, передаваемым в bind ( boundObject ).

// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);

Другие примеры

При вызове связанной функции this значение нельзя изменить с помощью call или apply :

// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);

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

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);

В противном случае, если this установлено во время вызова:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);

Значением this является объект, передаваемый в call / apply .

К сожалению, такие вещи, как прослушиватели событий DOM, устанавливают для this другое значение, и его использование может привести к созданию сложного для понимания кода:

Не
element.addEventListener('click', function (event) {
  // Logs `element`, since the DOM spec sets `this` to
  // the element the handler is attached to.
  console.log(this);
});

Я избегаю использования this в случаях, подобных приведенным выше, и вместо этого:

Делать
element.addEventListener('click', (event) => {
  // Ideally, grab it from a parent scope:
  console.log(element);
  // But if you can't do that, get it from the event object:
  console.log(event.currentTarget);
});

В противном случае, если функция вызывается через родительский объект ( parent.func() ):

const obj = {
  someMethod() {
    return this;
  },
};

// Logs `true`:
console.log(obj.someMethod() === obj);

В этом случае функция вызывается как член obj , поэтому this будет obj . Это происходит во время вызова, поэтому ссылка разрывается, если функция вызывается без родительского объекта или с другим родительским объектом:

const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);

const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);

someMethod() === obj имеет значение false, поскольку someMethod не вызывается как член obj . Возможно, вы столкнулись с этой ошибкой, когда пытались сделать что-то вроде этого:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

Это не работает, потому что реализация querySelector сама просматривает this значение и ожидает, что это будет своего рода узел DOM, и приведенное выше разрывает это соединение. Чтобы правильно достичь вышеуказанного:

const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);

Интересный факт: не все API используют this внутри себя. Консольные методы, такие как console.log были изменены, чтобы избежать this ссылок, поэтому log не нужно привязывать к console .

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

function someFunction() {
  'use strict';
  return this;
}

// Logs `true`:
console.log(someFunction() === undefined);

В данном случае значение this не определено. 'use strict' не требуется в функции, если родительская область находится в строгом режиме (и все модули находятся в строгом режиме).

В противном случае:

function someFunction() {
  return this;
}

// Logs `true`:
console.log(someFunction() === globalThis);

В данном случае значение this такое же, как и globalThis .

Уф!

И все! Это все, что я знаю об this . Есть вопросы? Что-то я пропустил? Не стесняйтесь писать мне в Твиттере .

Спасибо Матиасу Биненсу , Ингвару Степаняну и Томасу Штайнеру за рецензирование.