bind(), call() i apply() w JavaScripcie: jak zapanować nad this

Dlaczego this zmienia wartość zależnie od sposobu wywołania funkcji i jak ustawiają go call(), apply() i bind(): uruchomione przykłady, przypadki callbacków i klas, porównanie z funkcjami strzałkowymi.

bind(), call() i apply() w JavaScripcie: jak zmieniać this
Szybka odpowiedź

call() i apply() wykonują funkcję natychmiast z wybranym this; call dostaje argumenty pojedynczo, apply w tablicy. bind() nie wykonuje niczego: zwraca nową funkcję, w której this (a czasem też pierwsze argumenty) są ustalone na stałe. Sięgamy po nie wtedy, gdy metoda trafia do callbacka i gubi swój obiekt.

Każda funkcja w JavaScripcie ma trzy metody — call(), apply() i bind() — które służą temu samemu: decydują, czym jest this w chwili wykonania funkcji. Żeby zrozumieć, po co są, trzeba najpierw poznać problem, który rozwiązują: w JavaScripcie this nie zależy od miejsca, w którym funkcja została napisana, tylko od sposobu, w jaki została wywołana.

Problem: this zmienia się zależnie od wywołania

Cztery reguły wystarczą, żeby przewidzieć this w niemal każdej sytuacji:

  1. Wywołanie metody objet.fonction(): this to objet.
  2. Zwykłe wywołanie fonction(): this to undefined w trybie strict (moduły, klasy), a poza nim obiekt globalny window.
  3. Wywołanie z new: this to właśnie konstruowany obiekt.
  4. Wywołanie przez call, apply albo funkcję związaną przez bind: this jest tym, co sam wskazałeś.

Funkcje strzałkowe wymykają się tym regułom: nie mają własnego this i zachowują ten z otaczającego kodu.

W praktyce boli przejście od reguły 1 do reguły 2:

probleme.js
const person = {
  name: 'Sarah',
  hello() {
    return `Bonjour ${this.name}`;
  },
};

person.hello();            // 'Bonjour Sarah'  : wywołanie metody, this = person

const detached = person.hello;
detached();                // TypeError w trybie strict: this jest undefined
                           // ('Bonjour undefined' albo 'Bonjour ' poza trybem strict)

Funkcja się nie zmieniła. Zmienił się tylko sposób jej wywołania, a razem z nim this. Dokładnie to dzieje się, gdy metodę przekazujemy do setTimeout, addEventListener albo map.

call(): wywołanie z wybranym this

fonction.call(thisArg, arg1, arg2, …) wykonuje funkcję natychmiast, z this równym thisArg i argumentami podanymi pojedynczo.

call.js
function presenter(age, ville) {
  return `${this.name}, ${age} ans, ${ville}`;
}

const person = { name: 'Sarah' };

presenter.call(person, 30, 'Lille');           // 'Sarah, 30 ans, Lille'
presenter.call({ name: 'Léa' }, 25, 'Paris');   // 'Léa, 25 ans, Paris'

const detached = person.hello;                  // metoda z poprzedniego przykładu
detached.call(person);                          // 'Bonjour Sarah' : this przywrócony

Klasyczne zastosowanie: użycie metody tablicy na czymś, co tablicą nie jest.

javascript
Array.prototype.slice.call('abc');   // ['a', 'b', 'c']
// Dziś napisalibyśmy raczej:
Array.from('abc');                   // ['a', 'b', 'c']

apply(): to samo, argumenty w tablicy

fonction.apply(thisArg, [arg1, arg2, …]) robi dokładnie to, co call, ale argumenty dostaje w tablicy. Wygodne, gdy i tak przychodzą zgrupowane.

apply.js
presenter.apply(person, [30, 'Lille']);   // 'Sarah, 30 ans, Lille'

const notes = [3, 9, 4];
Math.max.apply(null, notes);   // 9 : Math.max oczekuje osobnych argumentów, nie tablicy

Od ES2015 operator spread sprawia, że apply rzadko bywa potrzebny: Math.max(...notes) i presenter.call(person, ...args) robią to samo i pozostają czytelne. apply spotyka się głównie w starszym kodzie.

bind(): ustawienie this na później

Tam gdzie call i apply wykonują funkcję od razu, bind nie wykonuje niczego. Zwraca nową funkcję, w której this jest ustawiony na stałe. To narzędzie do callbacków: funkcję przygotowujesz teraz, a wywoła ją później ktoś inny.

bind.js
const bound = person.hello.bind(person);

bound();                          // 'Bonjour Sarah', niezależnie od kontekstu wywołania
bound.call({ name: 'X' });        // 'Bonjour Sarah' : bind wygrywa, call nie zmieni już this
bound.name;                       // 'bound hello'

Typowy przypadek: metoda przekazana jako callback

compteur.js
class Compteur {
  constructor() {
    this.n = 0;
  }
  incrementer() {
    this.n += 1;
    return this.n;
  }
}

const compteur = new Compteur();

// Bez bind: metoda jest oderwana od swojego obiektu
const inc = compteur.incrementer;
inc();   // TypeError: Cannot read properties of undefined (reading 'n')

// Z bind: this jest ustawiony na compteur
const incLie = compteur.incrementer.bind(compteur);
incLie();   // 1
incLie();   // 2

