< Home
Stampa

Funzioni e componenti

Sommario

Introduzione

Le funzioni possono essere definite in due modi, ciascuno con due varianti:

Funzione standard – variante classica:

function somma(x, y) {
  return x+y;
} 

Funzione standard – variante anonima

let f = function(x, y) {
  return x+y;
}

Funzione freccia (detta anche lambda)

const f = (x,y) => {
  return x+y;
}

Funzione freccia compatta, notare che vengono eliminate parentesi e istruzione return, questa notazione si può usare per funzioni di una riga di codice che ritornano un valore.

const f = x,y => x+y

Le tipologie di funzione sono praticamente identiche (in realtà esistono delle differenze, ma in questo contesto sono irrilevanti).

Le funzioni sono oggetti (in gergo tecnico sono “first class objects”) e vanno trattate come qualsiasi altro tipo di dato.

  • è possibile salvare una funzione in una variabile;
  • è possibile inserire una funzione in un array o un dizionario;
  • si possono realizzare funzioni che restituiscono funzioni;
  • si possono passare funzioni come parametro di altre funzioni.

Unica cosa non possibile, non possono essere trasformate in stringa, quindi sono escluse dalla loro trasformazione in CSV o JSON.

Funzioni autoeseguenti

E’ possibile scrivere funzioni anonime che si auto eseguono nello script principale. Il vantaggio delle funzioni autoeseguenti è che evitano di utilizzare variabili globali nello script, ma dipendono solo dai parametri della funzione. Ad esempio poniamo di avere questo script di pagina:

const data = [1,2,3,4,5];

