JavaScript: Qual è il significato di tutto questo?

Determinare il valore di this può essere complicato in JavaScript, ecco come fare…

Jake Archibald
Jake Archibald

Il this di JavaScript è oggetto di molte barzellette, perché è piuttosto complicato. Tuttavia, ho notato che gli sviluppatori fanno cose molto più complicate e specifiche per i singoli domini per evitare di avere a che fare con questo this. Se hai dubbi in merito a this, spero che queste informazioni ti siano di aiuto. Questa è la mia guida this.

Inizierò con la situazione più specifica e concluderò con la meno specifica. Questo articolo è un po' come un grande if (…) … else if () … else if (…) …, quindi puoi andare direttamente alla prima sezione che corrisponde al codice che stai esaminando.

  1. Se la funzione è definita come funzione freccia
  2. Altrimenti, se la funzione o la classe viene chiamata con new
  3. In caso contrario, se la funzione ha un valore this "limitato"
  4. In caso contrario, se this è impostato al momento della chiamata
  5. In caso contrario, se la funzione viene chiamata tramite un oggetto principale (parent.func())
  6. In caso contrario, se la funzione o l'ambito principale è in modalità rigorosa
  7. In caso contrario

Se la funzione è definita come funzione freccia:

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

In questo caso, il valore di this è sempre uguale a this nell'ambito principale:

const outerThis = this;

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

Le funzioni freccia sono ottime perché il valore interno di this non può essere modificato, poiché è sempre lo stesso del valore this esterno.

Altri esempi

Con le funzioni freccia, il valore di this non può essere modificato con bind:

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

Con le funzioni freccia, il valore di this non può essere modificato con call o apply:

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

Con le funzioni freccia, il valore di this non può essere modificato chiamando la funzione come membro di un altro oggetto:

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

Con le funzioni freccia, il valore di this non può essere modificato chiamando la funzione come costruttore:

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

Metodi di istanza "bound"

Con i metodi di istanza, se vuoi assicurarti che this faccia sempre riferimento all'istanza di classe, il modo migliore è utilizzare le funzioni freccia e i campi di classe:

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

Questo pattern è molto utile quando si utilizzano metodi di istanza come ascoltatori di eventi nei componenti (ad esempio i componenti React o i componenti web).

Quanto sopra potrebbe sembrare che violi la regola "this sarà uguale a this nell'ambito padre", ma inizia a avere senso se pensi ai campi della classe come a zucchero sintattico per impostare elementi nel costruttore:

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

I pattern alternativi prevedono l'associazione di una funzione esistente nel costruttore o l'assegnazione della funzione nel costruttore. Se per qualche motivo non puoi utilizzare i campi della classe, l'assegnazione di funzioni nel costruttore è un'alternativa ragionevole:

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

In caso contrario, se la funzione o la classe viene chiamata con new:

new Whatever();

Il codice riportato sopra chiamerà Whatever (o la relativa funzione di costruttore se si tratta di una classe) con this impostato sul risultato di Object.create(Whatever.prototype).

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

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

Lo stesso vale per i costruttori di tipo precedente:

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

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

Altri esempi

Se viene chiamato con new, il valore di this non può essere modificato con bind:

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

Se viene chiamato con new, il valore di this non può essere modificato chiamando la funzione come membro di un altro oggetto:

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

In caso contrario, se la funzione ha un valore this "bound":

function someFunction() {
  return this;
}

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

Ogni volta che viene chiamato boundFunction, il suo valore this sarà l'oggetto passato a bind (boundObject).

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

Altri esempi

Quando viene chiamata una funzione vincolata, il valore di this non può essere modificato con call o 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);

Quando chiami una funzione associata, il valore di this non può essere modificato chiamando la funzione come membro di un altro oggetto:

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

In caso contrario, se this è impostato al momento della chiamata:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

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

Il valore di this è l'oggetto passato a call/apply.

Purtroppo this viene impostato su un altro valore da elementi come gli ascoltatori di eventi DOM e il suo utilizzo può generare codice difficile da comprendere:

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

Evito di utilizzare this in casi come quello riportato sopra e, al suo posto:

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

In caso contrario, se la funzione viene chiamata tramite un oggetto principale (parent.func()):

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

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

In questo caso la funzione viene chiamata come membro di obj, quindi this sarà obj. Questo accade al momento della chiamata, quindi il collegamento viene interrotto se la funzione viene chiamata senza l'oggetto principale o con un oggetto principale diverso:

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 è falso perché someMethod non viene chiamato come membro di obj. Potresti aver riscontrato questo problema quando hai provato qualcosa di simile:

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

Il problema si verifica perché l'implementazione di querySelector esamina il proprio valore this e si aspetta che sia un tipo di nodo DOM, mentre il codice riportato sopra interrompe questa connessione. Per eseguire correttamente quanto sopra:

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

Curiosità: non tutte le API utilizzano this internamente. I metodi della console come console.log sono stati modificati per evitare riferimenti a this, quindi log non deve essere associato a console.

In caso contrario, se lo scopo della funzione o del contesto principale è in modalità rigorosa:

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

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

In questo caso, il valore di this non è definito. 'use strict' non è necessario nella funzione se l'ambito padre è in modalità restrittiva (e tutti i moduli sono in modalità con restrizioni).

Altrimenti:

function someFunction() {
  return this;
}

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

In questo caso, il valore di this è uguale a globalThis.

Finalmente.

È tutto. È tutto quello che so su this. Domande? Mi è sfuggito qualcosa? Non esitare a twittarmi.

Grazie a Mathias Bynens, Ingvar Stepanyan e Thomas Steiner per la revisione.