JavaScript: co to znaczy?

Ustalenie wartości pola this w języku JavaScript może być trudne. Zobacz, jak to zrobić...

Jake Archibald
Jake Archibald

Język this JavaScriptu jest początkiem wielu żartów. To dlatego, że jest on dość skomplikowany. Deweloperzy wykonują jednak bardziej skomplikowane czynności związane z określoną domeną, aby uniknąć problemów z this. Jeśli nie masz pewności, co to jest this, mamy nadzieję, że te informacje okażą się przydatne. To jest mój przewodnik po this.

Zacznę od najbardziej konkretnej sytuacji i zakończę od najmniej konkretnej sytuacji. Ten artykuł jest jak obszerny if (…) … else if () … else if (…) …, więc możesz od razu przejść do pierwszej sekcji pasującej do tego, który oglądasz.

  1. Jeśli funkcja jest zdefiniowana jako funkcja strzałki
  2. W przeciwnym razie, jeśli funkcja/klasa jest wywoływana za pomocą metody new
  3. W przeciwnym razie, jeśli funkcja ma „powiązaną” wartość this
  4. W przeciwnym razie, jeśli na czas połączenia ustawiono this
  5. W przeciwnym razie, jeśli funkcja zostanie wywołana przez obiekt nadrzędny (parent.func())
  6. W przeciwnym razie, jeśli funkcja lub zakres nadrzędny są w trybie ścisłym
  7. W innym przypadku

Jeśli funkcja jest zdefiniowana jako funkcja strzałki:

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

W tym przypadku wartość this jest zawsze taka sama jak wartość this w zakresie nadrzędnym:

const outerThis = this;

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

Funkcje strzałek są świetne, ponieważ nie można zmienić wewnętrznej wartości this. Jest ona zawsze taka sama jak zewnętrzna wartość this.

Inne przykłady

W przypadku funkcji strzałek wartości this nie można zmienić za pomocą funkcji bind:

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

W przypadku funkcji strzałek wartości this nie można zmienić za pomocą poleceń call ani apply:

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

W przypadku funkcji strzałek wartości this nie można zmienić przez wywołanie tej funkcji jako elementu innego obiektu:

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

W przypadku funkcji strzałek wartości this nie można zmienić przez wywołanie tej funkcji jako konstruktora:

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

Metody instancji „Bound”

Jeśli w przypadku metod instancji chcesz mieć pewność, że this zawsze odwołuje się do instancji klasy, najlepszym sposobem jest użycie funkcji strzałek i pól klas:

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

Ten wzorzec jest bardzo przydatny w przypadku korzystania w komponentach z metod instancji, takich jak odbiorniki zdarzeń (np. komponenty reakcji lub komponenty sieciowe).

Powyżej może wydawać się, że narusza to regułę „this będzie taka sama jak this w zakresie nadrzędnym”, ale jeśli spojrzysz na pola klas jako cukier składniowy do ustawiania rzeczy w konstruktorze:

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

Alternatywne pattry obejmują powiązanie istniejącej funkcji w konstruktorze lub przypisanie funkcji w konstruktorze. Jeśli z jakiegoś powodu nie możesz używać pól klas, rozsądną alternatywą jest przypisanie funkcji w konstruktorze:

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

W przeciwnym razie, jeśli funkcja lub klasa jest wywoływana za pomocą metody new:

new Whatever();

Powyższe wywołanie wywoła Whatever (lub jego funkcję konstruktora, jeśli jest to klasa) z this ustawionym jako wynik Object.create(Whatever.prototype).

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

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

To samo dotyczy starszych konstruktorów:

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

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

Inne przykłady

Po wywołaniu za pomocą funkcji new wartości this nie można zmienić za pomocą funkcji bind:

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

Po wywołaniu funkcji new wartości this nie można zmienić przez wywołanie tej funkcji jako członka innego obiektu:

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

W przeciwnym razie, jeśli funkcja ma „powiązaną” wartość this:

function someFunction() {
  return this;
}

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

Przy każdym wywołaniu funkcji boundFunction jej wartość this jest obiektem przekazywanym do funkcji bind(boundObject).

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

Inne przykłady

Podczas wywoływania funkcji granicznej wartości this nie można zmienić za pomocą call ani 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);

Podczas wywoływania funkcji granicznej nie można zmienić wartości this przez wywołanie tej funkcji jako członka innego obiektu:

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

W innym przypadku, jeśli na czas połączenia ustawiono 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);

Wartość this to obiekt przekazywany do call/apply.

Element this ma inną wartość, np. przez detektory zdarzeń DOM, przez co korzystanie z niego może sprawić, że kod będzie trudny do zrozumienia:

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

W przypadkach takich jak powyżej nie używam this, a zamiast tego:

Tak
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);
});

W przeciwnym razie, jeśli funkcja zostanie wywołana przez obiekt nadrzędny (parent.func()):

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

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

W tym przypadku funkcja jest wywoływana jako element składowy obj, więc this ma wartość obj. Dzieje się to w czasie wywołania, więc link jest uszkodzony, jeśli funkcja zostanie wywołana bez obiektu nadrzędnego lub innego obiektu nadrzędnego:

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 ma wartość fałsz, ponieważ someMethod nie jest wywoływany jako członek grupy obj. Być może udało Ci się natrafić na ten błąd podczas próby wykonania polecenia podobnego do tego:

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

Awaria, ponieważ implementacja querySelector analizuje własną wartość this i oczekuje, że będzie rodzajowym węzłem DOM, co powoduje zerwanie tego połączenia. Aby poprawnie wykonać powyższe czynności:

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

Ciekawostka: nie wszystkie interfejsy API używają this wewnętrznie. Metody konsoli takie jak console.log zostały zmienione, aby uniknąć odwołań this, więc log nie musi być powiązany z console.

W przeciwnym razie, jeśli funkcja lub zakres nadrzędny są w trybie ścisłym:

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

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

W tym przypadku wartość this jest nieokreślona. 'use strict' nie jest potrzebny, jeśli zakres nadrzędny jest w trybie ścisłym (a wszystkie moduły są w trybie ścisłym).

W innym przypadku:

function someFunction() {
  return this;
}

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

W tym przypadku wartość this jest taka sama jak wartość globalThis.

Uff...

Gotowe! To wszystko, co wiem o firmie this. Masz pytania? Coś mi umknęło? Możesz napisać do mnie tweeta.

Dziękujemy Mathias Bynens, Ingvar Stepanyan i Thomas Steiner za recenzję.