JavaScript: Qual è il significato di tutto questo?

Capire il valore di this può essere difficile in JavaScript, ecco come fare...

Giacomo Archibald
Jake Archibald

L'this di JavaScript è il frutto di molte battute il motivo è che è piuttosto complicato. Tuttavia, ho notato che gli sviluppatori fanno cose molto più complicate e specifiche del dominio per evitare di avere a che fare con questo this. Se hai dubbi su this, speriamo che queste informazioni ti siano di aiuto. Questa è la mia guida di this.

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

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

Se la funzione è definita come una funzione a 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 a freccia sono molto utili perché il valore interno di this non può essere modificato, ed è sempre uguale al valore this esterno.

Altri esempi

Con le funzioni a 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 a 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 a 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 a 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 "associati"

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

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

Questo pattern è davvero utile quando si utilizzano metodi di istanza come listener di eventi nei componenti (ad esempio componenti di reazione o componenti web).

Quanto riportato sopra potrebbe sembrare che violi la regola "this è uguale a this nell'ambito padre", ma inizia ad avere senso se pensi ai campi di classe come allo zucchero sintattico per impostare cose 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 brevetti 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 = () => {
      // …
    };
  }
}

Altrimenti, se la funzione/classe viene chiamata con new:

new Whatever();

Quanto sopra chiamerà Whatever (o la sua funzione di costruttore se è 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 meno recenti:

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

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

Altri esempi

Quando 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();

Quando 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();

Altrimenti, se la funzione ha un valore this "associato":

function someFunction() {
  return this;
}

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

Ogni volta che boundFunction viene chiamato, 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 chiami una funzione associata, 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 si chiama 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);

Altrimenti, 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 il valore this è impostato su un altro valore da elementi come i listener di eventi DOM e il suo utilizzo può causare 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);
});

Nei casi come sopra, evito di utilizzare this, ma invece:

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

Altrimenti, se la funzione viene chiamata tramite un oggetto padre (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 in fase di chiamata, quindi il collegamento viene interrotto se la funzione viene chiamata senza l'oggetto padre o con un oggetto padre 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 è chiamato come membro di obj. Potresti riscontrare questo recupero quando provi qualcosa del genere:

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

Questo comando interrompe perché l'implementazione di querySelector esamina il proprio valore this e prevede che sia una sorta di nodo DOM. Quanto riportato sopra, interrompe la connessione. Per eseguire correttamente questi passaggi:

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 i riferimenti a this, quindi non è necessario associare log a console.

Altrimenti, se la funzione o l'ambito principale è in modalità con restrizioni:

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à rigida (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.

e il gioco è fatto. Questo è tutto quello che so su this. Domande? Mi è sfuggito qualcosa? Non esitare a inviarmi un tweet.

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