JavaScript bind(), call() en apply(): this begrijpen en beheersen

Waarom this van waarde verandert afhankelijk van hoe je een functie aanroept, en hoe call(), apply() en bind() dat vastleggen: uitgevoerde voorbeelden, callbacks en klassen, vergelijking met pijlfuncties.

JavaScript bind(), call() en apply(): het verschil
Kort antwoord

call() en apply() voeren een functie meteen uit met een gekozen this, call ontvangt de argumenten één voor één, apply ontvangt ze in een array. bind() voert niets uit: het geeft een nieuwe functie terug waarvan this (en eventueel de eerste argumenten) definitief vastligt. Je gebruikt het wanneer een methode als callback wordt doorgegeven en haar object verliest.

Elke JavaScript-functie beschikt over drie methoden, call(), apply() en bind(), die hetzelfde doel dienen: bepalen wat this waard is op het moment dat de functie wordt uitgevoerd. Om te begrijpen waarvoor ze dienen, moet je eerst het probleem begrijpen dat ze oplossen: in JavaScript hangt this niet af van de plek waar een functie is geschreven, maar van de manier waarop ze wordt aangeroepen.

Het probleem: this verandert met de aanroep

Vier regels volstaan om this in bijna alle gevallen te voorspellen:

  1. Methodeaanroep objet.fonction(): this is objet.
  2. Eenvoudige aanroep fonction(): this is undefined in strict mode (modules, klassen), en het globale object window in non-strict mode.
  3. Aanroep met new: this is het object dat wordt aangemaakt.
  4. Aanroep via call, apply of een functie gebonden met bind: this is wat je hebt bepaald.

Pijlfuncties ontsnappen aan deze regels: ze hebben geen eigen this en behouden die van de omliggende code.

Het geval dat in de praktijk pijn doet, is de overgang van regel 1 naar regel 2:

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

person.hello();            // 'Bonjour Sarah'  : methodeaanroep, this = person

const detached = person.hello;
detached();                // TypeError in strict mode: this is undefined
                           // ('Bonjour undefined' of 'Bonjour ' in non-strict mode)

De functie is niet veranderd. Alleen de manier waarop ze wordt aangeroepen is veranderd, en this daarmee. Dat is precies wat er gebeurt als je een methode doorgeeft aan setTimeout, aan addEventListener of aan map.

call(): aanroepen met een gekozen this

fonction.call(thisArg, arg1, arg2, …) voert de functie meteen uit, met this gelijk aan thisArg en de argumenten één voor één doorgegeven.

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;                  // de methode uit het vorige voorbeeld
detached.call(person);                          // 'Bonjour Sarah': this is hersteld

Een klassiek gebruik: een array-methode hergebruiken op iets dat geen array is.

javascript
Array.prototype.slice.call('abc');   // ['a', 'b', 'c']
// Tegenwoordig schrijf je het liever zo:
Array.from('abc');                   // ['a', 'b', 'c']

apply(): hetzelfde, maar met argumenten in een array

fonction.apply(thisArg, [arg1, arg2, …]) doet precies hetzelfde als call, maar ontvangt de argumenten als array. Handig wanneer ze al gegroepeerd binnenkomen.

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

const notes = [3, 9, 4];
Math.max.apply(null, notes);   // 9: Math.max verwacht losse argumenten, geen array

Sinds ES2015 maakt de spread-operator apply zelden nog noodzakelijk: Math.max(...notes) en presenter.call(person, ...args) doen hetzelfde werk en blijven leesbaar. Je komt apply vooral tegen in code die van vóór die tijd dateert.

bind(): this vastleggen voor later

Waar call en apply meteen worden uitgevoerd, voert bind niets uit. Het geeft een nieuwe functie terug waarvan this definitief vastligt. Het is het gereedschap voor callbacks: je bereidt de functie nu voor, iemand anders roept ze later aan.

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

bound();                          // 'Bonjour Sarah', ongeacht de aanroepcontext
bound.call({ name: 'X' });        // 'Bonjour Sarah': bind wint, call kan this niet meer veranderen
bound.name;                       // 'bound hello'

Het typische geval: een methode doorgegeven als callback

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

const compteur = new Compteur();

// Zonder bind: de methode is losgekoppeld van haar object
const inc = compteur.incrementer;
inc();   // TypeError: Cannot read properties of undefined (reading 'n')

// Met bind: this ligt vast op compteur
const incLie = compteur.incrementer.bind(compteur);
incLie();   // 1
incLie();   // 2

Hetzelfde patroon geldt voor timers en browserevents:

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

// Om de listener later te kunnen verwijderen, bewaar je de gebonden referentie:
const onClick = compteur.incrementer.bind(compteur);
bouton.addEventListener('click', onClick);
bouton.removeEventListener('click', onClick);

