JavaScript Factory funziona con ES6 +

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Nota: fa parte della serie "Composing Software" (ora un libro!) Sull'apprendimento da zero della programmazione funzionale e delle tecniche software compositive in JavaScript ES6 +. Rimanete sintonizzati. C'è molto di più a venire!
Acquista il libro | Indice |

Una funzione factory è qualsiasi funzione che non è una classe o un costruttore che restituisce un oggetto (presumibilmente nuovo). In JavaScript, qualsiasi funzione può restituire un oggetto. Quando lo fa senza la nuova parola chiave, è una funzione di fabbrica.

Le funzioni di fabbrica sono sempre state attraenti in JavaScript perché offrono la possibilità di produrre facilmente istanze di oggetti senza immergersi nelle complessità delle classi e della nuova parola chiave.

JavaScript fornisce una sintassi letterale dell'oggetto molto utile. Sembra qualcosa del genere:

const user = {
  userName: 'echo',
  avatar: 'echo.png'
};

Come JSON (che si basa sulla notazione letterale dell'oggetto JavaScript), il lato sinistro di: è il nome della proprietà e il lato destro è il valore. Puoi accedere agli oggetti di scena con la notazione a punti:

console.log (user.userName); // "eco"

È possibile accedere ai nomi delle proprietà calcolate utilizzando la notazione parentesi quadra:

const key = 'avatar';
console.log (utente [chiave]); // "echo.png"

Se si hanno variabili nell'ambito con lo stesso nome dei nomi delle proprietà previste, è possibile omettere i due punti e il valore nella creazione letterale dell'oggetto:

const userName = 'echo';
const avatar = 'echo.png';
const user = {
  nome utente,
  avatar
};
console.log (utente);
// {"avatar": "echo.png", "userName": "echo"}

I letterali di oggetti supportano la sintassi del metodo conciso. Possiamo aggiungere un metodo .setUserName ():

const userName = 'echo';
const avatar = 'echo.png';
const user = {
  nome utente,
  avatar,
  setUserName (userName) {
    this.userName = userName;
    restituire questo;
  }
};
console.log (user.setUserName ( 'foo') userName.); // "Foo"

In metodi concisi, questo si riferisce all'oggetto su cui viene chiamato il metodo. Per chiamare un metodo su un oggetto, accedi semplicemente al metodo usando la notazione a punti dell'oggetto e invocalo utilizzando le parentesi, ad esempio game.play () applicherebbe .play () all'oggetto gioco. Per applicare un metodo usando la notazione punto, tale metodo deve essere una proprietà dell'oggetto in questione. Puoi anche applicare un metodo a qualsiasi altro oggetto arbitrario utilizzando i metodi prototipo di funzione, .call (), .apply () o .bind ().

In questo caso, user.setUserName ('Foo') applica .setUserName () all'utente, quindi questo === utente. Nel metodo .setUserName (), cambiamo la proprietà .userName sull'oggetto utente tramite questa associazione e restituiamo la stessa istanza di oggetto per il concatenamento del metodo.

Letterali per uno, fabbriche per molti

Se devi creare molti oggetti, ti consigliamo di unire la potenza dei letterali degli oggetti e le funzioni di fabbrica.

Con una funzione di fabbrica, puoi creare tutti gli oggetti utente che desideri. Se stai costruendo un'app di chat, ad esempio, puoi avere un oggetto utente che rappresenta l'utente corrente e anche molti altri oggetti utente che rappresentano tutti gli altri utenti che sono attualmente connessi e in chat, quindi puoi visualizzare i loro nomi e anche gli avatar.

Trasformiamo il nostro oggetto utente in una factory createUser ():

const createUser = ({userName, avatar}) => ({
  nome utente,
  avatar,
  setUserName (userName) {
    this.userName = userName;
    restituire questo;
  }
});
console.log (createUser ({userName: 'echo', avatar: 'echo.png'}));
/ *
{
  "avatar": "echo.png",
  "userName": "echo",
  "setUserName": [Funzione setUserName]
}
* /

Restituzione di oggetti

Le funzioni freccia (=>) hanno una funzione di ritorno implicita: se il corpo della funzione è costituito da una singola espressione, puoi omettere la parola chiave return: () => 'pippo' è una funzione che non accetta parametri e restituisce la stringa " foo".

Prestare attenzione quando si restituiscono valori letterali oggetto. Per impostazione predefinita, JavaScript presuppone che si desideri creare un corpo di funzione quando si utilizzano le parentesi graffe, ad esempio {broken: true}. Se vuoi usare un ritorno implicito per un oggetto letterale, dovrai chiarire le cose racchiudendo l'oggetto letterale tra parentesi:

const noop = () => {foo: 'bar'};
console.log (noop ()); // non definito
const createFoo = () => ({foo: 'bar'});
console.log (createFoo ()); // {foo: "bar"}

Nel primo esempio, foo: viene interpretato come un'etichetta e la barra viene interpretata come un'espressione che non viene assegnata o restituita. La funzione restituisce non definita.

Nell'esempio createFoo (), le parentesi obbligano le parentesi graffe a essere interpretate come un'espressione da valutare, piuttosto che un blocco del corpo di funzione.

destrutturazione

Prestare particolare attenzione alla firma della funzione:

const createUser = ({userName, avatar}) => ({

In questa riga, le parentesi graffe ({,}) rappresentano la destrutturazione dell'oggetto. Questa funzione accetta un argomento (un oggetto), ma distrugge due parametri formali da quel singolo argomento, userName e avatar. Tali parametri possono quindi essere utilizzati come variabili nell'ambito del corpo della funzione. È inoltre possibile destrutturare le matrici:

const swap = ([first, second]) => [second, first];
console.log (swap ([1, 2])); // [2, 1]

E puoi usare il resto e diffondere la sintassi (... varName) per raccogliere il resto dei valori dall'array (o un elenco di argomenti), e quindi ripartire quegli elementi dell'array in singoli elementi:

const rotate = ([first, ... rest]) => [... rest, first];
console.log (ruotare ([1, 2, 3])); // [2, 3, 1]

Chiavi di proprietà calcolate

In precedenza abbiamo usato la notazione di accesso alla proprietà calcolata con parentesi quadre per determinare in modo dinamico a quale proprietà dell'oggetto accedere:

const key = 'avatar';
console.log (utente [chiave]); // "echo.png"

Possiamo anche calcolare il valore delle chiavi da assegnare a:

const arrToObj = ([chiave, valore]) => ({[chiave]: valore});
console.log (arrToObj (['foo', 'bar'])); // {"foo": "bar"}

In questo esempio, arrToObj prende un array costituito da una coppia chiave / valore (aka una tupla) e lo converte in un oggetto. Dato che non conosciamo il nome della chiave, dobbiamo calcolare il nome della proprietà per impostare la coppia chiave / valore sull'oggetto. Per questo, prendiamo in prestito l'idea della notazione tra parentesi quadre dagli accessor delle proprietà calcolate e riutilizziamo nel contesto della costruzione di un oggetto letterale:

{[chiave]: valore}

Al termine dell'interpolazione, finiamo con l'oggetto finale:

{"foo": "bar"}

Parametri predefiniti

Le funzioni in JavaScript supportano i valori dei parametri predefiniti, che presentano numerosi vantaggi:

  1. Gli utenti sono in grado di omettere i parametri con valori predefiniti adeguati.
  2. La funzione è più autocompattante perché i valori predefiniti forniscono esempi di input previsto.
  3. Gli IDE e gli strumenti di analisi statica possono utilizzare i valori predefiniti per inferire il tipo previsto per il parametro. Ad esempio, un valore predefinito 1 implica che il parametro può accettare un membro del tipo Numero.

Utilizzando i parametri predefiniti, possiamo documentare l'interfaccia prevista per la nostra fabbrica createUser e inserire automaticamente i dettagli "Anonimi" se le informazioni dell'utente non vengono fornite:

const createUser = ({
  userName = 'Anonimo',
  avatar = 'anon.png'
} = {}) => ({
  nome utente,
  avatar
});
console.log (
  // {userName: "echo", avatar: 'anon.png'}
  createUser ({userName: 'echo'}),
  // {userName: "Anonimo", avatar: 'anon.png'}
  creare un utente()
);

L'ultima parte della firma della funzione sembra probabilmente un po 'divertente:

} = {}) => ({

L'ultimo bit = {} poco prima della chiusura della firma del parametro significa che se non viene passato nulla per questo parametro, useremo un oggetto vuoto come predefinito. Quando provi a distruggere i valori dall'oggetto vuoto, i valori predefiniti per le proprietà verranno utilizzati automaticamente, poiché è quello che fanno i valori predefiniti: sostituisci non definito con un valore predefinito.

Senza il valore predefinito = {}, createUser () senza argomenti genererebbe un errore perché non puoi provare ad accedere alle proprietà da non definito.

Tipo di inferenza

JavaScript non ha annotazioni di tipo nativo al momento della stesura di questo documento, ma diversi formati concorrenti sono sorti nel corso degli anni per colmare le lacune, tra cui JSDoc (in calo a causa dell'emergere di opzioni migliori), Flow di Facebook e TypeScript di Microsoft. Uso rtype per la documentazione - una notazione che trovo molto più leggibile di TypeScript per la programmazione funzionale.

Al momento della stesura di questo documento, non esiste un chiaro vincitore per le annotazioni dei tipi. Nessuna delle alternative è stata benedetta dalla specifica JavaScript e sembrano esserci chiare carenze in tutte.

Inferenza del tipo è il processo di inferimento dei tipi in base al contesto in cui vengono utilizzati. In JavaScript, è un'ottima alternativa al tipo di annotazioni.

Se fornisci indizi sufficienti per l'inferenza nelle firme delle funzioni JavaScript standard, otterrai la maggior parte dei vantaggi delle annotazioni di tipo senza costi o rischi.

Anche se decidi di utilizzare uno strumento come TypeScript o Flow, dovresti fare tutto il possibile con l'inferenza del tipo e salvare le annotazioni del tipo per situazioni in cui l'inferenza del tipo non è all'altezza. Ad esempio, non esiste un modo nativo in JavaScript per specificare un'interfaccia condivisa. È sia semplice che utile con TypeScript o rtype.

Tern.js è uno strumento di inferenza di tipo popolare per JavaScript che ha plugin per molti editor di codice ed IDE.

Il codice Visual Studio di Microsoft non ha bisogno di Tern perché porta le funzionalità di inferenza del tipo di TypeScript al normale codice JavaScript.

Quando specifichi i parametri predefiniti per le funzioni in JavaScript, strumenti in grado di dedurre il tipo come Tern.js, TypeScript e Flow possono fornire suggerimenti IDE per aiutarti a utilizzare l'API con cui stai lavorando correttamente.

Senza i valori predefiniti, gli IDE (e spesso gli umani) non hanno abbastanza suggerimenti per capire il tipo di parametro previsto.

Senza impostazioni predefinite, il tipo è sconosciuto per `userName`.

Con le impostazioni predefinite, gli IDE (e spesso gli umani) possono inferire i tipi dagli esempi.

Con le impostazioni predefinite, l'IDE può suggerire che `userName` si aspetta una stringa.

Non ha sempre senso limitare un parametro a un tipo fisso (ciò renderebbe difficili le funzioni generiche e le funzioni di ordine superiore), ma quando ha senso, i parametri predefiniti sono spesso il modo migliore per farlo, anche se riutilizzando TypeScript o Flow.

Funzioni di fabbrica per la composizione di Mixin

Le fabbriche sono fantastiche per far girare gli oggetti usando una bella API di chiamata. Di solito, sono tutto ciò di cui hai bisogno, ma di tanto in tanto ti ritroverai a costruire caratteristiche simili in diversi tipi di oggetti e vorrai astrarre quelle caratteristiche in mixin funzionali in modo da poterle riutilizzare più facilmente.

È qui che brillano i mixin funzionali. Costruiamo un mixin withConstructor per aggiungere la proprietà .constructor a tutte le istanze di oggetto.

con-constructor.js:

const withConstructor = constructor => o => ({
  // crea il delegato [[Prototype]]
  __proto__: {
    // aggiungi l'elica del costruttore al nuovo [[Prototype]]
    costruttore
  },
  // mescola tutti gli oggetti di scena nel nuovo oggetto
  ... o
});

Ora puoi importarlo e usarlo con altri mixin:

importare conConstructor da `./with-constructor ';
const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
// o `import pipe da 'lodash / fp / flow';`
// Imposta alcuni mixin funzionali
const withFlying = o => {
  let isFlying = false;
  ritorno {
    ... o,
    volare () {
      isFlying = true;
      restituire questo;
    },
    terra () {
      isFlying = false;
      restituire questo;
    },
    isFlying: () => isFlying
  }
};
const withBattery = ({capacity}) => o => {
  let percentCharged = 100;
  ritorno {
    ... o,
    disegnare (percentuale) {
      const rimanente = percentCharged - percent;
      percentCharged = rimanente> 0? rimanente: 0;
      restituire questo;
    },
    getCharge: () => percentCharged,
    getCapacity: () => capacità
  };
};
const createDrone = ({capacity = '3000mAh'}) => pipe (
  withFlying,
  withBattery ({capacity}),
  withConstructor (createDrone)
) ({});
const myDrone = createDrone ({capacità: '5500mAh'});
console.log ( `
  può volare: $ {myDrone.fly (). isFlying () === true}
  può atterrare: $ {myDrone.land (). isFlying () === false}
  capacità della batteria: $ {myDrone.getCapacity ()}
  stato della batteria: $ {myDrone.draw (50) .getCharge ()}%
  batteria scarica: $ {myDrone.draw (75) .getCharge ()}% rimanente
`);
console.log ( `
  costruttore collegato: $ {myDrone.constructor === createDrone}
`);

Come puoi vedere, il mixin riutilizzabile conConstructor () viene semplicemente rilasciato nella pipeline con altri mixin. withBattery () potrebbe essere utilizzato con altri tipi di oggetti, come robot, skateboard elettrici o caricabatterie per dispositivi portatili. withFlying () potrebbe essere usato per modellare auto volanti, razzi o mongolfiere.

La composizione è più un modo di pensare che una particolare tecnica nel codice. Puoi realizzarlo in molti modi. La composizione delle funzioni è solo il modo più semplice per crearla da zero e le funzioni di fabbrica sono un modo semplice per avvolgere un'API amichevole attorno ai dettagli di implementazione.

Conclusione

ES6 fornisce una sintassi conveniente per gestire la creazione di oggetti e le funzioni di fabbrica. Il più delle volte, questo è tutto ciò di cui hai bisogno, ma poiché si tratta di JavaScript, c'è un altro approccio che lo fa sembrare più Java: la parola chiave della classe.

In JavaScript, le classi sono più dettagliate e restrittive delle fabbriche, e un po 'un campo minato quando si tratta di refactoring, ma sono state anche abbracciate dai principali framework front-end come React e Angular, e ci sono un paio di rari usi -casse che rendono utile la complessità.

“A volte, l'implementazione elegante è solo una funzione. Non è un metodo Non una lezione. Non un quadro. Solo una funzione. ”~ John Carmack

Inizia con l'implementazione più semplice e passa a implementazioni più complesse solo come richiesto.

Successivo: Perché la composizione è più dura con le classi>

Prossimi passi

Vuoi saperne di più sulla composizione degli oggetti con JavaScript?

Scopri JavaScript con Eric Elliott. Se non sei un membro, ti stai perdendo!

Eric Elliott è un esperto di sistemi distribuiti e autore dei libri "Composing Software" e "Programmazione di applicazioni JavaScript". Come co-fondatore di DevAnywhere.io, insegna agli sviluppatori le competenze di cui hanno bisogno per lavorare in remoto e abbracciare l'equilibrio tra lavoro e vita privata. Crea e fornisce consulenza a team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC e i migliori artisti discografici tra cui Usher, Frank Ocean, Metallica e molti altri.

Gode ​​di uno stile di vita remoto con la donna più bella del mondo.