Funzioni
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. Le funzioni sono oggetti in memoria e vanno trattati come tali: è possibile come visto salvare una funzione in una variabile, in array e dizionari, si possono restituire nelle funzioni, ed altro ancora. Le funzioni sono molto articolate e sono una caratteristica fondamentale di Javascript.
Oggetto this, funzioni apply e call
this è una parola chiave riservata di Javascript che si riferisce ad un oggetto. Questo oggetto rappresenta il contesto di esecuzione di uno script e di default si riferisce all’oggetto globale (nel browser l’oggetto globale è Window). Questo comportamento si replica, di default, anche nelle funzioni.
E’ tuttavia possibile modificare questo comportamento usando i modificatori di funzione apply, call e bind, che eseguono la funzione associando al this invece un oggetto specifico.
Call
Vediamo un esempio.
const p1 = {
firstName: "Mario",
lastName: "Rossi"
}
const p2 = {
firstName: "Maria",
lastName: "Bianchi"
}
function displayName() {
return this.firstName + " " + this.lastName;
}
console.log(displayName()); // mostrerà una stringa con uno spazio, Window non contiene infatti queste proprietà
console.log(displayName.call(p1)); // mostrerà "Mario Rossi"
console.log(displayName.call(p2)); // mostrerà "Maria Bianchi"
In pratica call sostituisce this con il valore passato per parametro ed esegue la funzione.
E’ possibile farlo anche con funzioni parametriche:
function addAge(age) {
this.age = age;
}
addAge.call(p1, 20);
addAge.call(p2, 17);
console.log(p1.age); // mostrerà l'età
console.log(p2.age); // mostrerà l'età
Con call è quindi possibile chiamare una funzione nella forma (context, parameters), dove il context è il valore di this e parameters è un sono i parametri presenti nella funzione.
functionName.call(context, parameters)
Apply
Apply è del tutto identica con una sola differenza, che i parametri sono passati sotto forma di array.
function addAge(age) {
this.age = age;
}
addAge.apply(p1, [20]);
addAge.apply(p2, [17]);
console.log(p1.age); // mostrerà l'età
console.log(p2.age); // mostrerà l'età
Spread (operatore “…”)
Spread è un operatore che consente di enumerare gli elementi di un array o di un dizionario.
Array
Usando l’operatore spread è possibile elencare gli elementi senza dover iterare su di essi. Ad esempio:
const a1 = ["mele", "pere", "banane"];
const a2 = ["fragole", "ciliegie", "lamponi"];
const a3 = [...a1, ...2];
E’ possibile destrutturare un array, ovvero scomporlo in parti usando sempre lo stesso operatore. Ad esempio:
const personData = ["Mario", "Rossi", "Roma", new Date(1,9,2000), "celibe"];
const [firstName, lastName, ...others] = personData;
console.log(firstName);
console.log(lastName);
console.log(...others);
E’ possibile usare lo spread per enumerare i parametri di una funzione in un array.
function doSomething(a, ...args) {
console.log(args);
}
doSomenthing(1,2,3); // stamperà [2,3]
Questo tipo di funzioni sono comode quando alcuni parametri sono opzionali.
Dizionari
Si utilizza in modo analogo. Un suo uso pratico è quando si vuole aggiungere una chiave ad un dizionario.
let dict = {
firstName: "Mario",
lastname: "Rossi"
}
dict = {
age: 20,
...dict
}
Funzioni autoeseguenti
E’ possibile scrivere funzioni anonime che si eseguono automaticamente nello script principale (e con utilità minore, anche dentro altre funzioni). Il vantaggio delle funzioni autoeseguenti è che evitano di utilizzare variabili globali nello script, ma di rinchiuderle come parametri della funzione autoeseguente. 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. Di solito le funzioni passate come parametro hanno lo scopo di far eseguire qualche azione alla fine della funzione. Ad esempio:
const f = (a, b, action) => {
const result = action(a,b);
console.log(result);
}
f(2, 3, (a,b) => a+b);
f(2,3, (a,b) => a*b);
Lo scopo di queste funzioni “contenitore” è che quando serve utilizzare lo Strategy pattern: in questo pattern occorre scrivere diverse funzioni simili, che differiscono solo per una parte del codice, e quindi risulta più conveniente creare una funzione contenitore, che contiene la parte comune, e poi eseguire la parte che diverge in una funzione, che appunto viene ricevuta come parametro. Un esempio sono gli ordinamenti, che ricevono come argomento la regola di ordinamento (decrescente, crescente, ecc.).
Un altro utilizzo, molto diffuso, è quello delle 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.
Funzioni generatrici
Le funzioni possono essere restituite come risultato della funzione. Qui un esempio:
const searchCreator = (list) => {
const filtered = list.filter(element => element !== null || element !== undefined);
return (searchString) => {
return filtered.first(element => element.search(searchString) !== -1);
}
}
const search = searchCreator(["Mario", null, "Maria", "Sergio");
console.log(search("Mario"));
La funzione search cercherà nella lista filtrata dagli elementi nulli.
Currying
Un altro utilizzo, molto utile nella programmazione frontend, è quello di creare funzioni che creano altre funzioni in cascata, il cosiddetto currying:
const adder = (a) => {
return (b) => {
return a + b;
}
}
const add3 = adder(3);
console.log(add3(5)); // stampa 8
console.log(add3(15)); // stampa 18
console.log(adder(5)(10)); // stampa 15
L’utilità del currying consiste nel fatto che è possibile scomporre una funzione complessa con molti passaggi in più funzioni con meno passaggi. Vediamo un esempio significativo, una funzione che riceve i titoli delle colonne di una tabella, e crea una funzione per completarla coi dati.
const tableBuilder = (...args) => {
let header = "<table><thead>";
header += args.map((title) => "<th>" + title + "</th>").join("");
header += "</thead";
return (data) => {
let html = header;
let rows = data.map((row) => {
let htmlRow = "<tr>";
htmlRow += row.map((cell) => "<td>" + cell + "</td").join("");
htmlRow += "</tr";
}).join("");
return html;
}
}
const nameTableFunction = tableBuilder("Nome", "Cognome");
const htmlTable1 = nameTableFunction(["Mario", "Rossi"]["Maria", "Bianchi"]);
const htmlTable2 = nameTableFunction(["Gianluigi", "Verdi"]["Marta", "Rosselli"]);
Il vantaggio è che il codice che elabora l’header viene eseguito una volta sola e memorizzato nella memoria della funzione.
Tecnicamente com’è possibile tutto questo? Quando una funzione termina, come abbiamo visto nelle applicazioni che sfruttano la macchina di Von Neumann, le variabili sono eliminate dallo stack e quindi il garbage collector dovrebbe eliminarle. Tuttavia le variabili di tableBuilder rimangono ancora in memoria perchè sono referenziate dalla funzione restituita (la variabile header, per la precisione) quindi il garbage collector non le elimina.
Funzioni Closure
Le closure sono un altro modo per sfruttare le funzioni generatrici. Proviamo a vederlo con un esempio:
const createAdder = () => {
let sum = 0;
return (value) => {
sum += value;
return sum;
}
}
const adder = createAdder();
console.log(adder(3)); // stampa 3
console.log(adder(3)); // stampa 6
console.log(adder(5)); // stampa 11
La funzione che abbiamo creato crea delle variabili che restano in memoria che continuano a persistere anche dopo che la funzione createAdder ha terminato, perché ne esiste in memoria una refenziazione (la funzione adder usa la variabile sum). 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. Le vedremo usate ancora con la programmazione ad oggetti e nei Componenti.