data.forEach((element) => { 
  console.log(element);
};

Questo codice prevede una variabile data che sarà visibile a tutte le funzioni presenti nella web application, col risultato che potranno globalmente vederla e modificarla, anche per errore.

Molto meglio scrivere una funzione autoeseguente anonima, che consiste nel racchiudere il codice dello script una funzione anonima tra parentesi tonde e subito eseguirla passando i parametri tra parentesi tonde. Vediamo un esempio:

((data) => {
  data.forEach((element) => { 
  console.log(element);
};
})([1,2,3,4,5]);

In questo caso non c’è modo di vedere il contenuto di data dall’esterno di questa funzione.

Funzioni come parametro

Le funzioni possono essere passate come parametri ad un’altra funzione. Ad esempio:

const sum = a,b => a+b;
const mult = a,b => a*b;

const myFunction = (value1, value2, operation) => {
  const result = operation(value1, value2);
  return result;
}

let mySum = myFunction(3,5,sum);
let myMult = myFunction(6,5,mult);
  

Quando occorre scrivere diverse funzioni molto simili tra loro, che differiscono solo per una parte del loro codice, risulta più conveniente creare una funzione contenitore, che contiene la parte comune, e poi eseguire la parte che diverge in una funzione specifica, che appunto viene ricevuta come parametro.

Questo principio di design viene usato anche in molte funzioni di libreria, come sort, map, filter, reduce ecc.

Come vedremo lo stesso meccanismo viene implementato per gestire il “cosa fare dopo” che una funzione ha svolto la sua attività. Questo meccanismo viene chiamato callback: una funzione riceve dei parametri ed una funzione callback. La funzione svolge l’attività per cui è progettata, ed al termine esegue la funzione ricevuta come parametro, detta appunto callback.

Closure

Le closure sono funzioni che generano oggetti (o come visto sopra funzioni) e che mantengono nella memoria delle variabili private utilizzabili dall’oggetto restituito, ma non visibili nè modificabili dall’esterno.

const createAdder = () => {
  let sum = 0;
  return {
     sum: (value) => sum += value,
     show: () => console.log(sum)
  }
}


const adder = createAdder();
adder(3); // aggiunge 10
adder(5); // aggiunge 5
adder(4); // aggiunge 4
adder.show(); // stampa la somma 19

La funzione che abbiamo creato crea delle variabili che restano in memoria ma che continuano a persistere anche dopo che la funzione createAdder ha terminato. Queste variaili sono però inaccessibili dall’esterno e l’unico modo per accedervi è usare le funzioni dell’Tuttavia sum è invisibile all’esterno e quindi non è possibile modificarne il valore direttamente, ma solo agire tramite la funzione adder.

Le closure sono un meccanismo efficace per memorizzare in modo protetto variabili senza che siano accessibili dall’esterno. Con questo sistema si ottiene l’incapsulazione, cioè la creazione di oggetti Javascript che possono accedere ai propri dati in modo esclusivo.

Il loro utilizzo è fondamentale per creare applicazioni Javascript basate sulla programmazione ad oggetti, dove ogni oggetto accede solo ai propri dati, e non a cariabili globali o di altri oggetti. Questo consente di evitare banali errori di programmazione, ma soprattutto consente di riutilizzare gli stessi oggeti in applicazioni differenti.

Currying

Le funzioni possono essere restituite come risultato della funzione. Qui un esempio:

const createSearch = (list) => {
  const filtered = list.filter(element => element !== null || element !== undefined);
  return (searchString) => {
    return filtered.first(element => element.find(searchString) !== -1);
  }
}

const search = createSearch(["Mario", null, "Maria", "Sergio");
console.log(search("Mario")); 

La funzione createSearch prima di tutto filtra la lista da elementi nulli o indefiniti. Poi restituirà una funzione di ricerca che cercherà nella lista filtrata.

Questa operazione viene chiamata “currying“. Il currying può essere usato come nell’esempio precedente per svolgere delle operazioni preliminari su un set di dati, che possono essere poi utilizzate per utilizzi successivi della funzione restituita, senza doverli rifare ogni volta. La funzione createSearch svolge una esecuzione parziale, e poi mette a disposizione la funzione definitiva.

E’ da osservare che la variabile filtered non viene cancellata quando createSearch si conclude. Infatti la porzione di stack di memoria rimane attiva perché esiste ancora un oggetto (la funzione search) che continua ad usarlo. Questo tipo di comportamento si chiama closure e lo vedremo meglio sotto.

Componenti

Partendo dal modello di funzionamento della pagina web (evento, azione, reazione) e la programmazione con le closure possiamo progettare applicazioni web che si basano sul concetto di componente.

Un componente è una porzione della pagina web indipendente dalle altre che viene gestita interamente da un solo oggetto Javascript, appunto il componente.

Il componente è un elemento autoconclusivo della applicazione Web, collegato ad un elemento della pagina, che gestisce autonomamente, tramite un oggetto, l’interazione con l’utente e la gestione delle logiche applicative ad esso collegate. Ad esempio un componente potrebbe essere una tabella, una form, un elemento contenente informazioni dinamiche, un elenco. Inoltre i componenti possono essere anche contenitori di altri componenti, come ad esempio tabelle complesse, dove ogni riga è essa stessa un componente. Il risultato a cui andremo ad arrivare è strutturare la pagina come nella seguente figura:

Le architetture a componenti sono alla base delle applicazioni Javascript moderne, e sono utilizzati da tutti i framework Javascript più diffusi, come React, Vue ed Angular.

In questa lezione vedremo come è possibile utilizzare i componenti anche con Javascript nativo, senza usare librerie esterne. Per funzionare useremo le Closure.

Un componente tabella

Un componente deve gestire l’intero ciclo di vita applicativo della porzione di pagina di cui si occupa, quindi sia la fase di render iniziale, sia la gestione di evento, azione e reazione. In questo esempio scriviamo una semplice tabella sotto forma di componente. Questa tabella avrà le seguenti proprietà private: parentElement (che contiene il riferimento all’elemento del DOM in cui essere inserita) e data (che contiene i dati della tabella, espressi come array bidimensionale, dove la prima riga contiene le intestazioni).

const createTable = (parentElement) => {
  let data;
  return {
    build: (dataInput) => {
      data = dataInput;
    },
    render: () => {
      let htmlTable = "<table>";
      htmlTable += data.map((row) => 
        "<tr>" + row.map((col) => 
          "<td>" + col + "</td>"
        ).join("")
      ).join("") + "</tr>";
      htmlTable += "</table";
      parentElement.innerHTML = htmlTable;
    }
  }
}

const table1 = createTable(document.querySelector("#table1"));
table1.build([["Cognome", "Voto"], ["Pogba", "6"], ["Vlahovic", 8], ["Thuram", 6.5]]);
table1.render();

const table2 = createTable(document.querySelector("#table2"));
table2.build([["Squadra", "punti"], ["Torino", 11], ["Napoli", 10], ["Udines", 10]]);
table2.render();

Come si può osservare abbiamo creato due oggetti tabella, associati a due contenitori distinti del DOM. E’ anche possibile rapidamente cambiare i dati usando il metodo build() ed eseguendo una nuova render().

Un componente form

In questo caso creiamo una semplice form che espone 3 metodi pubblici (setLabels, onsubmit e render) che gestiscono le label della form, la render e la onsubmit che associa la callback da eseguire una volta che l’utente ha cliccato. Qui il codice:

const createForm = (parentElement) => {
  let data;
  let callback = null;

  return {  
    setLabels: (labels) => { data = labels; },  
    onsubmit: (callbackInput) => { callback = callbackInput},
    render: () => { 
      parentElement.innerHTML = 
        data.map((name, index) => {
            return `<div>${name}\n<input id="${name}" type="text" /></div>`;
          }).join('\n')
          + "<button type='button' id='submit'>Submit</button>";  
      document.querySelector("#submit").onclick = () => {
// modo 1: lista di valori
        const result = data.map((name) => {
          return document.querySelector("#" + name).value;
        });
// modo 2 alternativo dizionario
        const result = {};
        data.forEach((name) => {
          result[name] = document.querySelector("#" + name).value;
        });
        callback(result);
      }          
    },
  };
};

const form = createForm(document.querySelector('#app'));
form.setLabels(["Nome", "Cognome", "Età"]);
form.onsubmit(console.log);
form.render();

Ricapitolando:

  • il metodo setLabels definisce le label associate agli input della form;
  • il metodo onSubmit serve per indicare all’oggetto form quale sarà la funzione callback da richiamare una volta che è stata effettuata la submit (cioè quando l’utente preme il pulsante submit)
  • il metodo render fa due cose:
    • crea la form dinamicamente in base alle labels definite sopra e la inserisce nel contenitore parentElement;
    • dopo il rendering, viene gestito il click al pulsante, che attiva una funzione che prima raccoglie i dati inseriti dall’utente nelle input e li mette in una lista, e poi richiama la funzione di callback memorizzata dal metodo onSubmit