JavaScript
JavaScript
È un linguaggio che fu introdotto nel 1995 da Netscape con l’idea di consentire la programmazione di pagine
web dinamiche.
Inizialmente era client-side ovvero l’utente richiede al server attraverso il browser una determinata pagina web
che viene fisicamente trasferita dal server al computer, interpretato dal browser ed infine visualizzata a
schermo. I processi di elaborazione e il codice vengono eseguiti sul computer in locale. Lo svantaggio
principale è che uno script può comportarsi in maniera differente a seconda del browser su cu viene eseguito.
Ora il suo utilizzo è diventato anche server-side ovvero l’utente richiede sempre al server una determinata
pagina web attraverso il browser ma i processi di elaborazione e il codice avvengono sul server, la pagina viene
interpretata dal server e all’utente arriva soltanto un file (pagina HTML) con le elaborazioni fatte dal server in
base alle richieste dell’utente. Il programma non è reso disponibile all’utente che può vedere solo il risultato
del programma.
Ci sono stati diversi upgrade negli ultimi 25 anni e la versione più diffusa è ECMAscript2021.
Linguaggio compilato o imperativo → permette di scrivere un codice che viene poi controllato e compilato,
ovvero ogni istruzione viene trasformata nel corrispondente codice macchina formando un file eseguibile che
può essere eseguito dal processore.
Si tratta inoltre di un linguaggio weakly type ovvero debolmente tipizzato perché consente di omettere la
descrizione del tipo di una variabile in fase di dichiarazione. Una stessa variabile può quindi cambiare tipo e
contenere in sequenza valori di ogni tipo. Questo concede al linguaggio grande flessibilità e poche restrizioni
ma rende più difficile il debugging.
Dato un problema da risolvere bisogna progettare un algoritmo che presi dei dati in input restituiscano un
output. Il programma è l’implementazione di una soluzione algoritmica che risolve un problema. Affinché il
programma implementi l’algoritmo noto bisogna mettere in sequenza una serie di comandi da eseguire in
ordine. Questi comandi devono essere comprensibili per l’interprete, quindi devono seguire le regole della
sintassi del linguaggio.
Sommario
BNF ........................................................................................................................................................ 4
TIPI DI DATO E CASTING ........................................................................................................................... 4
ESERCIZI .......................................................................................................................................... 6
CONTROLLO DI FLUSSO........................................................................................................................... 8
CONDIZIONALE: COMANDO IF - ELSE .................................................................................................... 8
ESERCIZI .......................................................................................................................................... 9
CONDIZIONALE: COMANDO SWITCH .................................................................................................... 9
ESERCIZI ........................................................................................................................................ 10
ITERAZIONE: COMANDO FOR .............................................................................................................. 11
ESERCIZI ........................................................................................................................................ 11
ITERAZIONE: COMANDO WHILE .......................................................................................................... 12
ESERCIZI ........................................................................................................................................ 12
ITERAZIONE: COMANDO DO WHILE ..................................................................................................... 13
BREAK E CONTINUE ............................................................................................................................ 13
FUNZIONI .............................................................................................................................................. 15
ESERCIZI ........................................................................................................................................ 17
ARRAY................................................................................................................................................... 19
ESERCIZI ........................................................................................................................................ 22
OGGETTI ............................................................................................................................................... 25
ESERCIZI ........................................................................................................................................ 27
RAPPRESENTAZIONE BINARIA ................................................................................................................ 29
ESERCIZI ........................................................................................................................................ 33
SCOPING .............................................................................................................................................. 35
INSIEMI ................................................................................................................................................. 38
ESERCIZI ........................................................................................................................................ 38
ALBERI BINARI ....................................................................................................................................... 40
ESERCIZI ........................................................................................................................................ 40
ALBERI K-ARI ......................................................................................................................................... 44
ESERCIZI ........................................................................................................................................ 44
ALGORITMI DI ORDINAMENTO ................................................................................................................ 48
STRUTTURE DATI CON ARRAY ................................................................................................................. 52
COPIA SHADOW E DEEP ......................................................................................................................... 55
ASSEGNAMENTI DESTRUTTURANTI ......................................................................................................... 56
OPERATORE SPREAD ............................................................................................................................. 58
ESPERIMENTI SUI CONCETTI DI ANALISI .................................................................................................. 59
PASSAGGIO PER VALORE O RIFERIMENTO ............................................................................................... 62
COSTRUTTORI ....................................................................................................................................... 64
METODI................................................................................................................................................. 65
PROTOTIPI............................................................................................................................................. 67
CLASSI .................................................................................................................................................. 69
EREDITARIETÁ .................................................................................................................................... 70
DICHIARAZIONI DI CAMPO .................................................................................................................. 71
MEMBRI PRIVATI ................................................................................................................................. 72
MEMBRI STATICI ................................................................................................................................. 72
GETTER E SETTER ............................................................................................................................... 72
GENERATORI ......................................................................................................................................... 74
ECCEZIONI............................................................................................................................................ 76
MODULI ................................................................................................................................................ 80
WRAPPER DI TIPI BASE ........................................................................................................................ 82
DATE ................................................................................................................................................. 83
ESPRESSIONI REGOLARI ..................................................................................................................... 84
ARRAY CON TIPI DI MACCHINA ............................................................................................................ 85
CLASSE SET ....................................................................................................................................... 85
CLASSE MAP ...................................................................................................................................... 85
JSON ................................................................................................................................................. 86
OGGETTI DELL’AMBIENTE (HOST-DEFINED).......................................................................................... 87
MODULO FILE SYSTEM ........................................................................................................................ 87
MODULO OPERATING SYSTEM ............................................................................................................ 87
MODULO PROCESS ............................................................................................................................ 87
MODULI URL E HTTP (ACCESSO AL WEB).............................................................................................. 88
BNF
Tutti i linguaggi di programmazione seguono una determinata grammatica più o meno complessa. La Backus-
Naur Form (BNF) è un modo di descrivere sinteticamente la grammatica di un linguaggio. La grammatica è data
da un insieme di produzioni ciascuna delle quali ha la forma:
classe ::= definizione1 | definizione2 | … | definizionen
e ogni definizione è una sequenza di simboli terminali e classi.
▪ Booleani
o True, false
o Valore di verità di espressioni (confronti)
o Operatori (AND, &&, OR, ||, NOT, !)
▪ Stringhe
o Letterali (caratteri alfanumerici o unicode) delimitati da ‘ ‘, “ “, ` `.
o Per usare apici dentro altri apici devono essere diversi.
o Operatori (+concatenazione, <, >, ==)
Variabile → è un elemento del programma a cui viene assegnato un nome identificatore e un valore che
cambia. Si dichiara con let o var. Ci sono dei nomi che non possiamo assegnare alle variabili come 15 o true.
Constante → non è possibile modificarne il valore e si dichiara con const.
Quando si fanno operazioni tra tipi diversi viene messo in atto il casting automatico.
Il programma produce la soluzione al problema un output, ovvero il risultato del programma che va mostrato o
salvato, sulla base di input forniti, ovvero dati iniziali su cui lavorare.
Il comando per passare parametri esterni da tastiera è prompt()
Il comando per mostrare a video nella console è console.log().
Se stampo console.log() è una riga vuota
Bisogna utilizzare: const prompt = require("prompt-sync")();
Il comando \\ commenta la riga, altrimenti per un paragrafo */
Per eseguire un’espressione all’interno di una stringa si utilizza `La somma è $(a+b)`
Si usa typeof per controllare il tipo della variabile
Number()/String()/Double() permettono di fare il casting
ESERCIZI
ESERCIZIO: scriviamo un programma che risolve un’equazione di primo grado
▪ A*x +b=c; x=?
▪ A, b, c sono input del programma
▪ X è l’output
let a, b, c
a=prompt(“Inserisci a:”) \\ a risulta come stringa, è sbagliato
a=Number(prompt(“Inserisci a:”))
b=Number(prompt(“Inserisci b:”))
c=Number(prompt(“Inserisci c:”))
let x=(c-b)/a
console.log(x)
ESERCIZIO: Scrivere un programma che stampa la tabella di verità per la funzione Booleana “OR”
console.log(“true OR true=”, true || true) \\la virgola viene sostituita con uno spazio mentre
il + non viene sostituito, non viene eseguito il cast
e stampa una stringa e un booleano
console.log(“true OR false= ” + String(true||false)) \\posso anche non mettere il cast perché con una
stringa e un booleano concatenati (+) trasforma il
booleano in stringa
console.log(“false OR true=”, (false!!true)) \\avendo la virgola stampa una stringa e un
booleano
console.log(`false OR false = ${false ||false}`) \\metodo per eseguire l’operazione all’interno
della stringa con dollaro, parentesi graffe e apici
storti esterni
Posso anche prendere valori da tastiera:
let a= prompt("a?")
let b= prompt("b?")
let ris = (a == "true") || (b == "true")
console.log("a OR b =", ris)
ESERCIZIO: Scrivere un programma che stampa la tabella di verità per la funzione Booleana “AND”
let a= prompt(”a?”)
let b= prompt(“b?”)
let ris = (a == "true") && (b == "true")
console.log("a AND b =", ris)
ESERCIZIO: Scrivere un programma che stampa la tabella di verità per la funzione Booleana “NOT”
ESERCIZIO: Scrivere un programma che dati un numero n ed un esponente e (letti dalla tastiera), calcola e
mostra a video il valore n^e
let n = Number(prompt(“n?”))
let e = Number(prompt(“e?”))
console.log(n**e)
^ vuol dire fare SOR ovvero esclusivo mentre ** vuol dire moltiplicare n per sé stesso e volte. Questo esercizio
si può fare anche con un ciclo.
ESERCIZIO: Scrivere un programma che data una temperatura in Celsius dalla tastiera, calcola e mostra a
video la temperatura in Fahrenheit (F=C * 1.8 + 32)
ESERCIZIO: Scrivere un programma che data una temperatura in Fahrenheit dalla tastiera, calcola e mostra a
video la temperatura in Celsius.
ESERCIZIO: Scrivere un programma che dato un numero di secondi dalla tastiera calcola e mostra a video il
numero di ore, minuti e secondi inclusi
Oltre alle grammatiche per rappresentare queste istruzioni si utilizzano i diagrammi di sintassi, una
rappresentazione grafica delle grammatiche.
Ciò che viene messo fra parentesi viene sempre valutato come un’espressione e quindi restituisce un
risultato. L’espressione può essere di qualsiasi tipo di dato grazie al cast automatico (una variabile, una
somma di interi, una concatenazione di stringhe) ma restituirà sempre un booleano. Se l’espressione viene
valutata true si esegue il primo comando, se false si esegue l’else o nel caso in cui non fosse specificato salta
la sequenza di comandi.
Un modo alternativo all’if è l’espressione condizionale, che tuttavia essendo un’espressione restituisce
sempre un risultato. L’istruzione che svolge l’espressione condizionale è: se è vera la prima espressione
prendi il primo valore altrimenti prendi il secondo.
ESERCIZIO: Scrivere un programma che legga da tastiera tre numeri e mostri a video il loro massimo.
Comando_switch:
Elenco_casi: Casi:
Caso: Defaulf:
Comandi:
ESEM Stampa del giorno della settimana in base all’input
ESERCIZI
ESERCIZIO: Scrivere un programma che legga da tastiera due numeri ed un operatore (tra +, -, * e /) e mostri a
video il risultato dell’operazione tra due numeri.
▪ Nel primo caso il primo comando assegna il valore iniziale della variabile di iterazione, la seconda
espressione determina la condizione per cui si ripete il comando, il terzo comando indica come andare
ad aggiornare la variabile di iterazione.
▪ La forma in e of si utilizza quando si opera su un array, ovvero un insieme di elementi.
ESERCIZI
ESERCIZIO: Si scriva un programma che, dato un intero positivo n, calcola e stampa la somma dei numeri
dispari da 1 a n (con n compreso)
let n = Number(prompt("n<"))
let sum = 0
for(let i=1; i<=n; i=i+2)
sum += i
console.log(sum)
ITERAZIONE: COMANDO WHILE
Il comando while esegue il blocco di comandi fino a quando l’espressione guardia è vera, per questo motivo si
usa per iterazioni indeterminate ovvero quando non si conosce a priori il numero di iterazioni da eseguire.
L’espressione ha valore booleano ma può essere qualsiasi tipo di dato grazie al cast automatico. È importante
modificare il valore di guardia nel corpo affinché sia possibile terminare il ciclo.
Spesso se sappiamo che almeno una volta dobbiamo eseguire una certa espressione scriviamo l’istruzione
prima del while, mettere la condizione e il blocco del while e poi ripetere l’istruzione.
ESERCIZI
ESERCIZIO: Si scriva un programma che legge numeri fino a che la loro somma non supera 101. Il programma
deve poi stampare la somma ottenuta
let sum =0
while (sum<=101){
let n = Number(prompt("n<"))
sum += n
}
console.log(sum)
ESERCIZIO: Si scriva un programma che legge un intero n e valuta se è primo, ovvero che non esista un numero
d tra 2 e n-1 tale che n sia divisibile per d. Si stampi 1 se il numero è primo, 0 altrimenti. Si stampi inoltre il
tempo di esecuzione del programma
ESERCIZIO: Si modifichi il programma di sopra in modo che legga 10 numeri n e calcoli per ciascuno se si tratta
di un numero primo
ESEM let n
let sum = 0
do {
n = Number(prompt("n"))
if (n!= -1) sum+= n
} while (n!=-1)
console.log("La somma è", sum)
BREAK E CONTINUE
A volte possono verificarsi delle condizioni per cui è necessario interrompere una o tutte le iterazioni. I
comandi con cui è possibile farlo sono:
▪ BREAK esce dal comando iterativo interrompendone l’esecuzione e proseguendo con il blocco di
comandi successivo al comando iterativo interrotto
▪ CONTINUE salta l’iterazione corrente e passa alla successiva continuando l’iterazione prescritta dal
comando iterativo
ESEM for (let x=-5; x<=5;x++) //iterazione per pari/dispari su un intervallo del dominio
console.log(“f(${x})==${x%2==0 ? 1 : 0}”);
ESEM for (let x=-5; x<=5; x++){ //funzione a due parametri che calcola la differenza
for(let y=2;y<=2;y++){
console.log (“f(“ + x + “, “ + y “)=“, x-y)
}
}
Le funzioni sono uno dei tipi base di JavaScript ovvero possono essere denotate da costanti letterali, cioè è
possibile esprimere un valore di tipo funzione scrivendolo nel testo del programma. Una funzione prende in
input dati e calcola un valore nuovo che sarà il valore di tipo funzione. Un valore di tipo funzione è per sempre
un valore e in quanto tale esso può essere usato nelle espressioni come parametro o può essere il risultato di
un’espressione, può essere assegnato ad una variabile o inserito in un array; in più posso applicare una
funzione che mi restituisca una funzione o combinare più funzioni.
Abbiamo due notazioni per esprimere una costante letterale di tipo funzione:
▪ La notazione “freccia” (o lambda espressione) => non dà alla funzione un nome ma indica quali
parametri formali prende in input (può essere un singolo identificatore o una lista di parametri fra
parentesi tonde) e un’espressione o un blocco di comandi fra parentesi graffe che indicano che valore
restituire.
(x)=>(x[0]!="o") oppure (a,b)=>{}
▪ Con la parola chiave function posso invece dare un identificatore alla funzione, una lista di parametri
fra parentesi tonde e un blocco di comandi fra parentesi graffe
function nome (a,b){}
Ciò è equivalente a dichiarare una variabile con var di nome f e inizializzarla con un valore di tipo
funzione
var f= (a,b)=>{return a+n}
letterale funzione:
ESEM //assegno lambda espressione a un identificatore, passo due parametri fra tonde e uso un’espressione
aritmetica
let somma = (a,b) => a+b
//assegno lambda espressione a un identificatore, passo due parametri fra tonde e ho come
implementazione un blocco di comandi fra graffe
let somma2 = (a,b) => {
return a+b
}
Essendo le funzioni dei valori ci sono degli operatori che si possono applicare alle funzioni.
Il più importante è un operatore postfisso che si chiama applicazione (invocazione o chiamata) della funzione
ed è indicato da una coppia di parentesi posta a destra del valore. Questa operazione serve per far partire la
funzione ponendo fra le parentesi i parametri che volete passare alla funzione. Concettualmente sto
applicando un operatore al valore funzione proprio come applichiamo l’operatore + ai valori 2 e 3 per una
somma; infatti, come 2+3 è un’espressione che calcola un valore, applicando l’operatore postfisso ad una
funzione si ottiene un’espressione che calcola il valore di quella funzione secondo quell’invocazione.
f è l’identificatore della funzione, fra parentesi tonde l’espressione lambda in cui ho il parametro formale x
ovvero l’input secondo la definizione della funzione e il corpo che utilizza il parametro formale per calcolare il
valore restituito dalla funzione.
Quando chiamo la funzione fra parentesi all’interno dell’operatore di applicazione di funzione ho il parametro
attuale o argomento ovvero l’input che passo come parametro alla funzione e che viene assegnato alla
variabile che denota il parametro formale; a quel punto si esegue il corpo. Il parametro attuale viene passato
come riferimento, dunque se viene modificato all’interno della funzione, verrà modificato anche all’esterno di
essa.
Il corpo della funzione viene valutato in un ambiente in cui i nomi dei parametri formali sono associati ai valori
dei parametri attuali sia nel caso in cui il corpo è un’espressione sia nel caso in cui sia un blocco di comandi.
ESEM somma(3,4) → 7
somma=9
somma(3,4) → errore \\ sto provando ad invocare una funzione su un valore che è di tipo intero
ESERCIZI
ESERCIZIO: Scrivere una funzione minimo() che legga da tastiera x interi e restituisca il minimo dei valori letti
function minimo(x) {
let min=Infinity
for(let i=0; i<x; i++){
let n=Number(prompt ("n"))
if (n<min) min=n
return min
}
ESERCIZIO: si scriva una funzione che legga da tastiera 10 interi e restituisca la media aritmetica di tutti i valori
diversi da 0 e di segno uguale al numero passato come parametro.
function media(x){
let totale =0
let quanti =0
for (let i =0; i<10; i++){
let n=Number(prompt(“n”))
if(n!=0 && n*x>0){ \\oppure if(n!=0 && ((n>0 && x>0)||(n<0 && x<0)))
totale +=n
quanti++ \\vengono inseriti 10 valori ma quanti vengono sommati?
}
}
if (quanti > 0) \\evita il caso in cui non venga sommato alcun numero
return totale / quanti
return undefined \\non metto else perchè, se entra nell’if con return si blocca
}
ESERCIZIO: scrivere una funzione zeri (f,a,b) che conti gli zeri della funzione f nell’intervallo [a,b]
\\ritorniamo un valore che è un numero e non \\uso un’espressione lambda per denotare
una funzione una funzione nel return
function sposta(p){
return function (n,m){
p.x+=n
p.y+=m
return p
}
}
ARRAY
Gli array sono strutture di dati nate come porzioni di memoria contigua a cui accedere attraverso un indice che
indica una specifica cella di memoria che può essere interpretata e che si comporta come una variabile
tradizionale. Tipicamente:
▪ tutte le celle sono variabili di uno stesso tipo detto tipo base dell’array
▪ il numero di celle è prefissato e denota la dimensione dell’array
▪ ogni cella è identificata da un valore di indice
▪ gli indici sono contigui, partono da 0 e finiscono a n-1
In JavaScript invece:
▪ le celle possono avere variabili di diverso tipo
▪ il numero di celle non è prefissato ma può essere modificato
▪ gli indici non vanno obbligatoriamente in ordine e non sono obbligatoriamente positivi.
Un array è delimitato da [] dentro alle quali si trova una lista di espressioni separate da virgole in cui però
alcune espressioni possono anche mancare occupando un posto vuoto nell’array con un indice ma non un
valore. Se l’espressione mancante si trova in fondo all’array invece viene ignorata.
ESEM let array=[ ,10, ,4, ] \\si tratta di un array di 4 elementi in cui l’elemento in posizione 0 e 2 non ci sono
L’ultimo elemento vuoto invece viene eliminato e non conta nella dimensione dell’array.
Attraverso l’indice è possibile accedere agli elementi dell’array e scorrere sequenzialmente tutti gli elementi
mediante un ciclo iterativo.
Quando stampo l’array gli elementi vengono visualizzati in ordine secondo l’indice.
▪ A.concat(b) forma un array con tutti gli elementi di a seguiti da tutti gli elementi di b in ordine
▪ A.toSorted() nuovo array con elementi in ordine crescente senza modificare quello originale
▪ A.sort() ordina un array in base al valore degli elementi e possiamo passargli un criterio di
ordinamento. La funzione prende a e b, se restituisce negativa a>b, se restituisce
positivo a<b, altrimenti con 0 sono uguali
▪ A.length mostra la lunghezza di un array ovvero il suo numero di elementi. Ad un array posso
anche assegnare una lunghezza definita come a.length=1
▪ delete(a[1]) cancella un elemento lasciando l’indice vuoto, dunque la lunghezza dell’array non varia
▪ A.toString() trasforma l’array in una stringa unendo gli elementi intervallati dalla virgola
▪ A.map(f) crea un nuovo array a partire dal precedente e applicando a tutti gli elementi
dell’array la funzione f passata come parametro. La funzione f può prendere fino a tre
parametri e map li riconoscerà come elemento, indice e array in questo ordine.
▪ A.filter(f) crea un nuovo array a partire dal precedente mantenendo soltanto gli elementi che
rispettano la funzione passata come parametro.
• A.splice(x,y,z) rimuove dalla posizione x, y elementi e aggiunge z dove z possono essere più elementi.
Se y e z non vengono espressi elimina fino alla fine dell’array. Non lascia buchi
nell’array. Restituisce un array con gli elementi eliminati
• A.every(p) restituisce true se tutti gli elementi di A soddisfano il predicato p, false altrimenti
• A.some(p) restituisce true se almeno uno degli elementi di A soddisfa il predicato p, false altrimenti
• A.find(p) restituisce un elemento e di A tale che p(e) è true, e undefined se nessun e soddisfa p
• A.findIndex(p) restituisce l’indice di un elemento e di A tale che p(e) è true, o -1 se nessun e soddisfa p
• A.slice(x, y) viene utilizzata per copiare pezzi di array. Se non gli passo parametri copia l’array
dall’inizio alla fine; altrimenti il primo parametro indica da dove iniziare a copiare, il
secondo l’ultimo elemento da copiare che però non viene copiato. Questa funzione
restituisce una copia dell’array originale.
• A.reduce(f,z) scorre l’array da sx verso dx, viene chiamata sui singoli elementi dell’array in maniera
ricorsiva e restituisce un solo valore finale. La funzione f prende due parametri f(x,y) cui
il primo è l’elemento dell’array mentre il secondo viene passato da reduce stessa.
Reduce prende il primo elemento a1 di A e ci applica la funzione data f, la cui prima
chiamata sarà f(a1,z) dove x è il primo elemento dell’array e y è il valore z passato dalla
funzione reduce. A questo punto viene chiamata la funzione data f sul secondo
elemento dell’array f(a2,f(a1,z), chiamata in cui x è il secondo elemento dell’array
mentre il valore y è il risultato della funzione sull’elemento dell’array precedente. La
chiamata sul terzo elemento a3 sarà f(a3,f(a2,f(a1,z))) e così via fino alla fine degli
elementi dell’array.
A=[a1,a2,a3]
f(x,y)=x+y
reduce(f,0) → z1=f(a1,0)=a1
z2=f(a2,z1)=a2+a1
f(a3,z2)=a3+a2+a1
In JavaScript gli indici possono essere sparsi e non per forza positivi. Tuttavia, un indice negativo non lo trovo
come si potrebbe intuire prima dell’indice zero ma in ultima posizione scritto in modo particolare
Posso anche avere degli array con indici sparsi dove alcuni elementi mancano. La lettura relativa ad indici che
non esistono restituisce undefined.
Le stringhe hanno una sintassi molto simile agli array, infatti, posso accedere ai singoli caratteri con la
notazione degli array e possiedono il modulo per la lunghezza ma non si comportano nello stesso modo infatti
non è possibile modificare un singolo carattere di una stringa.
ESERCIZI
ESERCIZIO: funzione che somma tutti gli elementi di un array a passato come argomento
a=[1,2,4,5}
function somma(a){
let sum=0
for(i=0; i<a.length;i++){ oppure i<=(a.length-1)
sum+=a[i]
}
return sum
}
ESEM a=[1,2,3,”w”]
sum(a) \\restituisce la stringa “6w”
L’operatore + è over loading ovvero ha due comportamenti a seconda del tipo di dato su cui viene applicato.
Applicato a due interi restituisce un intero, applicato a stringhe restituisce la concatenazione fra le stringhe,
applicato ad un intero e una stringa converte l’intero in una stringa e fa la somma fra stringhe che restituisce
una stringa.
In questo caso fa due operazioni ovvero inizialmente somma interi e sum è un intero, quando trova una stringa
converte sum a stringa e inizia a concatenare.
ESEM a=[“w”,1,2,3]
sum(a) \\restituisce “0w123” in quanto sum è inizializzato a 0 che viene sommato alla stringa w
function max(a){
let max=a[0] //tiene in memoria il massimo parziale partendo dalla prima
casella
for (i=1; i<a.length; i++){ //la riga sopra è il motivo per cui parto dall’indice 1
if (a[1]>max)
max=a[i] //con espressione condizionale max=a[i]>max?a[i]:max
}
return max
}
function min(a){
let min=a[0] \\tiene il massimo parziale partendo dalla prima casella
for(i=1; i<a.length; i++{ \\la riga sopra è il motivo per cui parto dall’indice 1
if (a[1]<min)
min=a[i] \\con espressione condizionale min=a[i]>min?a[i]:min
}
return min
}
ESERCIZIO: funzione che trova minimo e massimo in un array a e li mette in ordine min, max in un altro array
restituito come valore di ritorno della funzione
ESERCIZIO: funzione che prende un array a e ne scambia gli elementi in posizione i, j passati come parametri.
function swap(a,i,j){
if((i<0) || (i>a.length-1) || (j<0) || (j>a.length-1)){ //condizioni limite
let x=a[i]
a[i]=a[j]
a[j]=x
}
return a
}
function inverti(a){
let i=0
let j=a.length-1
while(i<j){ //finchè i>j continuo a sommare
swap(a, i, j)
I++
J--
}
return a
}
let a=[]
let b=[2]
var som=0
if(a.length==b.length){
for (let i=0;i<(a.length); i++){
for(let j=0;j<(b.length); j++){
if (a[i]==b[j]) som=som+1
}
}
if(som==(a.length)) console.log("a=b")
}
else console.log("a è diverso da b")
function cerca(A,k){
var i=0
var R=-1
while(i<A.length && R==-1){
if (A[i]==k) R=1
else i++
}
if (i<=(A.length-1)) return i
else return -1
}
ESERCIZIO: funzione che cerca un elemento in un array ordinato e restituisce la sua posizione
function triangoliRettangoliV2(triangoli) {
let rettangolo = (t) => (t.rettangolo)
let genera = (t) => {
let nt = {}
nt.cateti = [t.base, t.altezza]
nt.ipotenusa = Math.sqrt(t.base ** 2 + t.altezza ** 2)
return nt
}
return triangoli.filter(rettangolo).map(genera)
}
OGGETTI
Letterale oggetto: lista proprietà:
Gli oggetti (o dizionario) è una mappa da chiavi a valori. Si tratta di una funzione matematica il cui dominio è
l’insieme di tutte le chiavi (stringhe e numeri) e il codominio è l’insieme di tutti i valori possibili. Possiamo
anche dire che un oggetto è un insieme di coppie (chiave, valore). Gli oggetti possono essere denotati dal
valore letterale racchiuso fra parentesi graffe. Gli oggetti consentono quindi di raccogliere in un solo valore (di
tipo oggetto) molti valori di diverso tipo, assegnando a ciascuno di essi un nome (chiave).
In questo esempio le chiavi sono stringhe che potremmo usare anche come identificatori ma in realtà fra i
valori ragionevoli di stringa in JavaScript abbiamo anche gli smile. All’interno della parentesi graffe troviamo
una lista di proprietà che possono essere descritte come chiave: espressione oppure con un identificatore
ovvero il nome di una variabile che crea una chiave con lo stesso nome della variabile e un valore associato
che è quello della variabile.
Una chiave invece può essere un nome di variabile, un letterale stringa, un letterale numero o può essere
valutata tramite con un’espressione fra quadre.
L’operazione principale sugli oggetti è l’accesso a una chiave. Inoltre, gli oggetti sono attrezzi dinamici che
possono essere modificati dinamicamente: aggiungendo o eliminando chiavi o modificando il valore di esse.
COMANDI OGGETTI
▪ Accedere a una chiave => ogg.chiave / ogg[chiave]
▪ Scorrere le chiavi del ciclo. Ad ogni iterazione k prende una chiave dell’oggetto. Il valore della chiave è
dentro k ma non è k => var k in ogg
Come per gli array == non confronta il contenuto ma l’identificativo assegnato, dunque, per valutare se due
oggetti sono uguali bisogna confrontare elemento per elemento. Devo estrarre una per una ogni chiave
dell’oggetto e per quella chiave confrontare il valore con quello della chiave corrispondente dell’altro oggetto.
L’istruzione for(e of a) invece estrae i valori corrispondenti alle chiavi di un array ma soltanto delle chiavi
positive. For(e of a) però funziona soltanto su cose iterabili e non funziona sugli oggetti; gli array sono iterabili
nelle loro chiavi numeriche positive mentre gli oggetti non lo sono.
Dunque, gli array sono oggetti ma non hanno il loro stesso comportamento, sono implementati dagli oggetti
ma sono iterabili e deve essere prestata attenzione per lo scorrimento dell’array; solo le chiavi positive sono
chiavi proprie dell’array e definiscono un comportamento iterabile
ESERCIZI
ESERCIZIO: Scrivere una funzione che calcola la distanza tra due punti su un piano cartesiano, date le loro
coordinate x e y
ESERCIZIO: Scrivere una funzione che, dati due punti (di cui si conoscono le coordinate x e y), restituisce il
punto che è più lontano dall’origine (0,0)
ESERCIZIO: Scrivere un programma che legge da tastiera una lista parole fino a che l’utente non scrive
“BASTA!”. A quel punto il programma deve stampare la lista delle parole inserite.
ESERCIZIO: Scrivere un programma che legge da tastiera una lista parole fino a che l’utente non scrive
“BASTA!”. A quel punto il programma deve stampare la lista delle parole inserite (senza ripetizioni)
ESERCIZIO: Modificare l’esempio precedente in modo che il programma stampi anche la quantità di volte che
una parola è stata inserita.
let parole={}
let w=prompt("Parola?")
while (w!="BASTA!"){
let trovata=false
for(j in parole){
if (j==w) {
trovata=true
break
}
}
if(!trovata){
parole[w]=1
j++
}
else parole[w]+=1
w=prompt("Parola?")
}
console.log(parole)
RAPPRESENTAZIONE BINARIA
L’informazione all’interno dei calcolatori viene rappresentata tramite la rappresentazione binaria.
Rappresentare una cifra in base 10 significa moltiplicare ogni cifra per una potenza di 10:
7452 = 7*103 + 4*102 + 5*10 + 2*100
In numeri in base dieci 𝑛10 = ∑𝑛−1𝑖=0 𝑑𝑖 ∗ 10
𝑖
ESEM funzione che stampa le cifre di un numero, dalla meno significativa alla più significativa
function cifre(n){
s=""
i=0
while(n>0){
resto=n%10
s= resto+"*10^" + i + "+" + s
n=(n-rest)/10
i++
}
console.log(s)
}
Cifre={0,1} Base=B=2
Se hai N bit ovvero N posizioni, il massimo numero naturale che puoi rappresentare è 2𝑁 − 1 = ∑𝑛−1
𝑖=0 2 ; su N
𝑖
ESEM funzione che stampa le cifre di un numero rappresentato in base 10 in un'altra base
SIMB=[0,1,2,3,4,5,6,7,8,9,”A”,”B”,”C”,”D”,”E”,”F]
function converti(n,b){
s="-" + b //specifichiamo in quale base rappresentiamo
while(n>0){
resto=n%b
s= SIMB[resto] + s
n=(n-resto)/b
}
console.log(s)
}
OVERFLOW E UNDERFLOW
I computer usano la rappresentazione binaria in quanto i circuiti elettronici la possono rappresentare e fare
operazioni a costo basso. Tuttavia, come abbiamo visto i numeri che si possono rappresentare sono finiti.
Possiamo rappresentare tutti e soli i numeri che stanno dentro il numero massimo di bit a disposizione. Le
architetture di un calcolatore sono fatte per calcolare con un numero massimo N di bit. Fissare un numero
massimo di bit impone un limite alla rappresentazione. Un calcolatore rappresenta le informazioni in maniera
discretizzata.
BIT: unità elementare di informazione. Ha solo due stati (il minimo perché si possa parlare di cose diverse).
Convenzionalmente li denotiamo con 0 e 1
BYTE: 8 bit unità elementare di memoria, 2 8 valori distinti.
PAROLA DI MEMORIA: 8,16,32,64 bit dimensione dei registri nella CPU
10011010+
11111111=
100110011
Non riesco a rappresentare il risultato dell’operazione con lo stesso numero di bit degli operandi. Quando si
superano le capacità di rappresentazione dell’architettura si va in overflow ovvero c’è un riporto oltre il bit più
significativo. Bisogna stare attenti perché due numeri potrebbero essere rappresentabili ma ciò non garantisce
che lo sia la loro somma. L’ultimo 1 a sx andrà perso e la somma sarà più piccola di uno dei due addendi. Non
è dunque possibile rappresentare tutti i numeri naturali fino all’infinito perché fissare un numero massimo di
bit pone un limite ai numeri che si possono rappresentare.
I numeri interi si possono rappresentare tramite il loro modulo e segno. Si può tenere il primo bit (MSb) per
rappresentare il segno del numero (0 per il + e 1 per il -); mentre N-1 bit vengono utilizzati per rappresentare il
modulo di numeri interi in un intervallo fra [-2N-1 -1; 2N-1 -1]. In questo modo però ho una capacità di
rappresentazione minore di un bit (quello occupato per il segno), ho due rappresentazioni diverse per zero
(0000 0000 e 1000 0000) che corrispondono a 0+ e 0-; inoltre la somma di due positivi è negativa se riporto sul
bit di segno.
Per quanto riguarda invece la rappresentazione di numeri reali, i numeri con la virgola vengono rappresentati in
termini di mantissa (nella forma 0, x) ed esponente.
Tutti i numeri reali sono rappresentati con la notazione esponenziale rappresentando la mantissa (numeri
dopo la virgola) separatamente dall’esponente. Se la dimensione di rappresentazione dei miei interi sta su 32
bit: uno è dedicato al segno, 8 bit rappresentano l’esponente fino a 27 -1 e 23 bit per la mantissa che è un
numero intero fino a 222 -1.
Questa notazione crea problemi dovuti alla finitezza della rappresentazione e se fra due numeri interi c’è un
buco di 1 fra due numeri reali non c’è nessun buco sulla linea dei numeri ma sul computer questo buco c’è
perché non è possibile rappresentarli tutti. Quello che succede è che ci sono sia i problemi di overflow nel
sommare numeri troppo grandi e nel sommare numeri troppo piccoli ovvero troppo negativi, ma anche il
problema di underflow ovvero il problema di precisione vicino allo zero. Se bisogna rappresentare un numero
molto piccolo che magari deriva da una moltiplicazione di numeri piccoli che può succedere che il numero
significativo cascherà nell’area rossa in cui non abbiamo abbastanza esponente per rappresentare quel
numero o potremmo finire la mantissa. C’è un numero finito di numeri reali rappresentabili che sono un ordine
di infinito in più rispetto ai numeri interi rappresentabili. Più aumento la precisione di macchina più la parte
rossa non rappresentabile è piccola. I linguaggi di programmazioni danno il numero massimo rappresentabile
sia intero sia con la virgola per fare dei calcoli di sicurezza.
Anche il testo si rappresenta in maniera binaria quindi quando scrivo un particolare carattere questo avrà
associato un codice decimale nell’ASCII che ha una sua codifica binaria, ottale e sedicimale. La prima
standardizzazione fatta della codifica dei caratteri va da 0 a 127 perché era il numero massimo
rappresentabile nelle architetture quando è stata formalizzata. Successivamente è arrivato UNICODE che mi
permette di rappresentare tutti i linguaggi del mondo. Tutti i simboli grafici sono rappresentati all’interno del
calcolatore perché ciascuno ha un codice numerico associato in UNICODE che si trasforma in un insieme di
bit.
Le immagini raster chiamate bitmap sono degli insiemi di bit dove 0 significa che il pixel è spento e 1 accesso
all’interno di una matrice che riescono a rappresentare un oggetto di natura grafica in bianco e nero. La
matrice posso anche rappresentarla con un vettore che mi codifica ogni riga della matrice composta da 8 bit in
base esadecimale in un numero di 2 cifre; espandendo il vettore con il trasformatore di base chiedendogli la
conversione binaria viene fuori l’immagine.
Per immagini più complesse ho sempre una matrice in cui nelle varie caselle non ho solo 0 e 1 che codificano
acceso o spento ma definisco tutte le tonalità di grigio fra 0 e 254 (2 8) dove 0 significa spento e 254 nero.
Dunque, in ogni pixel dell’immagine ho un numero tra questo intervallo. Se invece è un’immagine a colori
prendo tre matrici ognuna con il canale rosso, verde e blu e indico da 0 a 254 quando proporzione di ogni
colore c’è, faccio poi sintesi additiva e ottengo il colore finale come somma dei pixel. Ogni pixel diventa una
tripletta di tre valori separati da virgola (x,y)=(R,V,B) rappresentabile in 3 bit.
Anche il suono che è un’onda meccanica deve essere digitalizzato. Il che significa che l’onda sonora che esiste
nel continuo potenzialmente in maniera infinita con tutti i valori infiniti possibili in ogni istante di tempo, deve
entrare nel mondo finito rappresentandola in un insieme di numeri finiti, ovvero discretizzandola. Devo
discretizzare il tempo perché non posso rappresentare un numero infinito di punti di un’onda ma devo
rappresentarne uno ogni tot tempo, intervallo che prende il nome di tempo di campionamento, e devo
discretizzare il valore rappresentandolo con un numero reale più o meno uguale. L’onda digitalizzata diventa
un array dove l’indice rappresenta il tempo in cui è stata misurata e il valore rappresenta il suono campionato.
Dunque, anche il suono campionato viene rappresentato con valori numerici su un alfabeto discreto.
Data una specifica posizione pone il bit a 1 se i bit di entrambe le 0101 & 1100 =
a&b AND
variabili sono 1. Viene fatto l’and dei singoli bit allineati. 0100
Data una specifica posizione pone il bit a 1 se almeno un bit è pari 0101 | 1100 =
a | b OR
a1 1101
Data una specifica posizione pone il bit a 1 se uno e un solo bit è 0101 ^ 1100 =
a ^ b XOR
pari a 1 1001
Bit shift a destra con Sposta i bit di n a destra di x posizioni, 0101 >> 1 = 0010
n >> x
mantenimento del segno inserendo il bit di segno a sx 1101 >> 1 = 1110
Bit shift a destra con Sposta i bit di n a destra di x posizioni, 0101 >>> 1 = 0010
n >>> x
inserimento di 0 inserendo 0 a sinistra 1101 >>> 1 = 0110
ESERCIZI
ESERCIZIO: dati due interi a e b azzerare da a i bit settati a 1 in b
function trasf(n,b1,b2){
let n1=parseInt(n, b1)
return n1.toString(b2)
}
function complemento2(n){
return -a+1
}
c={v:1}
d={v:2}
function scambia(a,b){
a.v=a.v^b.v
b.v=a.v^b.v
a.v=a.v^b.v
console.log(a.v)
console.log(b.v)
}
function trasf(n,b1,b2){
let n1 = parseInt(n,b1)
return n1.toString(b2)
}
SCOPING
La dichiarazione di una variabile consiste nel definire il suo nome (identificatore). Se una variabile non viene
dichiarata il nome non è disponibile perché l’interprete non sa che semantica dare ad una cosa che non è
dichiarata.
L’inizializzazione di una variabile consiste nell’assegnarle un valore per la prima volta.
La dichiarazione non implica inizializzazione e una dichiarazione può essere fatta sia con che senza
inizializzazione. Se dichiarate una variabile ma non la inizializzate essa diventa disponibile come nome legale
all’interno del codice ma ha come valore associato undefined.
Le funzioni sono valori e tutto ciò che è un valore può essere assegnato ad una variabile; dunque, una
dichiarazione di funzione con function è un modo diverso di scrivere una dichiarazione di variabile e
inizializzarla con un valore di tipo funzione.
VISIBILITÀ
Un nome dichiarato è visibile, ovvero è utilizzabile come stringa legale del linguaggio, in certe parti del
programma più o meno ampie a seconda di dove è stato dichiarato e di quale tipo di dichiarazione viene usata.
JS prevede tre scope o ambiti di visibilità:
console.log(“benvenuto”)
n = 1
function map(f,a) {
let r=[]
for (var i=0;i<a.length;i++) {
r.push(f(n,a[i]))
n= 1-n
}
return r
}
Const aa = [4,7,12,3,22,1,6]
Var t = map(mul, aa)
console.log(t)
▪ SCOPE GLOBALE
Le dichiarazioni fatte fuori da qualunque funzione avvengono nello scope globale. Queste dichiarazioni
sono visibili in tutto il programma, sia fuori che dentro le funzioni o blocchi. Per dichiarare una variabile
globale si può usare var, const, let, function o lasciare la dichiarazione implicita.
Le variabili dichiarate implicitamente nel programma senza usare le parole chiave, si intendono
implicitamente dichiarate nello scope globale ovunque siano dichiarate.
Notare che function map e function mul sono due dichiarazioni di funzioni definite nello scope globale,
dunque, le parole map e mul saranno disponibili in tutto il codice. Posso anche usare mul dentro map
anche se dichiarata dopo.
ESEM La variabile n=1 che non viene neanche dichiarata con var o let, ma implicitamente, fa si che la
variabile n abbia visibilità o scope globale. Hanno scope globale anche le dichiarazioni delle
due funzioni, aa e t.
▪ SCOPE DI FUNZIONE
Ogni funzione definisce il proprio scope di funzione, ovvero una visibilità ristretta al corpo della
funzione. I parametri formali si intendono dichiarati nello scope di funzione ma il nome della funzione
invece è nello scope globale. Le dichiarazioni fatte all’interno di una funzione con var, let, const sono
visibili nello scope di quella funzione ma non fuori.
Quando dichiaro una funzione creo due scope uno di funzione e uno di blocco.
ESEM I parametri formali f e a della funzione map è equivalente a una dichiarazione all’interno della
funzione delle due variabili che sono visibili in tutto il blocco della funzione. Quando successivamente
dichiaro la funzione mul che ha parametri formali a e b, il parametro a è diverso dal parametro a in map
perché sono variabili con scope di funzione.
▪ SCOPE DI BLOCCO
Ogni blocco (comandi racchiusi tra {}) definisce il proprio scope di blocco; in particolare il corpo di una
funzione è un blocco. Un blocco può essere contenuto in un altro blocco. Le dichiarazioni all’interno di
un for si considerano fatte nel suo blocco, compresa la dichiarazione della sua guardia e non sono
dunque disponibili al di fuori del for.
Le dichiarazioni fatte all’interno di un blocco con let o const sono visibili nello scope di quel blocco ma
non fuori. Se invece dichiariamo all’interno di un blocco con var la variabile viene automaticamente
promossa allo scope di funzione.
Var ha un comportamento leggermente diverso rispetto a let perché promuove la dichiarazione locale
al blocco all’ambiente esterno della funzione.
ESEM La variabile i dichiarata con var nella guardia del for è visibile dentro il ciclo e anche quando esso
finisce. Se fosse stata dichiarata con let sarebbe stata visibile dentro il ciclo e basta.
ANNIDAMENTI E SHADOWING
Si noti che gli scope sono contenuti uno nell’altro. Lo scope globale contiene 0 o più scope di funzione e 0 o più
scope di blocco. Ogni scope di funzione contiene 0 o più scope di funzione e 1 o più scope di blocco. Ogni
scope di blocco contiene 0 o più scope di funzione e 0 o più scope di blocco. Da dentro uno scope si vedono gli
scope esterni ma da quelli esterno non possiamo guardare a quelli interni.
Quando JS incontra una dichiarazione di un identificatore definisce una nuova variabile con quel nome e
inserisce il nome del blocco corrispondente. Quando JS incontra un riferimento a un identificatore lo interpreta
come un riferimento alla dichiarazione più interna con lo stesso nome, partendo dalla posizione in cui incontra
il riferimento.
Quando sono dentro la funzione map e mi riferisco al simbolo f per prima cosa cerco f all’interno della
funzione se non lo trovo salto fuori e vedo se è definito nella scatola esterna.
La regola del “dall’interno all’esterno” fa sì che sia possibile ri-usare lo stesso nome all’interno di uno scope
senza troppi problemi in quanto la variabile esterno non sarà più visibile perché quella interna con lo stesso
nome la copre: shadowing. I nomi dichiarati dentro uno scope sono locali o privati allo scope.
SOLLEVAMENTO O HOISTING
Le dichiarazioni, ovunque si trovino all’interno di uno scope, sono implicitamente spostate all’inizio del loro
scope (hoisting). Questo è il motivo per cui possiamo utilizzare la funzione mul all’interno di map anche se mul
è dichiarata dopo rispetto a map.
Tuttavia, le inizializzazioni rimangono dove sono scritte quindi la variabile prima della riga in cui è dichiarata
sarà visibile ma non inizializzata (undefined nel caso di var).
Eccezionalmente per le funzioni dichiarate con function, anche l’inizializzazione si considera all’inizio dello
scope
CLOSURE
Function è un tipo di dato come tutti gli altri in JS. Possiamo usare una funzione in un’espressione o comando,
possiamo passare una funzione come parametro a un’altra funzione e restituire una funzione da un’altra
funzione.
Closure: lo scope attivo durante la dichiarazione della funzione rimane visibile alla funzione restituita anche se
la funzione padre finisce.
RIASSUNTO
▪ JS ha scope globale, di funzione e di blocco
▪ C’è un solo scope globale ma gli scope di funzione e di blocco possono essere annidati uno nell’altro
▪ Le dichiarazioni con let e const hanno scope di blocco, o di funzione, o globale (vale lo scope più
interno in cui sono scritte)
▪ Le dichiarazioni con var hanno scope di funzione o globale (vale lo scope più interno in cui sono scritte)
▪ Le variabili non dichiarate hanno scope globale
▪ Le dichiarazioni con function si comportano come var ma si portano dietro l’inizializzazione in caso di
hosting
INSIEMI
Un insieme è un raggruppamento di elementi di qualsiasi tipo che può essere individuato mediante una
caratteristica comune agli elementi che vi appartengono oppure per semplice elencazione degli elementi
dell’insieme.
Si tratta di una struttura dati dove mettere elementi tutti distinti tra loro, dove non conta l’ordine e la
molteplicità e dove non conta l’indice ma solo l’appartenenza o meno dell’elemento.
Gli insiemi sono molto diversi dagli array dove posso aggiungere, ripetere gli elementi e dove conta l’ordine
mentre c’è una similitudine con gli oggetti perché ragionando in termini delle chiavi sono in grado di elencarle,
vedere se è definita oppure no nell’oggetto, e siccome indicizzano i campi se aggiungo un chiave che già
esistente cambio il valore di quella chiave stessa ma non la aggiungo più volte.
Un generico insieme S di elementi può essere realizzato sfruttando le chiavi degli oggetti JS che saranno i
nostri elementi dell’insieme a cui associo un valore predefinito in quanto tanto questa informazione verrà
persa. Posso dunque usare tutti i comandi degli oggetti per vedere se un elemento (chiave) è in un oggetto, per
scorrere gli elementi, per aggiungere elementi senza inserirli due volte se già presenti.
ESEM L’insieme S:{Pippo, Pluto, Topolino} può essere rappresentato con l’oggetto S={Pippo:1, Pluto: 1,
Topolino: 1}
ESERCIZI
ESERCIZIO: funzione che verifica se val appartiene a S
function contains(S,val) {
return (val in S)
}
function insert(S,val) {
S[val] = 1
}
function remove(S,val) {
delete S[val]
}
function subset(A,B) {
for (let a in A) {
if (!(a in B)) return false
}
return true
}
function equal(A,B) {
return subset(A,B) && subset(B,A)
}
function subtract(A,B) {
let S = {}
for (a in A) {
if (!(a in B)) insert(S,a)
}
return S
}
function union(A,B) {
let U = {}
for (a in A) insert(U,a)
for (b in B) insert(U,b)
return U
}
function cardinality(A) {
let i = 0
for (a in A) i++
return i
}
ESERCIZIO: funzione che calcola la similarità di Jaccard tra due insiemi (jaccard(A,B) = | A ∩ B | / | A U B |)
function jaccard(A,B) {
return cardinality(intersection(A,B)) / cardinality(union(A,B))
}
ALBERI BINARI
Un albero è un grafo connesso aciclico. I nodi di grado 1 sono detti foglie, esiste un nodo senza genitori, la
radice, e poi abbiamo i nodi interni di grado maggiore di 1. Ogni nodo interno ha 1 o più figli che consideriamo
ordinati. In un albero binario i nodi interni hanno al massimo 2 figli (arietà 2). Si possono definire sottoalberi
sinistri e destri, estrazioni di parti dell’albero che hanno il nodo destro o sinistro come radice.
Ad ogni nodo associamo un qualche valore (o etichetta) scomponendo l’informazione in modo organizzato.
Quando creo la rappresentazione di un albero devo definire tutti i nodi e per ogni nodo che valore contiene
valore. Si utilizzano quindi i dizionari.
Possiamo rappresentare un nodo come un oggetto con tre chiavi predefinite che definiscono la struttura del
nodo: la chiave val contiene il valore dell’etichetta del nodo, la chiave sx che contiene il figlio sinistro del nodo
che è a sua volta un oggetto e la chiave dx che contiene il figlio destro del nodo; se i figli sono assenti anche la
chiave sarà assente o conterrà null. L’assenza di entrambi i sottoalberi sinistro e destro è la condizione per
identificare una foglia. Quando si parla di alberi si parla quindi i oggetti annidati. Dentro al campo val ci
possono mettere un qualunque tipo di valore (numero, stringa, booleano, funzioni, array e oggetti) ed i nodi di
uno steso albero possono avere anche tipi diversi.
ESEM Posso definire prima due variabili una per ognuno degli oggetti foglia che non hanno figli.
let pino={val: “Giuseppe”}
let gigi={val: “Luigi”}
Poi definisco l’oggetto radice che contiene i due oggetti foglia
let ciccio={val:”Francesco”, sx: pino, dx: gigi}
Gli alberi sono naturalmente una struttura dati ricorsiva, ovvero definisce un tipo di dato con il tipo stesso,
perché un albero è un nodo che contiene altri nodi al suo interno fino ad arrivare a nodi senza figli. Se la
struttura dati è ricorsiva anche il codice che si scrive per processarla deve essere ricorsivo.
ESERCIZI
let T={val:7, let T={val: 1, let T={val:5,
sx:{val: 4, sx: {val: 2}, sx:{val:10,
sx: {val: 3}, dx: {val: 3, dx:{val:15,
dx: {val:12, dx: {val: 6}}} sx:{val:34,
sx: {val: 4, dx:{val:2,
sx:{val:8}, dx:{val:33}}},
dx:{val: 3}}}}, dx:{val:45,
dx:{val: 11, sx:{val:-10,
dx: {val: 1}, dx:{val:2}
sx: {val:9, },
sx: {val: 6}}}} dx:{val:8,
dx:{val: 12,
dx:{val: 5}}}}}}}
ESERCIZIO: massimo
function minNode(tree) {
if (tree === null){
return Infinity;
}
return Math.min(tree.val, Math.min(minNode(tree.sx), minNode(tree.dx)))
}
function sommaT(tree){
if (tree==undefined) return 0
return tree.val + sommaT(tree.sx) + sommaT(tree.dx)
}
function isInT(tree,x){
if (tree==undefined) return false
return (tree.val == x) || isInT(tree.sx,x) || isInT(tree.dx,x)
}
function size(tree){
if(tree==undefined) return 0
let c=1
return c+size(tree.sx)+size(tree.dx)
}
function stampaAlbero(tree){
printTree(tree, "")
}
function swap(tree){
let tmp = tree.dx
tree.dx = tree.sx
tree.sx = tmp
}
function prune(tree,val){
if ((tree) && (tree.val == val)) {
delete tree.sx
delete tree.dx
} else{
if (tree) {
prune(tree.sx,val)
prune(tree.dx,val)
}
}
}
function update(tree,fun){
if(tree){
tree.val=fun(tree.val)
update(tree.sx,fun)
update(tree.dx,fun)
}
return tree
}
ESERCIZIO: stampa i valori dei nodi attraversati implementando la seguente visita: se il valore di un nodo è
pari, visita il figlio sinistro; altrimenti, visita il figlio destro
function even_odd_visit(tree){
if(tree){
console.log(tree.val)
if(tree.val%2==0) even_odd_visit(tree.sx)
else even_odd_visit(tree.dx)
}
}
ESERCIZIO: si scriva una funzione che, dato un albero binario che rappresenta un'espressione, restituisca il
valore calcolato da tale espressione
function compute(expr) {
if (!expr.sx && !expr.dx)
return expr.val
let op = expr.val
let l_value = compute(expr.sx)
let r_value = compute(expr.dx)
switch (op) {
case "+":
return l_value + r_value
case "-":
return l_value - r_value
case "*":
return l_value * r_value
case "/":
return l_value / r_value
case "%":
return l_value % r_value
case "**":
return l_value ** r_value
default:
return NaN
}
}
ALBERI K-ARI
Possiamo considerare gli alberi k-ari con k>2 ovvero alberi in cui i nodi possono avere più di 2 figli. A questo
punto non possiamo più rappresentare i nodi come oggetti con tre chiavi (val, sx, dx) perché un nodo può avere
un numero qualsiasi di figli. Per questo motivo i nodi figli, ognuno rappresentato da un oggetto, vengono messi
in un array. Ognuno degli n figli di un nodo è la radice di un sottoalbero.
Un nodo in un albero k-ario è rappresentato da un oggetto con due chiavi: la chiave val indica il valore del nodo
e una chiave figli a cui è associato un array contenente tutti gli oggetti figli in maniera ordinata.
nodo={val: 5, figli: []}
Se il nodo in questione è una foglia e dunque non ha figli l’array è vuoto o è assente la chiave figli.
Notate che il numero di figli può crescere o diminuire arbitrariamente come la lunghezza di un array può
variare.
Possiamo sfruttare il fatto che il valore associato a un nodo può essere di qualunque tipo anche diverso da
nodo a nodo per creare strutture interessanti.
RAPPRESENTAZIONE LEFTCHILD-RIGTHSIBLING
Gli alberi k-ari si possono rappresentare a loro volta come alberi binari (evitando array di figli) facendo in modo
che il sottoalbero di sinistra di un nodo siano i suoi figli mentre il sottoalbero destro i suoi fratelli. Si
rappresenta come:
ESERCIZI
let T = {
val: 5,
figli: [
{ val: 10, figli: [] },
{ val: 15, figli: [
{ val: 34, figli: [] },
{ val: 2, figli: [] },
{ val: 33, figli: [] },
] },
{ val: 45, figli: [
{ val: -10, figli: [] },
{ val: 2, figli: [] },
] },
{ val: 8, figli: [] },
{ val: 12, figli: [] },
{ val: 5, figli: [] }
]
}
ESERCIZIO: massimo
function max(tree){
if (tree == {})
return undefined
if(!(tree.figli) || tree.figli==[]){
return tree.val
}
let max=tree.val
for(let i in tree.figli){
let mf=max(i)
if(mf>max) max=mf
}
return max
}
function size(t){
if(t==undefined) return 0
let c=1
if(t.figli){
for(let k of t.figli){
c+=size(k)
}
}
return c
}
function update(tree,fun){
tree.val=fun(tree.val)
for(let k of tree.figli){
update(k,fun)
}
}
function stampaAlbero(tree){
printTree(tree,"")
}
function somma(tree){
let somma=tree.val
if(!tree.figli || tree.figli.length==0){
return tree.val
}
for(let k of tree.figli){
somma+=somma(k)
}
return somma
}
function insert(set,x){
set[x]=true
}
function union(set1,set2){
let ins={}
for(let i in set1){
insert(ins, i)
}
for(let i in set2){
insert(ins, i)
}
return ins
}
function ins_foglie(tree){
let foglie={}
if(!tree.figli || tree.figli.length==0){
insert(foglie, tree.val)
return foglie
}
for(let k of tree.figli){
foglie=union(foglie, ins_foglie(k))
}
return foglie
}
function ins_nodiint(tree){
let nodi_int={}
for(let k of tree.figli){
if(k.figli && k.figli.length!=0){
insert(nodi_int, k.val)}
nodi_int=union(nodi_int, kins_nodiint(k))
}
return nodi_int
}
ALGORITMI DI ORDINAMENTO
RICERCA IN ARRAY CON RICERCA SEQUENZIALE E RICERCA BINARIA
1) Generiamo un array a di interi casuali di dimensione size (non gli viene passata la dimensione)
Crea un array e poi lo scorre mettendoci numeri interi casuali. Per ottenere numeri casuali
Math.random mi genera un numero tra 0 e 1 lo moltiplico per 100000 per avere un numero tra 0 e
100000 e poi lo arrotondo al numero superiore per eliminare le virgole.
function selectionSort(a) {
for (let i=0; i<a.length; i++) {
let min = a[i]
let minIndex = i
for (let j=i+1; j<a.length;j++) {
if (a[j] < min) {
min = a[j]
minIndex = j
}
}
let tmp = a[i]
a[i] = a[minIndex]
a[minIndex] = tmp
}
}
RICERCA SEQUENZIALE
function search(x,a) {
for (let i=0; i<a.length; i++)
if (a[i] == x)
return i
return -1
}
RICERCA BINARIA
function binarySearch(x,a,left,right) { function binarySearch(a,x) {
if (left == undefined) left = 0 let left = 0
if (right == undefined) right = a.length-1 let right = a.length-1
if (right < left) return -1 while(left <= right) {
let center = Math.round((left + right)/2) let center = Math.round((left + right)/2)
if (a[center] == x) if(a[center] == x)
return center return center
else if (a[center] > x) if(a[center] < x)
return binarySearch(x,a,left,center-1) left = center + 1
else else
return binarySearch(x,a,center+1,right) right = center - 1
} }
return -1
}
Mi conviene sempre ordinare l’array e applicare la ricerca binaria? Non sempre usare l’algoritmo più
efficiente è la scelta migliore. Se costruiamo noi l’array potremmo fare in modo che i valori vengono
inseriti già in modo ordinato in modo tale da non doverlo ordinare successivamente
Inoltre la complessià degli algoritmi O(f(n)) indica l’ordine di grandezza della complesità ma nella realtà f(n)
avrà una costante moltiplicativa che inclina più o meno della curva particolare rappresentata da O(f(n)). Tale
costante si può determinare con (num_elementi_array^2)/tempo_esecuzione. Usiamo questi accorgimenti per
confrontare gli algoritmi implementati con la loro complessità ideale.
INSERTIONSORT
function insertionSort(a) {
for (j = 1; j < a.length; j++) {
key = a[j];
i = j - 1;
while (i>-1 && a[i] > key) {
a[i+1] = a[i];
i = i - 1;
}
a[i+1] = key;
}
}
MERGESORT
Versione umana Versione di JS Versione Cormen
function merge(a1, function mergeJS(a1, a2){ function mergeCormen(a, p, q, r)
a2){ let sorted = []; {
let result = []; while (a1.length && a2.length) { n1 = q-p+1;
let i = 0; if (a1[0] < a2[0]) sorted.push(a1.shift()); n2 = r-q;
let j = 0; else sorted.push(a2.shift()); let L = [];
while(i < a1.length && }; let R = [];
j < a2.length){ return for (i = 0; i < n1; i ++) {
if(a1[i] > a2[j]) { sorted.concat(a1.slice().concat(a2.slice())); L[i] = a[p+i];
result.push(a2[j]); }; }
j++; for (j = 0; j < n2; j++) {
} else { function mergeSortJS(a) { R[j] = a[q+j+1];
result.push(a1[i]); if (a.length <= 1) return a; }
i++; let q = Math.floor(a.length / 2); L[n1] = Infinity;
} let L = mergeSortJS(a.slice(0, q)); R[n2] = Infinity;
} let R = mergeSortJS(a.slice(q)); i = 0;
while(i < a1.length){ return mergeJS(L, R); j = 0;
result.push(a1[i]); }; for (k = p; k <= r; k++) {
i++; if (L[i] <= R[j]) {
} a[k] = L[i];
while(j < a2.length){ i++;
result.push(a2[j]); } else {
j++; a[k] = R[j];
} j++
return result; }
} }
return a;
function mergeSort(a){ }
if(a.length <= 1) return
a; function
let q = mergeSortCormen0(a,p,r) {
Math.ceil(a.length / 2); if (p < r) {
let q = Math.floor((p + r) / 2);
mergeSortCormen0(a,p,q);
let L = mergeSortCormen0(a,q+1,r);
mergeSort(a.splice(0, mergeCormen(a,p,q,r);
q)); }
let R = }
mergeSort(a.splice(-
q)); function mergeSortCormen(a) {
return merge(L, R);
} mergeSortCormen0(a,0,a.length-
1)
}
Le tre versioni di Mergesort pur avendo la stessa complessità possono farci registrare tempi diversi. La più
veloce è quella scritta in versione umana. La versione del Cormen fa molti accessi in scrittura e in lettura in
memoria e questo è il motivo per cui all’aumentare della dimensione aumenta il tempo.
La versione in JS fa un po’ di accessi in lettura e praticamente 0 accessi di scrittura mentre la versione umana
fa pochissimi accessi sia in lettura che in scrittura.
QUICKSORT
function quickSortCormen0(a,p,r) {
if (p < r) {
q = partitionCormen(a,p,r);
quickSortCormen0(a,p,q-1);
quickSortCormen0(a,q+1,r);
}
}
function partitionCormen(a,p,r) {
x = a[r];
i = p - 1;
for (j = p; j < r; j++) {
if (a[j] <= x) {
i = i + 1;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
function quickSortCormen(a) {
quickSortCormen0(a,0,a.length-1);
}
STRUTTURE DATI CON ARRAY
Gli array in JS sono molo flessibili. Infatti mentre in C gli array sono un blocco di celle di memoria in JS gli array
sono dei dizionari con chiavi numeriche.
• Possono essere disomogenei
[ 1, 2, “pippo”, 3.1415, {x: 10, y:12}, 3 ]
• Possono essere sparsi ovvero presentare spazi vuoti
[ 1, 2, <empty>×4, 7 ]
• Possono avere chiavi non numeriche in quanto sotto l’implementazione degli array c’è un oggetto
var a=[1, 2]; a.pippo=3
a → [1, 2, pippo: 3] a.length → 2
• c’è una gestione implicita della lunghezza perché operando sul campo lunghezza posso modificare
l’array.
var a=[1, 2, 3]; a.length → 3
a.length=2; a → [1, 2]
a.length=4; a → [1, 2, <empty>×2 ]
TUPLE
Gli array possono essere utilizzati per implementare le tuple fissando la loro lunghezza. Una tupla è un insieme
ordinato di k elementi. La posizione all’interno delle tuple ha un significato.
La tupla <4, 1> può essere rappresentato dall’array t=[4,1]
Si tratta di una convenzione in cui il programmatore promette di usare sempre array di lunghezza 2 e sempre
con gli elementi corretti. Se ho una tupla t=[4,1] posso:
• assegnare separatamente gli elementi attraverso l’assegnamento destrutturante che prende una
struttura dati e assegna tutti o solo alcuni elementi a delle variabili.
[a, b] = t a→4 b→1
• chiamare una funzione passando una tupla come parametro formale semplicemente passando un
array
• restituire più risultati da una funzione restituendo una tupla (array) e poi destrutturandola in due
variabili.
LISTE
Gli array possono anche essere usati come liste ovvero come sequenze di elementi di lunghezza variabile. In
questo stile raramente si accede agli elementi tramite la loro posizione (per indice) ma si lavora in maniera
strutturale ovvero ricorsivamente. Le liste sono delle strutture ricorsive in quanto ogni lista è formata da un
elemento + una lista a sua volta formata da un elemento + una lista…
• Una lista vuota è una lista [ ]
• Un elemento seguito da una lista è una lista: [testa resto]
• Una lista seguita da un elemento è una lista: [resto coda]
Molte funzioni predefinite sugli array li trattano come liste e consentono di realizzare facilmente strutture dati
come code e pile. Queste funzioni permettono di eliminare e inserire elementi in posizioni precise:
▪ a.push(x)
▪ a.pop()
▪ a.shift()
▪ a.unshift(x)
Per realizzare una pila opero convenzionalmente con a.push() per inserire in coda e con a.pop() per estrarre
dalla coda (last in-first out). In realtà potrei anche utilizzare a.unshifth() per inserire dalla testa e shift per
rimuovere dalla testa.
Per realizzare una coda opero convenzionalmente con a.push() per aggiungere in coda e con a.shift() per
estrarre dalla testa. Il primo elemento ad essere entrato è il primo ad uscire (first in-first out). In realtà potrei
anche utilizzare a.unshift() per inserire in testa e a.pop() per estrarre dalla coda.
Se volessi rappresentare un’immagine quindi dire attraverso una matrice quali pixel colorare potrei fare un
array di array.
Questa è una matrice 4x3 dove gli elementi del primo array rappresentano le righe mentre gli elementi dei vari
array annidati, le colonne
A questo punto per ottenere l’immagine posso fare una funzione che inserisce i valori nella matrice
Se guardo gli array come liste ho bisogno di vederli ricorsivamente come strutture dati ricorsive composte da
una testa e un’altra lista (che al caso base è vuota). Per fare ciò possiamo utilizzare sulle liste l’assegnamento
destrutturante con spread.
Se l’array contiene tre elementi uso l’assegnamento destrutturante ed assegno il primo elemento alla prima
variabile, poi il secondo e così via fino a quando ho variabili o elementi nell’array. Se un elemento dell’array
non viene assegnato semplicemente rimane nell’array originale, al contrario se a una variabile non viene
assegnato un elemento assume il valore undefined o il valore assegnato di default.
La funzione spread invece mi permette di assegnare attraverso l’assegnamento destrutturante il primo
elemento dell’array (testa) ad una variabile e tutti il resto dell’array ad un’altra variabile come un array.
[testa, …resto] = [1, 2, 3, 4, 5] testa → 1 resto → [2, 3, 4, 5]
[testa, …resto] = [1] testa → 1 resto → [ ]
[testa, …resto] = [ ] testa → undefined resto → [ ]
Posso iterare questa procedura fino a quando non arrivo al caso base che può essere visto in due modi:
quando alla testa viene assegnato il valore undefined oppure quando resto è un array vuoto.
A questo punto possiamo scrivere degli algoritmi ricorsivi sugli array vedendoli come liste.
Con l’assegnamento destrutturante assegno a t il primo elemento dell’array e assegno a r il resto dell’array. La
funzione deve restituire 1 più len(r) fino a quando t==undefined o r==[ ] che è il caso base in cui non richiamo la
funzione.
function max(a) {
let [t, ...r] = a
if (t)
return Math.max(t,max(r))
else
return -Infinity
}
ALBERI
Combinando le convenzioni per tuple e liste possiamo rappresentare alberi binari o k-ari con gli array.
Se applico il modulo A.slice dall’inizio alla fine mi posso copiare l’array A in A1=[1,2,3,4]. A questo punto se io
modifico gli elementi di A1, queste modifiche non cambieranno l’array originale A. questo perché essendo A un
array di valori di tipo primitivo essi vengono copiati con una deep copia ovvero una copia fisica dei valori che
hanno un codice fiscale diverso dai valori all’interno dell’array originale.
Se applico il modulo B.slice dall’inizio alla fine mi posso copiare l’array di array B in B1={[1,2,3], [4,5,6], [7,8,9],
[10,11,12]]. A questo punto se io cambio l’elemento in posizione 0 di B1 ottengo:
ESEM B1[0]=10
B1={10, [4,5,6], [7,8,9], [10,11,12]}
B={[1,2,3], [4,5,6], [7,8,9], [10,11,12]}
Noto dunque che la modifica è avvenuta solo sull’array copia senza modificare l’array originale questo perché i
vari elementi dell’array B sono stati copiati con una deep copia e quindi indipendenti dall’array originale.
Al contrario se io cambio l’elemento in posizione 0 dell’elemento in posizione 0 di B1:
ESEM B1[0][0]=10
B1={[10,2,3], [4,5,6], [7,8,9], [10,11,12]}
B={[10,2,3], [4,5,6], [7,8,9], [10,11,12]}
Noto che la modifica non è avvenuta solo sull’array copia B1 ma che è stato modificato anche l’array originale.
Questo accade perché gli elementi all’interno degli elementi copia di B sono shadow copie ovvero non sono
copie fisiche ma per ottimizzare metto il riferimento ai valori originali dunque l’elemento B1[0][0] si riferisce
esattamente all’elemento B[0][0], hanno lo stesso codice fiscale, si riferiscono alla stessa casella di memoria.
Per fare una copia fisica di elementi di tipo non primitivo è necessario farlo a mano elemento per elemento.
ASSEGNAMENTI DESTRUTTURANTI
La destrutturazione è un’operazione che permette di prendere un tipo strutturato (array strutturato in posizioni
denotate da indici, oggetto strutturato in campi denotati da chiavi) e di spacchettare le sue componenti in
delle variabili indipendenti. JS permette di usare la notazione di un letterale array o oggetto in cui al posto dei
valori si indicano dei nomi di variabili a sinistra dell’operatore per questo tipo di assegnamento.
NEGLI ARRAY
1. Negli array l’assegnamento destrutturante mette dentro le varie variabili i singoli elementi dell’array
ESEM A=[4,7,1]
[a, b] = A a→4 b→7
[a,b,c,d]=A a→4 b→7 c→1 d → undefined
2. Allo stesso modo per cui se faccio una chiamata alla funzione in cui mi scordo i parametri al parametro
non definito viene assegnato undefined oppure un valore di default (f(x, y=3)) posso anche in caso di
assegnamento destrutturante passare un valore di default.
Posso farlo anche qui ovvero se non ci sono gli do un valore di default:
3. E anche possibile saltare degli elementi all’interno dell’array per assegnarne solo alcuni:
4. Posso fare un assegnamento destrutturante con spreed che mi permette di spacchettare il primo
elemento dell’array in una variabile e il resto dell’array in un’altra:
5. Posso usare l’assegnamento destrutturante per scambiare i valori x e y senza variabili intermedie
mettendoli prima all’interno di un array in ordine e poi assegnandoli alle variabili in ordine inverso.
ESEM [ y, x ] = [ x, y ]
ESEM [ x, y ] = f(“pippo”,3)
Negli array le cose vengono impacchettate per posizione (indice) e anche l’assegnamento destrutturante
avviene per posizione. L’assegnamento destrutturante può essere utilizzato anche sugli oggetti, tuttavia per gli
oggetti dobbiamo lavorare sulle chiavi che dobbiamo quindi conoscere.
NEGLI OGGETTI
1. {a,c}=O a → 35 c → true
Cerca in O le chiavi a e c e poi assegna ad una variabile a il valore della chiave a e ad una variabile c il
valore della chiave c
2. {a,d}=O a → 35 d → undefined
Cerca in O le chiavi a e d, per a procede come prima mentre se non trova la chiave d assegna alla
variabile d il valore undefined.
3. {a, d=2 } = O a → 35 d→2
Oppure è possibile mettere un valore di default nel caso non venisse trovata la chiave
6. {x, y} = f(“pluto”, 3)
Posso destrutturare un oggetto che è il valore di ritorno di una funzione assegnando i vari valori restituiti
in variabili diverse.
In alcuni casi, il simbolo { potrebbe essere interpretato come l’inizio di un blocco. Per evitare confusione, si
può scrivere l’assegnamento fra parentesi tonde.
✔ let { a, c } = O
✘ { a, c } = O
✔ ( { a, c } = O )
Inoltre, devo dichiarare l’oggetto che contiene le variabili. Se a e c esistono di già all’interno del programma
allora racchiudo solo fra tonde.
La destrutturazione sugli oggetti consente di avere funzioni di libreria con molti parametri non tutti obbligatori. I
parametri obbligatori verranno chiamati per posizione nella chiamata della funzione, mentre quelli facoltativi
si troveranno fra i parametri formali in un oggetto e verranno chiamati nella chiamata della funzione non per
posizione ma con il loro nome e un valore altrimenti assumeranno il valore di default.
disegna(5,8, {colore=”blu”})
disegna(3, 2)
disegna(5,8,{colore=”rosso”, bordo=2})
disegna(0,1,stile)
VALORI DI DEFAULT
Come abbiamo visto i valori di default si usano:
• Nella dichiarazione di funzione per dare un valore di default a parametri formali
• Negli assegnamenti destrutturanti per dare un valore agli elementi assenti (per numero negli array e per
nome negli oggetti)
In generale quando normalmente a una variabile sarebbe assegnato undefined se nella dichiarazione c’è un
valore di default si assegna quel valore
OPERATORE SPREAD
L’operatore di spread è denotato da tre puntini consecutivi. Ha il significato generale di trasformare una
sequenza di elementi singoli in un singolo dato strutturato e viceversa
A è un array:
• f(...A) chiama la funzione f passandogli gli elementi di a come parametri formali singoli. L’array
viene spacchettato nelle sue componenti che diventano elementi distinti passati a f
come parametri singoli.
• p={…q, c:3} definiamo un nuovo letterale oggetto p dove vengono copiate (copia shadow quindi per
riferimento se non primitivi) tutte le chiavi e i valori associati di q. Se l’oggetto q non
possiede una chiave c questa viene aggiunta con valore 3 altrimenti la chiave c di q viene
sovrascritta e assume valore 3.
• p ={ c: 3, ...q} definiamo un nuovo letterale oggetto p dove inseriamo la chiave c con valore 3 e poi
copiamo tutte le chiavi con corrispettivi valori di q. Se q contiene una chiave c questa
sovrascriverà il valore 3.
• B={1,2,…A,6] array contenente gli elementi 1,2, tutti gli elementi di A e poi 6
• C=[…a,..b] concatenazione dei due array (copia shadow). Se A e B contengono valori di tipo
primitivo
abbiamo delle copie fisiche altrimenti se contengono array o oggetti abbiamo i
riferimenti agli oggetti originali.
• R=[…o1,..02] unione dei due oggetti. Copiamo prima tutte le chiavi di o1 e poi tutte le chiavi di 02 il
che
significa che se ci sono chiavi in comune alla fine queste avranno i valori che avevano in
o2.
ESPERIMENTI SUI CONCETTI DI ANALISI
DERIVAZIONE NUMERICA
Consiste nel calcolo del valore della derivata in un punto. Il più delle volte non sono interessato a scrivere la
funzione derivata ma sono interessato a calcolare la derivata in un punto. Data una funzione f ed un punto (x,
f(x)) il valore della derivata f’ in x è:
f’(x)=lim (f(x)-f(x0))/(x-x0) per x che tende a x0
Per calcolare la derivazione numerica in JS basta riscrivere la definizione matematica in linguaggio JS:function
function valDerivataDx(f,x,epsilon=0.0001){
return (f(x+epsilon)-f(x))/epsilon
}
Abbiamo l’assegnamento di un valore di default per il parametro formale epsilon e possiamo aumentare la
precisione della derivata andando a ridurre tale valore. Questa funzione ci restituisce il valore numerico della
derivata destra della funzione in un punto.
Data una funzione f ed un punto (x,f(x)) la derivata f’ in x esiste se il valore della derivata dx (f’(x)_dx=lim (f(x+E)-
f(x))/E per E che tende a 0) e la derivata sinistra (f’(x)_sx=lim (f(x)-f(x-E))/E per E che tende a 0) coincidono.
function valDerivataSx(f,x,epsilon=0.0001){
return (f(x)-f(x-epsilon))/epsilon
}
Tuttavia, se io uguaglio queste due derivate esse possono risultare diverse perché abbiamo
un’approssimazione nei calcoli tale per cui entrambe le derivate hanno un errore che raramente le rende
uguali. Dunque, nell’uguaglianza fra queste due derivate dobbiamo tener conto dell’errore:
function valDerivata(f,x,epsilon=0.0001){
var dx=valDerivataDx(f,x,epsilon)
var sx=valDerivataSx(f,x,epsilon)
if(Math.abs(dx-sx)<0,01) return sx
}
Adesso ho una funzione che mi restituisce la derivata in un punto ovvero f’(x) con x fissato ad un valore e voglio
una funzione che dato f mi restituisce f’ ovvero la funzione della derivata numerica in ogni punto.
function funDerivata(f,epsilon=0.0001){
return x=>valDerivata(f,x,epsilon)
}
La funzione restituita mantiene l’accesso alle variabili f e epsilon che sono passate a funDerivata anche se non
sono tra i suoi parametri formali. F e epsilon sono chiuse nello scope della funzione restituita da funDerivata
(chiusura lessicale).
function derN(f,N=1,eps=0.00001){
if (N==0) return f
return derN(funDerivata(f,epsilon),N--,epsilon)
}
DERIVAZIONE SIMBOLICA
Spesso però quando si fa il calcolo della derivata non si calcola il limite ma si lavora sui simboli.
Valori -5 0 4 1
Se dovessi calcolare il polinomio in certo punto x dovrei scorrere l’array e sommare tutti i prodotti fra x elevato
all’indice per il coefficiente.
function polyinx(p,x){
var ris=0
for(var i=0; i<p.length; i++){
ris+=(x**i)*p[i]
}
return ris
}
La regola di derivazione dei polinomi dice di moltiplicare il coefficiente per l’esponente e di ridurre l’esponente
di 1.
-5 0 4 1 / 0 8 3
function derPoly(p){
function modifica(e,i){
let ne=e*(i)
return ne
}
let ris=p.map(modifica)
ris.shift()
return ris
}
REGOLE DI DERIVAZIONE
La derivata, dunque, è definita ricorsivamente e il caso base sono i polinomi o i numeri. Dunque, davanti alla
derivata di una funzione complessa si richiama la funzione derivata in accordo con le regole sopra fino ad
arrivare ad un polinomio o ad una costante
Ci serve dunque un modo per rappresentare le funzioni in maniera simbolica e si utilizza la codifica prefissa
ovvero una notazione composta da una tupla di tre posizioni dove il primo elemento è l’operatore e gli elementi
che seguono gli operandi. Si dice essere una tupla perché si assume un ordinamento specifico al suo interno.
essa potrebbe anche essere composta soltanto da due elementi nel caso l’operatore sia unario. Ho dunque
una struttura ricorsiva, la tupla, su cui posso operare ricorsivamente.
a+b => [“+”, a, b]
(a * b) + c => [“+”, [“*”, a, b], c]
x^3 => [“^”, x, 3]
Per prima cosa si definiscono delle costanti come che cosa è un numero, che cosa è una variabile, la
negazione per distinguerla dalla sottrazione e così via…
Successivamente la funzione derivata prende una generica espressione “e” rappresentata come una tupla
(che può essere formata a sua volta da tuple) e inizia a lavorarci ricorsivamente.
Attraverso un assegnamento destrutturante estraggo i tre elementi della tupla che per definizione sono in
ordine e li assegno a tre variabili op, x e y. Nel caso in cui la tupla contenesse soltanto due elementi y
risulterebbe undefined.
A questo punto proseguo in modo differente a seconda dell’operatore che mi trovo davanti.
• Nel caso in cui l’operatore sia un numero allora quello che segue è la denotazione di una costante e la
derivata di una costante è 0. Dunque, restituisce una tupla che afferma che la derivata di una costante
è lei stessa una costante e specificatamente 0
• Nel caso in cui l’operatore sia x restituisce una tupla che afferma che la derivata è una costante e
specificatamente 1
• Nel caso in cui l’operatore fosse la negazione restituisce una tupla che afferma che la derivata è la
negazione della derivata dell’argomento facendo una chiamata ricorsiva.
• …
PASSAGGIO PER VALORE O RIFERIMENTO
JS prevede valori di tipi base come numeri, booleani e stringhe e valori di tipo complesso come gli array, gli
oggetti e le funzioni (che però sono tipi primitivi). Nella maggior parte dei contesti il valore effettivo a cui si fa
riferimento tramite un identificatore non fa differenza ma tuttavia è importante ricordare che i tipi base hanno
una semantica per valore mentre i tipi complessi hanno una semantica per riferimento.
• Quando si parla di una semantica per valore si intende che le variabili contengono direttamente il
valore e dunque l’assegnamento copia il valore mentre l’uguaglianza confronta il valore.
• Quando si parla invece di una semantica per riferimento si intende che le variabili contengono un
riferimento a un’area della memoria dove è memorizzato il valore. Consiste essenzialmente nell’usare
indirizzi di memoria come valori e nell’avere operazioni di indirizzamento implicito (o.chiave, o[chiave],
a[indice],funzione(arg)) per accedere ai valori contenuti nella memoria all’indirizzo dato.
L’assegnamento copia, dunque, il riferimento e non i contenuti dell’area di memoria e allo stesso
modo l’uguaglianza confronta il riferimento e non i contenuti. La semantica per riferimento permette la
condivisione di oggetti in memoria e le modifiche apportate tramite un riferimento sono visibili
attraverso altri riferimenti allo stesso oggetto.
In particolare, questo è vero per gli argomenti passati alle funzioni. JS usa il passaggio per valore degli
argomenti. Per il corpo della funzione il parametro formale è una variabile locale, inizializzata con una copia
del valore del parametro attuale, oppure con il valore di default se indicato nella dichiarazione e se la chiamata
non comprendeva quel parametro. Questo implica che la funzione non può modificare il valore di una variabile
usata nella sua invocazione.
Però se il valore è un riferimento, una funzione può modificare il valore di tipo complesso riferito al riferimento.
Il concetto di riferimento si applica anche alle strutture dati. Finora abbiamo lavorato con strutture dati in cui
ogni oggetto appariva in un solo posto. Per esempio, gli alberi godono di questa proprietà: ogni nodo compare
come valore solo nel campo sx o nel campo dx del padre. Invece una volta chiarito l’uso del riferimento
possiamo definire strutture dati in cui lo stesso oggetto compaia in più campi per esempio i grafi.
ESEM Usiamo la definizione classica dei grafi ovvero G= <N,E> dove N è un insieme di nodi (oggetto) e E è un
insieme di archi incluso in NxN. Rappresentiamo un grafo come un oggetto con un campo nodi e un
campo archi: G = {nodi: N, archi: E}
Rappresentiamo un nodo come un oggetto con un campo val per ospitare un’etichetta: n1 = {val: x}
Rappresentiamo un arco come un oggetto con un campo da e un campo a: a = {da: n1, a: n2}
Per esprimere il fatto che gli archi b,c e d insistono sullo stesso nodo è indispensabile fare affidamento
ai riferimenti. La via più semplice è quella di usare delle variabili per riferire i vari nodi e archi.
var n1 = {val: 1}, n2 = {val: 2}, n3 = {val: 3}, n4 = {val: 4}, n5 = {val: 5}
var a = {da: n1, a: n2}, b = {da: n1, a: n3}, c = {da: n3, a: n5}, d = {da: n3, a: n4}, e = {da: n4, a: n5},
f = {da: n5, a: n2}
G = {nodi: [n1, n2, n3, n4, n5], archi: [a, b, c, d, e, f]}
function nodoGradoMassimo(G){
var nodo_max, grado_max= 0
for(var n of G.nodi){
var grado_nodo=0
for (var e of G.archi){
if(e.da==n) grado_nodo++
if(e.a==n) geado_nodo++
}
if (grado_nodo>grado_max){
grado_max=grado_nodo
nodo_max=n
}
}
return nodo_max
}
COSTRUTTORI
Quando si usano oggetti complicati è usuale (e comodo) usare una funzione per costruire l’oggetto, spesso
partendo da alcuni valori che lo descrivono. In questo modo facendo costruire gli oggetti tutte le volte che
serve da una sola funzione siamo sicuri di avere sempre i campi giusti. Queste funzioni usano il fatto che ogni
volta che viene valutato un letterale di tipo oggetto viene allocato un suo riferimento diverso da tutti quelli già
esistenti. Spesso queste funzioni prevedono argomenti di default oppure effettuano dei calcoli o dei controlli
prima di inizializzare l’oggetto.
L’operazione di creare un nuovo oggetto e assegnare le sue proprietà e i suoi metodi all’interno di una funzione
costruttore in modo da costruire oggetti tutti con la stessa struttura è molto comune. Tanto comune che JS
prevede un modo speciale per farlo attraverso l’operatore new.
L’operatore new crea un nuovo oggetto vuoto, chiama la funzione costruttore passando come this l’oggetto
vuoto appena creato, più i parametri indicati e restituisce l’oggetto inizializzato.
In questa nuovo modo di scrivere i costruttori non c’è più un letterale di tipo oggetto per creare il nuovo oggetto
perché la creazione viene fatta da new e non c’è un return perché new implicitamente restituisce l’oggetto
creato da new e inizializzato da Persona.
Per convenzione le funzioni-costruttore destinate a essere usate con new iniziano per lettera maiuscola.
METODI
Inoltre, un oggetto può avere proprietà ovvero coppie chiave-valore con valori di tipo funzione. Conosciamo
due modi per denotare un valore funzione: con => o con functon. Tuttavia, per il caso particolare di funzioni
definite come proprietà di un oggetto esiste una terza sintassi abbreviata.
Finora abbiamo utilizzato funzioni speciali come Console.log(), Math.max e così via. Console e Math sono in
effetti variabili globali pre-dichiarate nell’ambiente di esecuzione. Si tratta di variabili di tipo oggetto e le
funzioni speciali sopracitate sono proprietà di questi oggetti con valori di tipo funzione. Naturalmente anche gli
oggetti che definiamo noi possono avere proprietà di tipo funzione.
A questi oggetti (Math e Console) possiamo volendo aggiungere anche nostre funzioni nello stesso modo in cui
normalmente impostiamo proprietà di un qualunque altro oggetto. Queste nuove funzioni possono essere poi
usate normalmente in tutto il resto del programma esattamente come quelle definite.
Le funzioni di Math sono funzioni pure ovvero non hanno effetti collaterali, si limitano a calcolare il risultato
che dipende solo dagli argomenti. È però assai più comune il caso in cui una funzione debba modificare
l’oggetto a cui si riferisce.
• Math.floor(…) difetto
• Math.round() eccesso
• Math.abs()
• Math.exp
• Math.max(…)
• Math.min(…)
• Math.random()
• Math.sqrt()
Possiamo usare le funzioni costruttori per assicurarci che tutti gli oggetti di una certa tipologia abbiamo i
metodi che ci servono. Più proprietà e metodi vogliamo aggiungere a un oggetto più è utile definire una
funzione per assicurarsi che tutti gli oggetti della stessa tipologia siano costruiti esattamente allo stesso
modo.
La funzione figli prende come argomento un nodo n che è un oggetto, successivamente prende
l’insieme degli archi del grafo che è un array composto da oggetti e crea con il metodo filter un nuovo
array in cui inserisce soltanto gli archi che partono dal nodo n. Il predicato del metodo filter è una
funzione a cui si passa un argomento con l’assegnamento destrutturante: si cerca nell’oggetto passato
(in questo caso un singolo oggetto arco dell’array archi alla volta) la chiave “da” e si passa il suo valore
come parametro. Inoltre su questo nuovo array viene applicato il metodo map che crea un nuovo array
in cui per ogni nodo che parte da n si inserisce il nodo di destinazione. Anche l’argomento di map è
passato attraverso l’assegnamento destrutturante: si cerca all’interno dei singoli oggetti archi la chiave
“a” e si passa il suo valore come argomento della funzione.
Ogni oggetto ha un prototipo, che è un altro oggetto, tranne il prototipo dell’oggetto Object che non ha
prototipo. Quando si vuole leggere il valore di una proprietà di un oggetto si guarda se l’oggetto ha la chiave
cercata, se la chiave è presente il valore è quello della chiave nell’oggetto, se la chiave non è presente e
l’oggetto ha un prototipo si cerca la proprietà nel prototipo, se la chiave non è presente e l’oggetto non ha un
prototipo il risultato è undefined. Quando si vuole scrivere il valore di una proprietà di un oggetto, la chiave e il
valore vengono inseriti nell’oggetto eventualmente sovrascrivendo il valore precedente della stessa chiave
presente nel prototipo.
Dunque, i metodi mancanti si trovano nella catena dei prototipi dei nostri oggetti. Possiamo scoprire chi è il
prototipo di un oggetto o accedendo alla sua proprietà speciale o.__proto__ con due prefissi e due suffissi,
oppure invocando Object.getPrototypeOf(o)
LA CATENA DI PROTOTIPI
Da notare che l’enumerazione delle proprietà di un oggetto fatta con for(k in o){} restituisce solo le chiavi
proprie dell’oggetto quindi non quelle che vengono trovate nei prototipi. Lo stesso vale anche per i metodi
Object.keys(o), Object.entries(o), ecc… In questo modo le chiavi presenti nel prototipo sono leggibili se le
accedete ma non sono enumerabili e ciò è particolarmente comodo per array e dizionari.
Object.keys(o) restituisce un array i cui elementi sono le chiavi sotto forma di stringhe in ordine se numeriche
Object.values(o) restituisce un array i cui elementi sono i valori, se si usano chiavi numeriche li restituisce in
ordine
Object.entries(o) restituisce un array contenente array di due elementi chiave-valore come stringhe, se si
usano chiavi numeriche le restituisce in ordine
Se aggiungiamo un metodo a un particolare oggetto il metodo sarà disponibile solo per quell’oggetto. Se
invece aggiungiamo un metodo al prototipo di un oggetto come Persona allora il metodo sarà disponibile per
tutti gli oggetti che hanno lo stesso prototipo.
Tutte le funzioni e in particolare le funzioni costruttore hanno una proprietà che si chiama prototype
inizializzata automaticamente dal linguaggio quando si dichiara una funzione e che contiene un oggetto pronto
per fare da prototipo per gli oggetti inizializzati dalla funzione.
L’operatore new assegna automaticamente come prototipo dell’oggetto creato il prototype della sua funzione
costruttore. Ciò crea un vero legame permanente fra gli oggetti e i loro costruttori, praticamente un tipo.
La versione più recente di JS ha aggiunto un modo diverso di dichiarare le classi. Si tratta comunque solo di
sintassi più facile: in realtà tutta la componente orientata agli oggetti di JS è basata sul concetto di prototipo.
• Gli oggetti sono creati da new tramite funzioni-costruttori
• Le funzioni-costruttori definiscono implicitamente il prototipo di ogni oggetto creato
• Tutte le parti in comune di oggetti della stessa famiglia vengono implementate dai prototipi, le parti
proprie sono implementate da ogni oggetto individualmente
CLASSI
La sintassi con la dichiarazione class consente di definire funzioni-costruttore e metodi di un oggetto di
particolare tipo in maniera molto più compatta. All’interno dei metodi, incluso il costruttore, l’oggetto viene
manipolato e riferito da this. Le classi non sono altro che funzioni.
Tecnicamente Persona non è una classe (un concetto che non esiste in JS) ma una funzione come abbiamo
visto. La creazione di nuovi oggetti avviene dunque con new.
EREDITARIETÁ
Finora abbiamo visto le classi come dichiarazioni ma essendo esse delle funzioni, ovvero dei valori in JS, esiste
anche la variante espressione.
Abbiamo visto come per ogni oggetto sia definita la sua catena dei prototipi. Quando si accede in lettura a una
proprietà se la chiave non è definita nell’oggetto, si va a cercare nel prototipo e così via ricorsivamente.
È anche possibile impostare il prototipo di una nuova classe a un’altra classe. Grazie al meccanismo dei
prototipi, gli oggetti della nuova classe avranno anche tutti i metodi definiti dalla classe che viene estesa. In più
la sottoclasse può aggiungere ulteriori metodi. Solo le istanze della sottoclasse avranno i metodi aggiunti.
Capita spesso che il codice in una sottoclasse debba fare riferimento ai metodi della sua superclasse per
completarlo o aggiungere caratteristiche. La parola chiave super è un riferimento alla superclasse di un
oggetto come this è un riferimento all’oggetto su cui il metodo è invocato. La classe è in realtà una funzione
quindi super è una funzione.
I diagrammi delle classi si disegnano spesso usando il linguaggio di modellazione UML. Ogni istanza di una
classe è anche istanza di tutte le sue superclassi ricorsivamente.
typeof anna
“object”
anna instanceof Liceale
true
anna instanceof Persona
true
pippo instanceof Studente
false
Se una classe ne estende un’altra ma senza modificare o aggiungere proprietà e metodi basterà scrivere:
class Studenti extends Persona{} e le istanze Studenti avranno metodi e proprietà di Persona.
DICHIARAZIONI DI CAMPO
È possibile dichiarare le proprietà di un oggetto direttamente nella classe anziché con assegnamenti a
this.proprietà nel costruttore. Se non viene fornito un inizializzatore di default il valore iniziale sarà undefined
per cui in pratica serve comunque inizializzarle nel constructor(). Ci sono comunque casi in cui le dichiarazioni
di campo sono utili, per esempio, quando il valore di inizializzazione è calcolato chiamando un metodo. Se non
è presente un constructor i campi rimangono inizializzati come fuori da esso altrimenti se presente vengono
sovrascritti.
MEMBRI PRIVATI
I nomi di proprietà che iniziano per # sono visibili sono all’interno della classe. Essi sono campi che possono
essere usati all’interno dei metodi ma che non sono visibili o accessibili dall’esterno. Ogni accesso a una
proprietà il cui nome inizia con # fuori dalla definizione della classe produce un errore.
MEMBRI STATICI
Una classe può includere delle proprietà statiche, introdotte dalla parola chiave static. Queste proprietà non
saranno proprietà delle singole istanze ma della classe stessa. A differenza di altri linguaggi non si può
accedere a un membro statico tramite un’istanza occorre proprio indicare il nome della classe.
GETTER E SETTER
Js include alcune caratteristiche che sono di uso generale, ma che vengono usate con particolare efficacia
nella definizione di classi.
ESEM var oggetto = {get x() {return 1}, set x() {;}}
I metodi di accesso (getter e setter) consentono di simulare la presenza di una proprietà in un oggetto, però le
letture o le scritture di quella proprietà causano l’esecuzione di un metodo anziché una lettura o scrittura nel
dizionario dell’oggetto.
Le parole chiave get e set davanti al nome della proprietà creano i metodi di accesso. Dall’esterno la proprietà
appare come una normale chiave con valore.
Ogni lettura di proprietà causa l’invocazione del getter.
Ogni scrittura di proprietà causa l’invocazione del setter passando come argomento il valore assegnato.
I metodi di accesso possono essere usati per esempio per consentire l’accesso allo stesso dato in modi
diverso
• Quando si vuole leggere il valore di una proprietà di un oggetto, si guarda se l’oggetto ha la chiave
cercata
Se la chiave è presente si controlla se è un getter o una proprietà base
Se è un getter si invoca la funzione corrispondente e il valore è quello restituito dalla funzione
Se è una proprietà base il valore è quello della chiave nell’oggetto
Se la chiave non è presente e l’oggetto ha un prototipo si cerca la proprietà nel prototipo
ricorsivamente
Se la chiave non è presente e l’oggetto non ha un prototipo il risultato è undefined
• Quando si vuole scrivere il valore di una proprietà di un oggetto, si guarda se l’oggetto ha la chiave
cercata
Se la chiave è presente si controlla se è un setter o una proprietà base
Se è un setter si invoca la funzione corrispondente passando come unico argomento il valore
che si vuole assegnare
Se è una proprietà base si assegna il valore alla proprietà eventualmente sovrascrivendo il
valore precedente
Se la chiave non è presente si cerca ricorsivamente un setter nel prototipo ricorsivamente
Se si trova un setter lungo la catena si esegue la funzione corrispondente passando come unico
argomento il valore che si vuole assegnare
Se non si trova un setter lungo la catena la proprietà viene aggiunta all’oggetto con il valore he si
vuole assegnare
GENERATORI
In alcuni casi si vorrebbe poter tornare da una invocazione di funzione restituendo il controllo al chiamante,
ma poi riprendere la computazione da dove si era rimasti. I generatori sono un particolare tipo di funzione (e
dunque di metodo) che ha proprio questa caratteristica.
I generatori si dichiarano con function* f() {} oppure *metodo() {} dentro le classi. Nel corpo di un generatore si
può usare il comando yield expr che restituisce al chiamante il valore di espressione ma riprende l’esecuzione
dal comando successivo e non dall’inizio del corpo in caso di rientro.
Nella pratica spesso lo scorrimento è fatto con un for(... of ...) e non si invoca esplicitamente next() oppure con
lo spread.
Se viene passato un argomento a next() questo sarà trattato come il valore restituito da yield dentro il corpo al
momento della ripresa.
BUG => denota un problema o un difetto che si introduce nel programma involontariamente come:
• Errori di sintassi che vengono riconosciuti dai programmi sia compilati che interpretati
• Errori di battitura come scrivere male il nome di variabile che può essere un problema nei linguaggi in
cui non è sempre necessario dichiarare una variabile e dunque è possibile non accorgersene
• Errori di logica ovvero non considerare all’interno del programma alcuni casi che si possono verificare
• Errori di input o output come l’inserimento di una stringa in un programma che si aspetta numeri
ESEM Scriviamo una funzione calc(A) che dato come argomento un array a in cui il primo elemento è un
operatore aritmetico rappresentato come carattere restituisca il risultato applicando l’operatore fra
tutti i rimanenti elementi dell’array utilizzando l’assegnamento destrutturante e la reduce
function calc(a){
[op,...el]=a
switch(op){
case "+":
return el.reduce((acc,el)=>(acc+el),0)
case "-":
[x,...el]=el
return el.reduce((acc,el)=>(acc-el),x)
case "*":
return el.reduce((acc,el)=>(acc*el),1)
case "/":
[x,...el]=el
return el.reduce((acc,el)=>(acc/el),x)
default:
return undefined
}
}
Stiamo assumendo che il primo elemento sia una stringa che contenga un operatore e che i restanti
elementi siano dei numeri.
La prima cosa da fare quando un risultato non è quello che ci aspettiamo è il debugging ovvero cercare e
rimuovere i bug che sono stati introdotti. A volte però può essere molto difficile fare debugging in programmi
molto lunghi o complicati. Per questo motivo è importante scrivere codice ben organizzato per risalire più
facilmente alle cause di vari bug.
C’è poi una modalità dei JS “strict” che si attiva inserendo all’inizio del codice “use strict” che evita che JS
faccia delle correzioni, ad esempio, se non viene inserito il return in una funzione questa non prova a
indovinare cosa deve ritornare ma non restituisce niente.
Ci sono però delle situazioni in cui JS non è capace di trovare una soluzione come ad esempio se noi
facessimo una divisione per zero esso ci restituirebbe Infinito ma magari noi preferiremmo che non eseguisse
l’operazione, o può capitare il caso in cui si ritrova un “uno” scritto in lettere anziché in numero e JS non
saprebbe gestire la situazione.
Per tutte queste situazioni fino ad ora restituivamo undefined oppure stampavamo a schermo un messaggio di
errore ma non è una buona idea esibire i messaggi di errore all’utente, bisogna gestire gli errori, dare feedback
all’utente e continuare l’esecuzione dal punto giusto utilizzando le eccezioni.
Un’eccezione è un meccanismo di interruzione del flusso del programma in caso di errore, dunque, si
interrompe l’esecuzione del programma e viene lanciata l’eccezione. Ci sono alcune eccezioni che sono
automatiche come, ad esempio, “ReferenceError” o “TypeError” che vengono lanciate dall’interprete, mentre
altre le possiamo lanciare manualmente ad esempio per segnalare che il programma non supporta la divisione
per 0 o un altro tipo di operatore. Questo perché nel momento in cui una funzione può dire la motivazione per
cui non funziona, chi l’ha invocata può prevedere meccanismi per gestire quelle eccezioni attraverso il blocco
try-catch.
ESEM Scriviamo una funzione calcAll(e) che dato come argomento un array e di espressioni invoca calc su
ciascuna espressione.
function calcAll(a){
return a.map(calc)
}
Se il nostro obiettivo è far visualizzare l’errore baserà creare eccezioni ad hoc e lanciarle in caso l’errore si
presenti, se invece in caso di errore si desidera rispondere con una determinata azione è possibile catturare le
eccezioni che ci lancia JS come, ad esempio, il “TypeError” e dire cosa fare se si presenta un determinato
problema attraverso il comando try-catch.
Dentro il blocco try si mette una sequenza di comandi che potrebbe lanciare un’eccezione, se ciò accade
quella sequenza di comandi si interrompe e si passa alla sequenza di comandi nel blocco catch che è pensata
per gestire le eccezioni.
ESEM try{
a=calcAll(expressions)
console.log(a)
} catch(e){
console.log("Sorry not working today!")
}
Il codice del blocco try viene eseguito fino al primo comando che genera un errore incluso nelle chiamate
annidiate di funzioni. Se non c’è nessun errore il blocco try termina e il blocco catch viene saltato continuando
l’esecuzione del programma altrimenti se compare un errore i comandi oltre l’errore non vengono eseguiti e si
esegue il blocco catch a cui si passano nella variabile e le informazioni sull’errore.
Possiamo fare qualcosa in più rispetto a stampare a video un messaggio in caso di errore. Ad esempio,
potremmo decidere di identificare l’eccezione che è stata lanciata e diversificare la gestione in base la tipo di
errore. Infatti, la variabile che viene passata a catch è una variabile di tipo Error o un suo sottotipo che
memorizza l’eccezione. Quella variabile avrà a disposizione una serie di campi e di informazioni che possiamo
utilizzare per gestire l’eccezione come message, stack, name…
ESEM try{
a=calcAll(expressions)
console.log(a)
} catch(e){
console.log("Sorry not working because of a", e.name, “[“, e.message, “]”)
}
Oltre alle eccezioni che in caso di errore lancia automaticamente JS tante volte può essere utile poter lanciare
un’eccezione noi stessi per poter segnalare un problema specifico del nostro programma. In questi casi
l’esecuzione si ferma e si salta al blocco catch più vicino.
Possiamo ora gestire due bug del nostro programma quali un operatore non supportato o il passaggio di una
stringa al posto di un numero
ESEM In questo momento la funzione se non riconosce l’operatore ritorna semplicemente undefined
default:
throw new Error("Operator”, op, “ is not correct, should be one of +-*/")
In questo modo quando l’operatore non è supportato si interrompe l’esecuzione di calc, anche quella
di calcAll e si passa al catch che restituisce “Sorry not working because of a Error [Operator op is not
correct, should be one of +-*/]
ESEM In questo momento il programma procederebbe in caso di + con la somma finchè trova numeri ma in
presenza di una stringa passerebbe alla concatenazione restituendo “0a23”, mentre con gli altri
operatori restituirebbe NaN
function calc(a){
[op,...el]=a
for(let e of el){
if(typeof(e) != "number")
throw new Error("Values must all be numbers")
}
switch(op){…]
Inoltre è possibile distingue i vari tipi di errore non solo dal messaggio ma ragionando in termini di tipo di
errore. Gli errori automatici di JS sono ad esempio TypeError e ReferenceError ma questi non sono gli unici
possibili nomi di errore assegnabili ad un’eccezione, possiamo definirci i nostri errori. New Error() è utile ma
non permette di riconoscere errori di natura diversa e nasconde gli altri errori, lancia un errore generico che
nasconde la vera tipologia dell’errore e ci affidiamo totalmente al messaggio per la differenziazione.
Noi invece vorremmo in grado di creare i nostri tipi di errore che vogliamo gestire e rilanciare al chiamante le
altre tipologie di errore. Ciò si può fare usando le classi e l’ereditarietà.
for(let e of el){
if(typeof(e) != "number")
throw new ValueError("Values must all be numbers")
}
default:
throw new OperatorError("Operator”, op, “ is not correct, should be one of +-*/")
Adesso quando lancio ValueError e OperatorError trova il nome della classe e restituisce quello ad
esempio “Sorry not working because of a OperatorError [Operator op is not correct, should be one of +-
*/]
ESEM try{
a=calcAll(expressions)
console.log(a)
} catch(e){
if(e instanceof CalcError)
console.log("Sorry not working because of", e.message)
else
throw e
}
In questo modo nel caso di un ReferenceError o di un TypeError viene rilanciato un nuovo tipo di errore
differenziando così gli errori che vogliamo gestire da quelli che non vogliamo o non possiamo gestire
che invece di creare un nuovo tipo di errore verranno rilanciati esattamente come li abbiamo ottenuti
È buona norma definirsi una propria gerarchia di eccezioni in modo tale che tutti gli errori sono figli di Error ma
quelli riferiti alla nostra funzione sono istanze di CalcError in questo modo è possibile prendere tutti i figli
relativi ad una specifica funzione escludendo gli altri oppure è possibile andare ancora più nello specifico
prendendo solo un singolo particolare errore.
Inoltre oltre a try e catch abbiamo anche finally. Si esegue il try se va tutto bene si esegue il catch altrimenti in
caso di errore si interrompe il try e si esegue il catch; indipendentemente da questo alla fine si esegue il blocco
finally per ripulire l’ambiente o fare determinate operazioni. Tante volte uscire dal programma subito dopo un
errore non è indicato ma bisogna lasciare i dati su cui si è lavorato in uno stato valid, chiudere i file… Il blocco
Finally ci permette di includere comandi da eseguire sempre sia nel caso di errore che non.
È possibile mettere il catch e mettere solo finally, in questo caso in caso di errore si interromperà l’esecuzione
del try e si andrà direttamente al finally senza gestire le eccezioni.
MODULI
I moduli sono un meccanismo che consente di dividere il codice sorgente dei programmi in vari pezzi ognuno
caricato indipendentemente nel suo file invece di scrivere un unico grande file JS che contiene tutto. Ciò
permette di tenere le cose ordinate, permette di riutilizzare una funzione o una classe in programmi diversi
trasformandole in una libreria evitando di fare copia incolla (se mi accorgo di un bag devo modificarlo solo una
volta), inoltre solitamente la programmazione è cooperativa e quindi è più facile suddividere il lavoro e le
responsabilità lavorando su file separati.
In JS un modulo è un file sorgente in JS contenente una o più dichiarazioni (di funzioni, variabili, classi,
costanti, generatori). In aggiunta il file deve contenere alcune istruzioni specifiche per indicare quali fra le
dichiarazioni presenti devono essere visibili a chi userà il modulo.
• Si dice che il modulo esporta alcune dichiarazioni mentre le dichiarazioni non esportate (magari usate
per implementare le funzioni esportate) rimangono private e visibili sono all’interno del modulo. Il
programmatore del modulo dichiarerà esplicitamente cosa esportare.
• Si dice che il codice che usa il modulo importa alcune dichiarazioni (fra quelle esportate dal modulo).
Ogni linguaggio ha il suo modo di importare ed esportare i moduli con varie metodologie per scrivere una
libreria da riutilizzare.
ESPORTARE
In JS un modulo si può scrivere come un file .js chiamato ad esempio “modulo.js” che implementa una libreria
che dichiara un certo numero di funzioni, variabili o costanti e alla fine espliciterà quali cose fra queste sono
esportabili con la dicitura in fondo di “exports.fun=f2” dove f2 è la funzione da esportare e fun il nome che avrà
nel file in cui viene importata. Dunque, le funzioni importate nel file principale non saranno visibili con il nome
che hanno all’interno della libreria ma con il nome con la quali le esporto; all’esterno sarà visibile solo il
campo dell’exports.
IMPORTARE
Nel file principale la libreria viene importata con la dicitura “const mod=require(‘./modulo’)” e mod sarà una
costante a cui è associato un oggetto {fun: {…} , …} le cui chiavi sono i nomi delle funzioni importate. In questo
caso mod è una costante ma potrebbe benissimo essere una variabile dichiarata con var, let, ecc; si dichiara
const per evitare che sia modificabile dal programmatore.
Una volta che si ha la costante mod con un oggetto per valore all’interno del programma principale posso
richiamare ciò che ho importato attraverso la notazione degli oggetti quindi con “mod.fun” ecc.
ESEM index.js modulo.js
const mod=require("./modulo.js") function square(x){
console.log(mod.compute(3)) return x**2
}
function cube(x){
return x**3
}
function poly(x){
return 2*square(x)+3*cube(x)
}
exports.compute=poly
Mi sono definito due funzioni che utilizzo per implementare la funzione che voglio esportare e rendo
visibile all’esterno solo la funzione poly
Questo è la modalità standard di dare l’import supportata da tutti gli interpreti js ma non è l’unica. Ci sono altri
approcci più moderni che ricalcano altri programmi di programmazione.
In questo secondo modo invece che scrivere il file modulo.js esplicitando alla fine cosa si vuole esportare, si
esplicita ciò che si esporterà al momento della dichiarazione della funzione o della variabile con “export
function f2(n){}”. In questo modo le funzioni avranno lo stesso nome sia nel modulo che nel programma
principale.
Nel file principale per importare una libreria si usa “import * as mod from ‘./modulo.js’” che nuovamente
creerà una variabile mod a cui è associato un oggetto che ha come campi le funzioni e variabili esportate a cui
si può accedere con la notazione “mod.f2”.
È possibile però in questo modo importare anche solo una funzione implementata nella libreria scrivendo
“import f2 as poly from ‘./modulo.js’ ed in questo modo importo un oggetto che contiene la sola funzione e
rinomino la funzione nel programma principale.
Se un file JS è un modulo ovvero contiene export allora tutto il file viene interpretato in modo “Strict”.
Il letterale stringa che rappresenta il nome di un modulo può essere interpretato in modi diversi a seconda
dell’ambiente di esecuzione:
• Un path-name: dire dove si trova il file da importare). Utilizzare come prima “./” significa cercare il file
nella cartella corrente. Se sono su un server come su Replit posso andare a partire da punto del file
system in cui viene eseguito il codice, a cercare il modulo seguendo il path
• Una URL che è supportato soprattutto nei browser ma non in tutti gli ambienti e permettono di fare
animazioni.
• Con nome semplice ovvero senza . o / come con prompt.sync per cui non dicevamo dove tale libreria si
trovava nel nostro computer o nell’internet ma andavamo ad utilizzare un nome noto di libreria per far
si che il package manager fosse in grado di trovarlo e scaricarla per metterla a disposizione.
Come nome di moduli sono ammesse solo stringhe letterali e se lo stesso modulo è importato più volte in
punti diversi del programma il sistema ne tiene una sola copia quando ad esempio un modulo A che sto
importando è stato implementato importando un altro modulo B e poi noi importiamo nuovamente il modulo
B.
Usare i moduli ES6 ovvero il secondo modo di import e export dei moduli è più complicato su Replit perché
richiedono una configurazione particolare.
Tuttavia su Replit è molto facile usare i package ovvero i moduli di libreria come prompt.sync. Questo è utile
non tanto per i moduli che già conosciamo ma per altri che non conosciamo cercandoli per argomento e
aggiungendoli al programma con require dopo averli scaricati.
È importante distinguere ciò che è nel sistema, oggetti predefiniti in qualunque interprete JS, come console,
Math, Number, Error e moduli esistenti che non abbiamo di default ma che possono essere utili.
Per quando riguarda i numeri se provo ad utilizzare i metodi di ciò che è un numero su un numero ovvero
3.24.toFixed(4) non funziona banalmente perché interpreta il punto come virgola dunque se voglio prendere un
qualunque valore e applicargli uno dei metodi predefiniti di Number devo fare il cast e poi invocarlo. Mentre le
costanti letterali numeriche non vengono automaticamente promosse ad oggetti, ciò succede invece con le
stringe e i booleani dunque è possibile scrivere “pippo”.indexOf(“i”) che chiama il metodo indexOf della classe
Sring sulla stringa “pippo”.
DATE
La classe Date che abbiamo anche questa già utilizzato (Date.now) fornisce oggetti che rappresentano date di
calendario nel range 20 aprile 271.821 a.C. – 13 Settembre 275.760 d.C.
Il costruttore cerca di interpretare date espresse come stringhe in una varietà di formati. Senza argomenti
Date() restituisce data e ora correnti. Con numeri li interpreta come anno, mese, giorno, ora, minuti, secondi,
millisecondi.
Gennaio viene indicato con 0 mentre per i giorni della settimana 0 è la domenica.
Gli oggetti Date sono sostanzialmente numeri espressi in millisecondi dal 1 gennaio 1970 dunque possiamo
usare l’aritmetica su di essi e confrontarli. Gli oggetti Date hanno numerosi metodi di istanza: getter e setter
per i vari componenti e conversioni da/verso stringhe e numeri.
ESPRESSIONI REGOLARI
Una delle applicazioni dei linguaggi regolari sono le espressioni regolari che sono un modo per esprimere
schemi di corrispondenza su stringhe ovvero permettono di dire se una qualunque stringa è scritta secondo il
linguaggio. Ciò è utile perché stiamo ad esempio facendo dei log che seguono un template data-gravità-
messaggio ed io vorrei poter trasformare questa stringa in informazioni separate; io sapendo quale è il
linguaggio regolare dei log ovvero il loro template scrivo l’espressione regolare che rappresenta quel linguaggio
e riesco a scorporare i vari pezzi.
Le espressioni regolari sono supportate dalla classe RegExp di JS che è una classe di default.
Nonostante JS sia un linguaggio privo di tipo definisco degli array con i tipi perché ad esempio se sto
programmando un device con poca memoria e storage e ho bisogno di processare un certo numero di dati
prodotti da dei sensori, se so che quei dati sono temperature non ho bisogno di un intero di 32 bit ma mi
interessa utilizzare meno memoria. Se mi creo uno di questi oggetti se l’interprete Js è ottimizzato per
funzionare opportunamente non occuperà più spazio del necessario. Gli oggetti di questo tipo sono detti
“TypedArray” e offrono metodi per convertire da normali array JS a TypedArray e viceversa.
CLASSE SET
La classe Set implementa insiemi di valori qualunque nel senso matematico del termine e fornisce operazioni
per aggiungere, togliere, svuotare, cercare, restituire la dimensione o fare l’enumerazione dei contenuti...
Finora abbiamo usato gli oggetti per implementare gli insiemi ma con chiavi che devono essere stringhe. Con i
Set le chiavi possono essere qualunque valore valido in JS e si possono per esempio creare insiemi di funzioni
o di oggetti.
• S.add(e) aggiunge e a S
• S.delete(e) rimuove e da S
• S.clear(e) svuota S
• S.has(e) metodo che controlla se S contiene e e restituisce un booleano
• S.size proprietà che contiene il numero di elementi
• S.values()/ S.keys() restituisce gli e in S (iteratore), come un generatore si usa con next()
• S.forEach(f) invoca la funzione f su ogni elemento e di S
• A.difference(B) restituisce un Set che è la sottrazione A-B
• A.intersection(B) restituisce l’intersezione fra gli insiemi
• A.symmetric(B) restituisce ciò che non è nell’intersezione
• A.union(B) restituisce l’unione
• A.isDisjointFrom(B) restituisce true se gli insiemi sono distinti
• A.isSubsetOf(B) restituisce true se A è un sottoinsieme di B
• A.isSupersetOf(B) restituisce true se B è un sottoinsieme di A
CLASSE MAP
Analogamente alla classe Set, la classe Map implementa una mappa che è una struttura dati diversa da un
insieme perché mantiene un’associazione tra la chiave e il valore. A differenza dei dizionari di JS in cui le chiavi
possono essere solo stringhe, con la classe Map sia le chiavi che i valori possono essere di qualunque tipo. Si
tratta della generalizzazione del dizionario di JS. Come al solito sono implementati dei metodi per aggiungere
elementi o fare operazioni. Una chiave nella mappa può esserci una sola volta.
Posso passare da un array di forma [[“key1”, “value1”],[“key2”, “value2”]] ad una mappa con new Map(array)
Al contrario trasformo una mappa in un array di quel tipo con Array.from(myMap) oppure […myMap]
Posso anche clonare una Mappa facendo new Map(myMap)
Unisco due mappe con new Map([…Map1, …Map2])
JSON
JSON sta per JavaScript Object Notation è un formato di serializzazione per oggetti JS. In pratica consente di
trasformare un oggetto qualunque in una stringa e viceversa. Una stringa JASON è fatta in un determinato
modo ovvero ha una { per indicare l’inizio di un oggetto, le chiavi sono fra virgolette e i valori rappresentati
nuovamente con la stessa notazione.
Questa scrittura ha avuto successo perché è facilmente leggibile sia dai programmi che dagli umani ma non
sempre è possibile effettuare una conversione da stringa a oggetto o viceversa.
Mi serve dunque per andare da un programma ad un'altra parte passandogli un file o una stringa, ottenere un
risultato e ritrasformalo in un oggetto che JS può manipolare.
OGGETTI DELL’AMBIENTE (HOST-DEFINED)
Ci sono poi alcuni oggetti che non sono dipendenti da un interprete JS e che sono predefiniti in qualunque
interprete ma che dipendono da dove stiamo eseguendo il codice. Ogni ambiente di esecuzione (host) di JS
può fornire ulteriori nomi predefiniti nello scope globale con funzioni adatte allo specifico ambiente. Per
esempio:
• su Node.JS abbiamo dei nomi disponibili tipo console che utilizziamo per interagire con il server
• se sto lavorando in un browser (pagina web) avrò definite delle variabili globali come document che si
riferisce alla pagina web stessa, window ovvero la tab del browser, location per la URL e metodi di
utilità per accedere alle risorse di rete
• AppScript che è il linguaggio delle app di Google ha CalendarApp, DriveApp, GmailApp…
Questi oggetti vengono forniti dall’ambiente per manipolare e interagire con il contesto.
Il codice JS è eseguito da una macchina virtuale che interpreta il linguaggio applicando le regole di semantica e
che viene eseguita in un ambiente virtuale. Ci sono diverse versioni di implementazioni di macchina virtuale JS
come V8 usata in Chrome, Edge e Node.js.
Node.js è un ambiente di esecuzione basato su V8 specializzato per l’esecuzione di programmi singoli su
server e non dentro un browser, molto efficiente nella gestione della programmazione asincrona e dotato di
moltissimi moduli di libreria.
A queste funzioni passiamo il path (“index.js) ovvero il nome del file da leggere di solito come stringa mentre i
dati possono essere di vario tipo.
Le funzioni readFileSync() e writeFileSync() mettono l’intero contenuto del file in una sola stringa dunque non
sono adatti per file di grandi dimensioni (metodo sincrono).
Altre funzioni consentono di leggere o scrivere file molto grandi processando il contenuto in blocchi più piccoli
chiamati chunk. Man mano che la lettura/scrittura procede una funzione passata come argomento (callback)
viene invocata ripetutamente passando un chunk dopo l’altro (metodo asincrono).
MODULO PROCESS
Il modulo process che deve essere importato in JS contiene funzioni per vari scopi:
• Ottenere informazioni sul processo che sta eseguendo il vostro programma come, ad esempio, con
che argomenti da riga di comando è stato lanciato o quali sono i valori delle variabili di ambiente del
sistema operativo
• Manipolare gli altri processi in esecuzione sulla macchina
• Registrare funzioni da eseguire in momenti specifici come subito prima di terminare