Zonder bind zou addEventListener incrementer aanroepen met this gelijk aan de aangeklikte knop: this.n zou undefined zijn en de teller zou niets tellen.

bind() voor partiële toepassing

De argumenten die na this aan bind worden meegegeven, liggen ook vast. De teruggegeven functie wacht op de resterende argumenten.

partiel.js
const presenterSarah = presenter.bind(person, 30);
presenterSarah('Lille');   // 'Sarah, 30 ans, Lille': alleen het laatste argument moet nog worden meegegeven

const add = (a, b) => a + b;
const add5 = add.bind(null, 5);   // this wordt niet gebruikt: null volstaat
add5(3);                          // 8

Het is een eenvoudige manier om een generieke functie te specialiseren zonder een wrapper-functie te schrijven.

Pijlfuncties: wanneer bind overbodig wordt

Een pijlfunctie legt de this vast van de plek waar ze is geschreven, en negeert die daarna. Twee gevolgen.

Ten eerste kunnen call, apply en bind haar this niet veranderen:

javascript
const arrow = () => this;
arrow.call({ a: 1 });   // this van de module, niet { a: 1 }

Ten tweede, en dat is het gangbare gebruik, behoudt een pijlfunctie die binnen een methode of een klasse is gedeclareerd de juiste this zonder bind:

fleche.js
class Compteur {
  n = 0;

  // Pijlfunctie-eigenschap: this wordt eens en voor altijd vastgelegd
  incrementer = () => {
    this.n += 1;
    return this.n;
  };
}

const c = new Compteur();
const inc = c.incrementer;
inc();   // 1: werkt zonder bind

setTimeout(() => c.incrementer(), 1000);   // of een pijlfunctie bij de aanroep

Keerzijde: een pijlfunctie-eigenschap wordt voor elke instantie opnieuw aangemaakt, terwijl een gewone methode wordt gedeeld via het prototype. Voor een paar objecten maakt dat niets uit, voor duizenden instanties bespaart de methode die één keer in de constructor wordt gebonden (this.incrementer = this.incrementer.bind(this)) geheugen.

this als tweede argument

Verschillende array-methoden accepteren rechtstreeks de waarde van this die in de callback moet worden gebruikt, waardoor een bind overbodig wordt:

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

forEach, filter, some en every hebben datzelfde tweede argument. Met een pijlfunctie wordt het overbodig.

Vergelijkingstabel

call apply bind
Voert de functie uit meteen meteen nee: geeft een functie terug
Argumenten één voor één in een array één voor één, vastgelegd voor latere aanroepen
Typisch gebruik een methode lenen een array uitspreiden (vervangen door ...) callbacks, events, partiële toepassing
Effect op een pijlfunctie geen op this geen op this geen op this, de argumenten liggen vast

Veelvoorkomende fouten en hoe je ze leest

  • “Cannot read properties of undefined (reading ‘n’)” in een klassemethode: de methode is losgekoppeld van haar instantie (regel 2). Bind haar met bind of declareer haar als pijlfunctie-eigenschap.
  • Een event listener die de status niet aanpast: this is het DOM-element, niet je object. Zelfde remedie.
  • Een bind die niets lijkt te doen: de functie is een pijlfunctie. Verwijder de bind, hij is overbodig.
  • removeEventListener die niets verwijdert: elke bind maakt een nieuwe functie aan, je moet dezelfde gebonden referentie meegeven bij het toevoegen en bij het verwijderen.

Voor het vervolg komen klassen in JavaScript terug op methoden en het prototype, en behandelt het hoofdstuk Functies van de tutorial de basis als het vocabulaire van dit artikel nog nieuw is. In PHP ligt de vraag anders: $this verandert nooit van object.

Veelgemaakte fouten

Een methode als callback doorgeven zonder bind bouton.addEventListener('click', compteur.incrementer) roept de functie aan met this gelijk aan de knop, niet aan compteur. Schrijf compteur.incrementer.bind(compteur) of gebruik een pijlfunctie.
Denken dat bind op een pijlfunctie this verandert Een pijlfunctie heeft geen eigen this: call, apply en bind veranderen er niets aan. Alleen de argumenten die via bind worden meegegeven, tellen mee.
bind bij elke render aanroepen bind maakt bij elke aanroep een nieuwe functie aan. Bind in een loop of een herhaalde render één keer in de constructor (of declareer de methode als pijlfunctie-eigenschap).
call en apply door elkaar halen met dezelfde argumentenlijst f.call(obj, [1, 2]) geeft één enkel argument door, de array. Om een array uit te spreiden gebruik je apply, of f.call(obj, ...tableau).

JavaScript

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.