Ten sam schemat dotyczy timerów i zdarzeń w przeglądarce:

javascript
setTimeout(compteur.incrementer.bind(compteur), 1000);
bouton.addEventListener('click', compteur.incrementer.bind(compteur));

// Żeby móc później usunąć nasłuchiwacz, zachowaj związaną referencję:
const onClick = compteur.incrementer.bind(compteur);
bouton.addEventListener('click', onClick);
bouton.removeEventListener('click', onClick);

Bez bind funkcja addEventListener wywołałaby incrementer z this równym klikniętemu przyciskowi: this.n byłoby undefined, a licznik nie policzyłby niczego.

bind() do aplikacji częściowej

Argumenty podane do bind po this również zostają ustalone. Zwrócona funkcja czeka na kolejne.

partiel.js
const presenterSarah = presenter.bind(person, 30);
presenterSarah('Lille');   // 'Sarah, 30 ans, Lille' : do podania zostaje tylko ostatni argument

const add = (a, b) => a + b;
const add5 = add.bind(null, 5);   // this nie jest potrzebny: wystarczy null
add5(3);                          // 8

To prosty sposób na wyspecjalizowanie ogólnej funkcji bez pisania funkcji opakowującej.

Funkcje strzałkowe: kiedy bind staje się zbędny

Funkcja strzałkowa przechwytuje this z miejsca, w którym została napisana, i później go ignoruje. Stąd dwa wnioski.

Po pierwsze, call, apply i bind nie zmienią jej this:

javascript
const arrow = () => this;
arrow.call({ a: 1 });   // this modułu, a nie { a: 1 }

Po drugie, i to jest jej codzienne zastosowanie, strzałkowa zadeklarowana w metodzie albo w klasie zachowuje właściwy this bez bind:

fleche.js
class Compteur {
  n = 0;

  // Właściwość strzałkowa: this przechwycony raz na zawsze
  incrementer = () => {
    this.n += 1;
    return this.n;
  };
}

const c = new Compteur();
const inc = c.incrementer;
inc();   // 1 : działa bez bind

setTimeout(() => c.incrementer(), 1000);   // albo funkcja strzałkowa przy wywołaniu

Druga strona medalu: właściwość strzałkowa powstaje na nowo w każdej instancji, podczas gdy zwykła metoda jest współdzielona przez prototyp. Przy kilku obiektach nie ma to znaczenia; przy tysiącach instancji metoda związana raz w konstruktorze (this.incrementer = this.incrementer.bind(this)) oszczędza pamięć.

this podany jako drugi argument

Kilka metod tablicowych przyjmuje wprost wartość this do użycia w callbacku, co pozwala pominąć bind:

javascript
const config = { k: 10 };
[1, 2, 3].map(function (x) { return this.k * x; }, config);   // [10, 20, 30]

forEach, filter, some i every mają ten sam drugi argument. Przy funkcji strzałkowej staje się on bezużyteczny.

Tabela porównawcza

call apply bind
Wykonuje funkcję natychmiast natychmiast nie: zwraca funkcję
Argumenty pojedynczo w tablicy pojedynczo, ustalone dla przyszłych wywołań
Typowe użycie pożyczenie metody rozwinięcie tablicy (zastąpione przez ...) callbacki, zdarzenia, aplikacja częściowa
Wpływ na funkcję strzałkową żaden na this żaden na this żaden na this, ustalane są argumenty

Częste błędy i jak je czytać

  • „Cannot read properties of undefined (reading 'n’)” w metodzie klasy: metoda została oderwana od swojej instancji (reguła 2). Zwiąż ją przez bind albo zadeklaruj jako właściwość strzałkową.
  • Nasłuchiwacz zdarzenia, który nie zmienia stanu: this to element DOM, a nie twój obiekt. Lekarstwo to samo.
  • Wywołanie bind, które zdaje się nic nie robić: funkcja jest strzałkowa. Usuń bind, jest zbędny.
  • removeEventListener, który niczego nie usuwa: każde bind tworzy nową funkcję; przy dodawaniu i przy usuwaniu trzeba podać tę samą związaną referencję.

Dalej: klasy w JavaScripcie wracają do metod i prototypu, a rozdział Funkcje z kursu daje podstawy, jeśli słownictwo z tego artykułu jest jeszcze nowe. W PHP sprawa wygląda inaczej: $this nigdy nie zmienia obiektu.

Częste błędy

Metoda przekazana jako callback bez bind bouton.addEventListener('click', compteur.incrementer) wywołuje funkcję z this równym przyciskowi, a nie compteur. Napisz compteur.incrementer.bind(compteur) albo funkcję strzałkową.
Przekonanie, że bind zmienia this w funkcji strzałkowej Funkcja strzałkowa nie ma własnego this: call, apply i bind go nie zmienią. Liczą się tylko argumenty podane przez bind.
Wywoływanie bind przy każdym renderowaniu bind tworzy nową funkcję przy każdym wywołaniu. W pętli albo przy powtarzanym renderowaniu zwiąż metodę raz w konstruktorze (albo zadeklaruj ją jako właściwość strzałkową).
Mylenie call i apply przy tej samej liście argumentów f.call(obj, [1, 2]) przekazuje jeden argument, czyli tablicę. Do rozwinięcia tablicy służy apply albo f.call(obj, ...tableau).

JavaScript

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.