JavaScript Garden è una collezione in continua crescita di documentazione
relativa alle parti più peculiari del linguaggio di programmazione JavaScript.
Il suo intento è quello di mostrare come evitare i più comuni errori, i
problemi legati alla performance e le cattive abitudini che i programmatori
JavaScript non esperti possono incontrare lungo il loro cammino di
approfondimento del linguaggio.
L'obiettivo di JavaScript Garden non è quello di insegnarti JavaScript.
Una conoscenza pregressa del linguaggio è fortemenete consigliata, in modo da
capire gli argomenti trattati da questa guida. Per poter imparare le basi del
linguaggio, ti suggeriamo di leggere l'eccellente guida su Mozilla
Developer Network.
JavaScript Garden è pubblicato sotto la licenza MIT ed ospitato su
GitHub. Se trovi inesattezze o errori di battitura, ti prego di
segnalare il problema o fare un pull request sul nostro repository.
Puoi anche trovarci nella stanza JavaScript della chat di Stack
Overflow.
Oggetti
Utilizzo di oggetti e proprietà
Tutto in JavaScript funziona come un oggetto, con la sola eccezione di
null e undefined.
Un'idea comunemente errata è che i numeri letterali non possano essere
usati come oggetti. Questo a causa di una scorretta gestione da parte del
parser di JavaScript, che tenta di analizzare la dot notation di un
numero come se fosse un letterale in virgola mobile.
2.toString(); // solleva SyntaxError
Esistono un paio di soluzioni che possono essere usate per far sì che i
numeri letterali vengano considerati come oggetti.
2..toString(); // il secondo punto viene correttamente riconosciuto
2 .toString(); // notate lo spazio tra il numero e il punto
(2).toString(); // viene prima valutato 2
Oggetti come un tipo di dato
Gli oggetti in JavaScript possono anche essere usati come tabelle hash e
consistono principalmente di proprietà con un nome che mappano dei valori.
Usando un oggetto letterale (notazione {}) è possibile creare un
semplice oggetto. Questo nuovo oggetto eredita da
Object.prototype e non ha proprietà definite.
var foo = {}; // un nuovo oggetto vuoto
// un nuovo oggetto con una proprietà `test` con valore 12
var bar = {test: 12};
Accedere alle proprietà
È possibile accedere alle proprietà di un oggetto in due modi.
Usando il punto oppure attraverso l'uso delle parentesi quadre.
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // SyntaxError
foo['1234']; // funziona
Le due notazioni funzionano quasi in modo identico, con la sola differenza
che usando le parentesi quadre è possibile impostare dinamicamente le
proprietà ed il loro nome identificatore, cosa che altrimenti genererebbe
un errore di sintassi.
Cancellazione delle proprietà
Il solo modo per rimuovere una proprietà da un oggetto è quello di usare
l'operatore delete. Impostando la proprietà a undefined o null, infatti,
si rimuove solo il valore associato alla proprietà, ma non la chiave.
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
Il codice qui sopra, stamperà sia bar undefined che foo null. Soltanto
baz è stato rimosso, e quindi non compare nell'output.
Notazione delle chiavi
var test = {
'case': 'Parola chiave, scrivimi come stringa',
// solleva SyntaxError
delete: 'Parola chiave, anche io devo essere una stringa'
};
Le proprietà di un oggetto possono essere scritte sia come normali caratteri
che come stringhe. A causa di un altro errore di progettazione del parser di
JavaScript, il codice appena visto genererà un SyntaxError in ECMAScript
precedente alla versione 5.
Questo errore nasce dal fatto che delete è una parola chiave, quindi,
deve essere scritta come una stringa letterale per assicurarsi che venga
correttamente interpretata dai vecchi motori JavaScript.
Il prototipo
JavaScript non segue il classico modello di ereditarietà ma, piuttosto,
utilizza quello prototipale.
Anche se ciò viene considerato come uno dei punti più deboli del JavaScript,
il modello ad ereditarietà prototipale è difatto più potente di quello
classico. Ad esempio, è piuttosto semplice creare un modello classico
sulle basi di quello prototipale, mentre l'operazione inversa è piuttosto
complessa.
JavaScript è il solo linguaggio ampiamente utilizzato che sfrutta l'ereditarietà
prototipale, quindi è possibile prendersi il proprio tempo per adeguarsi alle
differenze esistenti tra i due modelli.
La prima grande differenza è che l'ereditarietà in JavaScript utilizza le
catene di prototipi.
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Imposta il prototipo di Bar ad una nuova istanza di Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// Si assicura di elencare Bar come l'attuale costruttore
Bar.prototype.constructor = Bar;
var test = new Bar(); // crea una nuova istanza di bar
// La catena di prototipi finale
test [istanza di Bar]
Bar.prototype [istanza di Foo]
{ foo: 'Hello World', value: 42 }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* ecc. */ }
Nel codice qui sopra, l'oggetto test erediterà sia da Bar.prototype che da
Foo.prototype, e quindi avrà accesso alla funzione method che era stata
definita in Foo. Avrà anche accesso alla proprietà value dell'unica
istanza di Foo, cioè il suo prototipo. È importante notare come
new Bar()non crei una nuova istanza di Foo, ma piuttosto riutilizzi
quella assegnata al suo prototipo. Perciò, tutte le istanze di Bar
condivideranno la stessa proprietà value.
Tabella delle proprietà
Quando si accede alle proprietà di un oggetto, JavaScript risale la
catena di prototipi fino a che non incontra una proprietà con il nome
richiesto.
Se raggiunge la cima della catena (cioè Object.prototype) senza aver
trovato le specifica proprietà, ritorna il valore undefined.
La proprietà Prototype
Anche se la proprietà prototype viene usata dal linguaggio per creare la
catena di prototipi, è comunque sempre possibile assegnarvi un qualsiasi
dato valore. Nonostante cio, i dati primitivi verranno semplicemente ignorati
quando assegnati ad un prototipo.
function Foo() {}
Foo.prototype = 1; // nessun effetto
L'assegnazione di oggetti, come mostrato nell'esempio precedente, funzionerà,
e permette la creazione dinamica di catene di prototipi.
Performance
Il tempo di ricerca per proprietà presenti in alto (all'inizio) della catena
di prototipi, può avere un impatto negativo sulla performance, e questo deve
essere tenuto bene in considerazione in codice dove la performance è un fattore
critico. Inoltre, il tentativo di accedere a proprietà inesistenti obbligherà
comunque ad attraversare tutta la catena di prototipi.
Oltre a ciò, iterando tra le proprietà di un oggetto,
ogni proprietà presente nella catena di prototipi verrà enumerata.
Estensione di prototipi nativi
Una caratteristica che viene spesso abusata, è quella di estendere
Object.prototype o uno degli altri prototipi interni al linguaggio.
Questa tecnica viene detta monkey patching e vìola il principio di
incapsulamento. Anche se usata da popolari framework come Prototype,
non c'è una valida ragione per pasticciare, aggiungendo ai tipi interni del
linguaggio funzionalità non standard.
La sola buona ragione per estendere un prototipo interno è quella di
effettuare il backport di funzionalità presenti nei motori JavaScript
più recenti, come ad esempio Array.forEach.
In conclusione
È essenziale capire il modello di ereditarietà prototipale prima
di scrivere codice complesso che ne faccia uso. Bisogna, inoltre, tenere
sotto controllo la lunghezza della catena di prototipi nel proprio codice,
e suddividerla in più catene se necessario, per evitare possibili problemi di
performance. Inoltre, i prototipi nativi non dovrebbero mai essere
estesi a meno che non sia per garantire compatibilità con le funzionalità
più recenti di JavaScript.
hasOwnProperty
Per verificare se un oggetto ha (possiede) una proprietà definita dentro
se stesso piuttosto che in qualche parte della sua
catena di prototipi, è necessario usare il metodo
hasOwnProperty che tutti gli oggetti ereditano da Object.prototype.
hasOwnProperty è la sola cosa in JavaScript che si occupa delle proprietà
senza attraversare la catena di prototipi.
Solo hasOwnProperty darà il risultato atteso e corretto. Guarda la sezione
[cicli for in][#object.forinloop] per maggiori dettagli riguardo a quando
usare hasOwnProperty per iterare le proprietà di un oggetto.
hasOwnProperty come proprietà
JavaScript non protegge il nome di proprietà hasOwnProperty. Quindi, se
esiste la possibilità che un oggetto possa avere una proprietà con questo
nome, è necessario usare un hasOwnPropertyesterno per ottenere il
risultato corretto.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // ritorna sempre false
// Usa un altro hasOwnProperty di Object e lo richiama con 'this' impostato a foo
({}).hasOwnProperty.call(foo, 'bar'); // true
// E' anche possibile usare hasOwnProperty dal prototipo di
// Object per questo scopo
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
In conclusione
Usare hasOwnProperty è l'unico metodo affidabile per verificare
l'esistenza di una proprietà in un oggetto. È raccomandabile che
hasOwnProperty venga usata in molti casi in cui è necessario iterare le
proprietà di un oggetto, come descritto nella sezione cicli for in.
Il ciclo for in
Come per l'operatore in, il ciclo for in attraversa la catena di
prototipi quando itera tra le proprietà di un oggetto.
// Modifichiamo Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // stampa sia bar che moo
}
Dato che non è possibile modificare il comportamento del ciclo for in,
è necessario filtrare le proprietà indesiderate all'interno del ciclo stesso.
In ECMAScript 3 o precedente, questo può essere fatto usando il metodo
hasOwnProperty di Object.prototype.
A partire da ECMAScript 5, Object.defineProperty può essere utilizzato con
enumerbale impostato a false per aggiungere proprietà agli oggetti (incluso
Object) senza che queste proprietà vengano enumerate. In questo caso è
ragionevole assumere che, nel codice di un'applicazione, ogni proprietà
enumerabile sia stata aggiunta per un motivo, ed quindi omettere hasOwnProperty
in quanto rende il codice più prolisso e meno leggibile. Nel codice delle
librerie hasOwnProperty dovrebbe essere ancora utilizzato, dato che non è
possibile presumere quali proprietà enumerabili siano presenti nella catena dei
prototipi.
Usare hasOwnProperty per il filtraggio
// questo è il foo dell'esempio precedente
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Questa è la sola versione corretta da usare con le vecchie versioni di ECMAScript.
Proprio a causa dell'utilizzo di hasOwnProperty, soltantomoo verrà
stampato; mentre omettendone l'uso, il codice sarà soggetto ad errori nei casi
dove i prototipi nativi (ad esempio Object.prototype) sono stati estesi.
Nelle nuove versioni di ECMAScript, le proprietà non enumerabili possono essere
definite con Object.defineProperty, riducendo il rischio di iterare sulle
proprietà non usando hasOwnProperty. È altresì importante stare attenti
quando si usano librerie come Prototype, che ancora non sfruttano le nuove
funzionalità di ECMAScript.
Quando questo framework viene incluso, è sicuro che i cicli for in che non
utilizzano hasOwnProperty non funzioneranno.
In conclusione
Si raccomanda di usare semprehasOwnProperty in ECMAScript 3 o precedenti,
e nel codice delle librerie. Non si dovrebbe mai dare per scontato nell'ambiente
in cui il codice sta girando, se i prototipi nativi sono stati estesi o meno. A
partire da ECMAScript 5 Object.defineProperty rende possibile definire proprietà
non enumerabili ed omettere hasOwnProperty nel codice dell'applicazione.
Funzioni
Dichiarazioni ed espressioni di funzione
Le funzioni in JavaScript sono oggetti di prima classe. Ciò significa che
possono essere usate come ogni altro valore. Un uso comune di questa
caratteristica è quello di passare una funzione anonima come funzione di
callback ad un'altra funzione, possibilmente asincrona.
La dichiarazione di function
function foo() {}
La funzione qui sopra viene elevata (hoisted) prima
che inizi l'esecuzione del programma. Questo vuol dire che essa è disponibile
da un qualsasi punto dello scope in cui è stata definita, anche se
richiamata prima dell'effettiva definizione nel sorgente.
foo(); // funziona perché foo è stata creata prima di eseguire il codice
function foo() {}
L'espressione function
var foo = function() {};
Questo esempio assegna la funzione anonima alla variabile foo.
foo; // 'undefined'
foo(); // questo solleva un TypeError
var foo = function() {};
Dato che var è una dichiarazione che eleva il nome di variabile foo
prima che l'esecuzione del codice inizi, foo è già dichiarata quando lo
script viene eseguito.
Ma, dal momento che le assegnazioni avvengono solo a runtime, il valore di
foo sarà undefined per default, prima che il relativo
codice sia eseguito.
Espressione di funzione con nome
Un altro caso speciale è l'assegnazione di funzioni con nome.
var foo = function bar() {
bar(); // funziona
}
bar(); // ReferenceError
Qui, bar non è disponibile nello scope più esterno, dal momento che la
funzione viene assegnata solo a foo, mentre è disponibile all'interno di
bar. Ciò è dato dal modo in cui funziona la risoluzione dei nomi
in JavaScript: il nome della funzione è sempre reso disponibile nello scope
locale della funzione stessa.
Come funziona this
JavaScript ha una concezione differente di ciò a cui il nome speciale this
fa normalmente riferimento nella maggior parte degli altri linguaggi di
programmazione. Ci sono esattamente cinque differenti modi nei quali
il valore di this può essere associato nel linguaggio.
Lo scope globale
this;
Usando this nello scope globale, esso farà semplicemente riferimento
all'oggetto globale.
Richiamando una funzione
foo();
Qui, this farà ancora riferimento all'oggetto globale.
Richiamando un metodo
test.foo();
In questo esempio, this farà riferimento a test.
Richiamando un costruttore
new foo();
Una chiamata di funzione che viene preceduta dalla parola chiave new
agisce come un costruttore. Dentro la funzione,
this farà riferimento all'Objectappena creato.
Impostazione esplicita di this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // l'array verrà espanso come mostrato sotto
foo.call(bar, 1, 2, 3); // risulterà in a = 1, b = 2, c = 3
Quando si usano i metodi call o apply di Function.prototype, il valore di
this all'interno della funzione chiamata viene esplicitamente impostato
al primo argomento della corrispondente chiamata di funzione.
Come risultato, nell'esempio sopra, il caso del metodonon viene applicato,
e this all'interno di foo sarà impostato a bar.
Insidie comuni
Mentre molti di questi casi hanno senso, il primo può essere considerato
un altro errore di progettazione del linguaggio perché non ha mai un
uso pratico.
Foo.method = function() {
function test() {
// this viene impostato all'oggetto globale
}
test();
};
Una comune credenza è che this all'interno di test faccia riferimento a
Foo mentre, invece, non è così.
Per poter ottenere l'accesso a Foo dall'interno di test, si può creare
una variabile locale all'interno di method che faccia riferimento a Foo.
Foo.method = function() {
var self = this;
function test() {
// Qui viene usato self invece di this
}
test();
};
self è solo un normale nome di variabile, ma viene comunemente usato come
riferimento ad un this più esterno. Abbinato alle closures
può anche essere usato per passare il valore di this.
Con l'introduzione di ECMAScript 5 è possibile usare il metodo bind combinato
con una funziona anonima
Foo.method = function() {
var test = function() {
// this ora fa riferimento a Foo
}.bind(this);
test();
};
Metodi di asseganzione
Un'altra cosa che non funziona in JavaScript è la creazione di un alias ad
una funzione, cioè l'assegnazione di un metodo ad una variabile.
var test = someObject.methodTest;
test();
A causa della prima dichiarazione, test ora agisce da semplice chiamata a
funzione e quindi, this all'interno di essa non farà più riferimento a
someObject.
Mentre l'assegnazione tardiva di this potrebbe sembrare una cattiva idea
in un primo momento, alla prova dei fatti è ciò che fa funzionare
l'ereditarietà prototipale.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
Quando method viene chiamato da un'istanza di Bar, this farà riferimento
a quell'istanza.
Closures e riferimenti
Una delle caratteristiche più potenti di JavaScript è la disponibilità delle
closure. Con le closure, gli scope hanno sempre accesso allo scope
più esterno nel quale sono state definite. Dal momento che il solo scope che
JavaScript ha è lo scope di funzione, tutte le funzioni,
per default, agiscono da closure.
Emulare variabili private
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
Qui, Counter ritorna due closure: la funzione increment e get.
Entrambe mantengono un riferimento allo scope di Counter e, quindi,
hanno sempre accesso alla variabile count definita in quello scope.
Perché le variabili private funzionano
Dato che non è possibile fare riferimento o assegnare scope in JavaScript,
non c'è modo per accedere alla variabile count dall'esterno. Il solo
modo per interagire con essa è tramite le due closure.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
Il codice sopra non modificherà la variabile count nello scope di Counter,
dato che foo.hack non è stato definito in quello scope. Invece, creerà
(o meglio, sostituirà) la variabile globalecount.
Closure nei cicli
Un errore che spesso viene fatto è quello di usare le closure all'interno dei
cicli, come se stessero copiando il valore della variabile dell'indice del ciclo.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Questo esempio non stamperà i numeri da 0 a 9, ma semplicemente il
numero 10 dieci volte.
La funzione anonima mantiene un riferimento ad i, ma al momento in cui
console.log viene richiamata, il ciclo for è già terminato, ed il valore
di i è stato impostato a 10.
Per ottenere l'effetto desiderato, è necessario creare una copia del valore
di i.
Evitare il problema del riferimento
Per copiare il valore della variabile indice del ciclo, è meglio usare un
contenitore anonimo.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
La funzione anonima più esterna viene chiamata immediatamente con i come
suo primo argomento e riceverà una copia del valore di i come suo
parametro e.
La funzione anonima che viene passata a setTimeout ora ha un riferimento a
e, il cui valore non viene modificato dal ciclo.
C'è anche un altro possibile modo per ottenere il medesimo risultato, e cioè
ritornare una funzione dal contenitore anonimo che avrà quindi lo stesso
comportamento del codice visto precedentemente.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
C'è un ulteriore modo per ottenere ciò, usando .bind, che può assegnare un
contesto this e degli argomenti ad una funzione. Esso funziona allo stesso
modo degli esempi precedenti
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
L'oggetto arguments
Ogni scope di funzione in JavaScript può accedere alla speciale variabile
arguments. Questa variabile mantiene un elenco di tutti gli argomenti
che sono stati passati alla funzione.
L'oggetto argumentsnon è un Array. Sebbene abbia in parte la
semantica di un array (nello specifico la proprietà length), esso non
eredita da Array.prototype ed è a tutti gli effetti un Object.
Proprio per questo motivo, non è possibile usare su arguments i metodi
standard degli array come push, pop, slice. E mentre l'iterazione con
un semplice ciclo for funzionerà senza problemi, sarà necessario convertire
l'oggetto in un vero Array per poter usare i metodi standard di Array con
esso.
Conversione ad array
Il codice seguente ritornerà un nuovo Array contenenente tutti gli elementi
dell'oggetto arguments.
Array.prototype.slice.call(arguments);
Dato che questa conversione è lenta, non è raccomandato usarla in sezioni
di codice in cui la performance è un fattore critico.
Passaggio di argomenti
Quello che segue è il metodo raccomandato per passare argomenti da una funzione
ad un'altra.
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// codice da eseguire
}
Un altro trucco è quello di usare call e apply insieme per creare veloci
contenitori senza vincoli.
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Crea una versione senza vincoli di "method"
// Richiede i parametri: this, arg1, arg2...argN
Foo.method = function() {
// Risultato: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Parametri formali e indici degli argomenti
L'oggetto arguments crea funzioni getter e setter sia per le sue
proprietà che per i parametri formali della funzione.
Come risultato, la modifica del valore di un parametro formale modificherà
anche il valore della corrispondente proprietà nell'oggetto arguments, e
vice versa.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Miti e verità sulla performance
Il solo caso in cui l'oggetto arguments non viene creato, è quando esso
viene dichiarato come un nome all'interno di una funzione o uno dei suoi
parametri formali. Non importa che venga usato o meno.
Sia i getter che i setter vengono sempre creati. Perciò, il loro
utilizzo non ha praticamente alcun impatto sulle prestazioni, specialmente
nel mondo reale dove nel codice c'è più di un semplice accesso alle proprietà
dell'oggetto arguments.
Ad ogni modo, c'è un caso che ridurrà drasticamente la performance nei motori
JavaScript moderni. È il caso dell'utilizzo di arguments.callee.
function foo() {
arguments.callee; // fa qualcosa con questo oggetto funzione
arguments.callee.caller; // e l'oggetto funzione chiamante
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // normalmente sarebbe sostituito con il suo codice...
}
}
Nel codice qui sopra, foo non può più essere soggetto ad inlining
dal momento che necessita di conoscere sia se stesso che il suo chiamante.
Questo non solo annulla possibili guadagni prestazionali ottenibili con
l'inlining, ma spezza anche il principio di incapsulazione perché la funzione
ora potrebbe essere dipendente da uno specifico contesto di esecuzione.
L'utilizzo di arguments.callee o di qualsiasi altra delle sue proprietà
è altamente sconsigliato.
Costruttori
I costruttori in JavaScript sono differenti da quelli di molti altri linguaggi.
Qualsiasi chiamata a funzione preceduta dalla parola chiave new agisce come
un costruttore.
Dentro al costruttore (la funzione chiamata) il valore di this fa riferimento
al nuovo oggetto creato. Il prototype di questo nuovo
oggetto viene impostato al prototype dell'oggetto funzione che è stato invocato
come costruttore.
Se la funzione che è stata chiamata non ha un'istruzione return esplicita,
allora essa ritorna implicitamente il valore di this (il nuovo oggetto).
function Person(name) {
this.name = name;
}
Person.prototype.logName = function() {
console.log(this.name);
};
var sean = new Person();
Questo esempio chiama Person come costruttore ed imposta il prototype del
nuovo oggetto creato a Person.prototype.
In caso di istruzione return esplicita, la funzione ritorna il valore
specificato da quell'istruzione, ma solo se il valore di ritorno è un
Object.
function Car() {
return 'ford';
}
new Car(); // un nuovo oggetto, non 'ford'
function Person() {
this.someValue = 2;
return {
name: 'Charles'
};
}
new Person(); // l'oggetto ritornato ({name: 'Charles'}), escluso someValue
Quando la parola chiave new viene omessa, la funzione non ritornerà un
nuovo oggetto.
function Pirate() {
this.hasEyePatch = true; // imposta la proprietà nell'oggetto globale!
}
var somePirate = Pirate(); // somePirate è undefined
Mentre l'esempio precedente potrebbe sembrare essere funzionante in alcuni
casi, a causa del modo in cui lavora this in JavaScript,
esso userà l'oggetto globale come valore di this.
Factory (Fabbriche di oggetti)
Per poter omettere la parola chiave new, la funzione costruttore deve
esplicitamente ritornare un valore.
function Robot() {
var color = 'gray';
return {
getColor: function() {
return color;
}
}
}
Robot.prototype = {
someFunction: function() {}
};
new Robot();
Robot();
Entrambe le chiamate a Robot ritornano lo stesso risultato, un nuovo oggetto
creato con una proprietà chiamata method, che è una Closure.
Bisogna anche notare che la chiamata new Robot()non influisce sul prototipo
dell'oggetto ritornato. Mentre il prototipo sarà impostato con il nuovo oggetto
creato, Robot non ritornerà mai quel nuovo oggetto.
Nell'esempio sopra, non c'è differenza funzionale nell'usare o meno la parola
chiave new.
Creare nuovi oggetti tramite factory
Viene spesso raccomandato di non usare new perché una sua dimenticanza
può portare a bug potenzialmente insidiosi da risolvere.
Per poter creare un nuovo oggetto, si dovrebbe invece usare una factory e
costruire un nuovo oggetto all'interno di quella factory.
function CarFactory() {
var car = {};
car.owner = 'nobody';
var milesPerGallon = 2;
car.setOwner = function(newOwner) {
this.owner = newOwner;
}
car.getMPG = function() {
return milesPerGallon;
}
return car;
}
Sebbene questo esempio sia a prova di omissione della parola chiave new e
renda sicuramente più semplice l'utilizzo delle variabili private,
esso ha alcuni aspetti negativi.
Usa più memoria dal momento che gli oggetti creati non condividono
i metodi di un prototipo.
Per poter ereditare, la factory deve copiare tutti i metodi da un altro
oggetto oppure mettere quell'oggetto nel prototipo del nuovo oggetto.
Perdere la catena di prototipi solo perché si vuole tralasciare la
parola chiave new è contrario allo spirito del linguaggio.
In conclusione
Sebbene l'omissione della parola chiave new possa portare all'introduzione di
bug, non è certo un motivo per privarsi completamente dell'uso dei prototipi.
Alla fine si tratta di decidere quale sia la soluzione più adatta per
l'applicazione. È specialmente importante scegliere uno specifico stile
di creazione degli oggetti ed usarlo in maniera consistente.
Scope e spazi di nome (namespace)
Sebbene JavaScript non abbia problemi con la sintassi delle parentesi
graffe per la definizione di blocchi, esso non supporta lo scope
per blocco, quindi, tutto ciò che il linguaggio ci mette a disposizione
è lo scope di funzione.
function test() { // questo è uno scope
for(var i = 0; i < 10; i++) { // questo non è uno scope
// conta
}
console.log(i); // 10
}
Anche gli spazi di nome (namespace) non sono gestiti in JavaScript, e ciò
significa che ogni cosa viene definita in un namespace globalmente condiviso.
Ogni volta che ci si riferisce ad una variabile, JavaScript risale attraverso
tutti gli scope fino a che non la trova e, nel caso esso raggiunga lo scope
globale senza aver trovato il nome richiesto, solleva un ReferenceError.
Il problema delle variabili globali
// script A
foo = '42';
// script B
var foo = '42'
Questi due script non hanno lo stesso effetto. Lo script A definisce una
variabile chiamata foo nello scope globale, mentre lo script B definisce
una foo nello scope attuale.
Ancora una volta. Questo esempio non sortisce lo stesso effetto: il
non utilizzo di var può avere importanti conseguenze.
L'omissione dell'istruzione var all'interno della funzione test sostituirà
il valore di foo. Sebbene questo possa non sembrare un grosso problema in
un primo momento, ritrovarsi con migliaia di linee di JavaScript senza
utilizzare var introdurrà orribili bug molto difficili da individuare.
// scope globale
var items = [/* un elenco */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// scope di subLoop
for(i = 0; i < 10; i++) { // istruzione var omessa
// fai qualcosa di eccezionale!
}
}
Il ciclo esterno terminerà dopo la prima chiamata a subLoop, dato che subLoop
sovrascriverà il valore globale di i. L'utilizzo di una var per il secondo ciclo
for avrebbe facilmente evitato questo errore. L'istruzione var non dovrebbe
mai essere omessa a meno che l'effetto desiderato non sia proprio quello
di influenzare lo scope esterno.
Variabili locali
In JavaScript le sole sorgenti per le variabili locali sono i parametri
funzione e le variabili dichiarate tramite l'istruzione
var.
// scope globale
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// scope locale della funzione test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
Mentre foo e i sono variabili locali all'interno dello scope della funzione
test, l'assegnazione di bar sostituirà la variabile globale con lo stesso
nome.
Elevamento (hoisting)
JavaScript eleva le dichiarazioni. Questo significa che le istruzioni var
e le dichiarazioni function verranno spostate in cima agli scope che le
racchiudono.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
Il codice qui sopra, viene trasformato prima che inizi l'esecuzione. JavaScript
sposta sia le istruzioni var che le dichiarazioni function in cima al più
vicino scope che le racchiude.
// le istruzioni var vengono spostate qui
var bar, someValue; // di default a 'undefined'
// la dichiarazione function viene spostata qui
function test(data) {
var goo, i, e; // il blocco scope mancante sposta qui queste istruzioni
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // fallisce con un TypeError dato che bar è ancora 'undefined'
someValue = 42; // le assegnazioni non vengono influenzate dall'elevazione
bar = function() {};
test();
L'omissione del blocco di scope non solo muoverà le istruzioni var fuori dal
corpo dei cicli, ma renderà anche i risultati di certi costrutti if poco
intuitivi.
Nel codice originale, sebbene l'istruzione if sembrasse modificare la
variabile globalegoo, effettivamente essa va a modificare la variabile locale
(dopo che l'elevazione è stata eseguita).
Senza la conoscenza dell'elevazione, uno potrebbe pensare che il codice
qui sotto sollevi un ReferenceError.
// verifica se SomeImportantThing è stato inizializzato
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Ma ovviamente tutto funziona grazie al fatto che l'istruzione var è stata
spostata all'inzio dello scope globale.
var SomeImportantThing;
// qui altro codice potrebbe o meno inizializzare SomeImportantThing
// ci assicuriamo che ci sia
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Ordine di risoluzione dei nomi
Tutti gli scope in JavaScript, scope globale incluso, hanno lo speciale
nome this definito in essi, che fa riferimento
all'oggetto attuale.
Gli scope di funzione hanno anche il nome arguments
definito in essi, che contiene gli argomenti passati alla funzione.
Per esempio, cercando di accedere ad una variabile di nome foo all'interno
dello scope di una funzione, JavaScript effettuerà una ricerca del nome nel
seguente ordine:
Nel caso ci sia un'istruzione var foo nello scope attuale, usa quella.
Se uno dei parametri funzione si chiama foo, usa quello.
Se la funzione stessa si chiama foo, usa quella.
Vai al successivo scope esterno e ricomincia dal numero 1.
Spazi di nome (Namespace)
Un comune problema associato al fatto di avere un solo spazio nomi globale,
è che facilmente si incappa in problemi dove i nomi di variabile si
sovrappongono. In JavaScript queso problema può essere facilmente evitato
con l'aiuto dei contenitori anonimi.
(function() {
// "namespace" auto contenuto
window.foo = function() {
// una closure esposta
};
})(); // esecue immediatamente la funzione
Le funzioni anonime sono considerate espressioni, quindi
per poter essere richiamabili, esse devono prima essere valutate.
( // valuta la funzione dentro le parentesi
function() {}
) // e ritorna l'oggetto funzione
() // richiama il risultato della valutazione
Ci sono altri modi per valutare e chiamare direttamente l'espressione funzione
i quali, sebbene differenti nella sintassi, hanno tutti il medesimo effetto.
// Alcuni modi per invocare direttamente la
!function(){}()
+function(){}()
(function(){}());
// e così via...
In conclusione
Si raccomanda sempre di usare un contenitore anonimo per incapsulare il
codice nel suo proprio namespace. Questo non solo protegge il codice da
eventuali conflitti con i nomi, ma permette anche una migliore modularizzazione
dei programmi.
Inoltre, l'uso delle variabili globali è considerato una cattiva pratica.
Qualsiasi loro uso indica codice scritto male che è suscettibile ad errori
e difficile da mantenere.
Array
Iterazione e proprietà degli Array
Sebbene gli array in JavaScript siano oggetti, non ci sono valide ragioni
per usare il ciclo for in. Infatti, ci sono varie
buone ragioni per evitare l'utilizzo di for in con gli array.
Dato che il ciclo for in enumera tutte le proprietà che sono presenti nella
catena di prototipi, e dal momento che il solo modo per escludere queste
proprietà è quello di usare hasOwnProperty,
esso è già venti volte più lento di un normale ciclo for.
Iterazione
Per poter ottenere la miglior performance durante l'iterazione degli array,
è meglio usare il classico ciclo for.
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
In questo esempio c'è un ulteriore particolare da notare, che è il caching
della lunghezza dell'array tramite l = list.length.
Sebbene la proprietà length sia definita nell'array stesso, c'è ancora un
sovraccarico di lavoro dato dal fatto che deve essere ricercata ad ogni
iterazione del ciclo. E mentre i motori JavaScript recenti potrebbero
applicare delle ottimizzazioni in questo caso, non c'è modo di dire se il
codice verrà eseguito su uno di questi nuovi motori oppure no.
Infatti, l'omissione della parte di caching può risultare in un ciclo eseguito
soltanto alla metà della velocità con cui potrebbe essere eseguito facendo
il caching della lunghezza.
La proprietà length
Mentre il getter della proprietà length ritorna semplicemente il numero di
elementi che sono contenuti nell'array, il setter può essere usato per
troncare l'array.
Assegnando una lunghezza più piccola si tronca l'array. Incrementandola si
crea un array frammentato.
In conclusione
Per la miglior performance, si raccomanda di usare sempre il ciclo for
classico e fare il caching della proprietà length. L'uso di for in su di
un array è segno di un codice scritto male che è suscettibile a bug e pessima
performance.
Il costruttore Array
Dato che il costruttore Array è ambiguo riguardo a come esso gestisca i suoi
parametri, si consiglia calorosamente di usare l'array letterale (notazione [])
quando si creano array.
Nei casi in cui c'è solo un argomento passato al costruttore Array e quando
l'argomento è un Number, il costruttore ritornerà un nuovo array frammentato
con la proprietà length impostata al valore dell'argomento. Si noti
che in questo modo solo la proprietà length del nuovo array verrà impostata,
mentre gli indici dell'array non verranno inizializzati.
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, l'indice non è stato impostato
Essere in grado di impostare la lunghezza dell'array in anticipo è utile soltanto
in poche situazioni, come ad esempio la ripetizione di una stringa, nel cui caso
si eviterebbe l'uso di un ciclo.
new Array(count + 1).join(stringToRepeat);
In conclusione
I letterali sono da preferirsi al costruttore Array. Sono più concisi, hanno una
sintassi più chiara ed incrementano la leggibilità del codice.
Tipi di dati
Uguaglianza e comparazioni
JavaScript usa due differenti metodi per comparare l'uguaglianza dei
valori degli oggetti.
L'operatore di uguaglianza
L'operatore di uguaglianza consiste di due segni di uguaglianza: ==.
JavaScript supporta la tipizzazione debole. Questo significa che
l'operatore di uguaglianza converte i tipi in modo da poterli
confrontare.
Questa tabella mostra i risultati della conversione di tipo, ed è il
principale motivo per cui l'uso di == è ampiamente considerato una
cattiva pratica. Esso introduce bug difficili da rilevare a causa delle
complesse regole di conversione.
Inoltre, c'è anche un impatto sulla performance quando entra in gioco la
conversione di tipo. Ad esempio, una stringa deve essere convertita in un
numero prima di poter essere confrontata con un altro numero.
L'operatore di uguaglianza stretta
L'operatore di uguaglianza stretta consiste di tre segni di uguaglianza: ===.
Funziona come il normale operatore di uguaglianza, con l'eccezione di
non eseguire la conversione di tipo tra gli operandi.
I risultati qui sono più chiari e permettono di identificare subito un problema
con il codice. Questo rende il codice più solido di un certo grado e fornisce anche
migliorie alla performance nel caso di operandi di tipo differente.
Comparazione di oggetti
Nonostante == e === vengano definiti operatori di uguaglianza, essi
funzionano differentemente quando almeno uno degli operandi è un Object.
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
Qui, entrambe gli operatori confrontano per identità e non per
uguaglianza. Essi confrontano, cioè, che sia la stessa istanza dell'oggetto,
in modo molto simile a is in Python e la comparazione di puntatori in C.
In conclusione
Si raccomanda calorosamente di usare solo l'operatore di uguaglianza stretta.
Nei casi dove è necessario che i tipi vengano convertiti, questa operazione
dovrebbe essere fatta esplicitamente piuttosto che essere
lasciata alle complesse regole di conversione del linguaggio.
L'operatore typeof
L'operatore typeof (assieme a instanceof) è
probabilmente il più grande difetto di progettazione di JavaScript,
dato che è quasi completamente inusabile.
Sebbene instanceof abbia ancora limitati casi d'uso, typeof ha realmente
un solo caso d'uso, che non è quello di verificare il tipo di un oggetto.
Tabella dei tipi di JavaScript
Valore Classe Tipo
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
In questa tabella, Tipo fa riferimento al valore ritornato dall'operatore typeof.
Come si può chiaramente vedere, questo valore è tutto fuorchè affidabile.
Classe si riferisce al valore della proprietà interna [[Class]] di un oggetto.
Per ottenere il valore di [[Class]], bisogna usare il metodo toString di
Object.prototype.
La classe di un oggetto
Le specifiche forniscono esattamente un modo per accedere al valore di
[[Class]], con l'uso di Object.prototype.toString.
Nel esempio qui sopra, Object.prototype.toString viene chiamato con il valore
di this impostato all'oggetto di cui si vuole ottenere il
valore di [[Class]].
Testare variabili non definite
typeof foo !== 'undefined'
Questo esempio verificherà se foo è stata attualmente dichiarata oppure no.
Un semplice referenziamento ad essa risulterebbe in un ReferenceError.
Questo è l'unico caso in cui typeof è utile a qualcosa.
In conclusione
Per verificare il tipo di un oggetto, è altamente raccomandato l'utilizzo di
Object.prototype.toString, dato che questo è il solo modo affidabile per
fare ciò. Come mostrato nella tabella precedente, alcuni valori di ritorno
di typeof non sono definiti nelle specifiche, e ciò dimostra come essi
potrebbero differire tra implementazioni differenti.
A meno che non si debba verificare se una variabile è definta, typeof
dovrebbe essere evitato.
L'operatore instanceof
L'operatore instanceof confronta i costruttori dei suoi due operandi.
È utile soltanto per la comparazione di oggetti realizzati dal
programmatore. Se usato sui tipi interni del linguaggio, esso è
praticamente inutile alla stregua dell'operatore typeof.
Confronto di oggetti personalizzati
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// Questo imposta Bar.prototype all'oggetto funzione Foo,
// ma non ad un'istanza di Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
Un'importante cosa da notare qui è che instanceof non funziona con oggetti
originati da differenti contesti JavaScript (ad esempio, differenti
documenti in un browser web), dato che i loro costruttori non saranno
esattamente lo stesso oggetto.
In conclusione
L'operatore instanceof dovrebbe essere usato solo quando si ha a che fare
con oggetti personalizzati creati dal programmatore, che provengono dallo
stesso contesto JavaScript. Proprio come per l'operatore typeof,
ogni altro tipo di utilizzo dovrebbe essere evitato.
Conversione di tipo (Type Casting)
JavaScript è un linguaggio debolmente tipizzato, perciò esso applicherà
una conversione di tipoovunque sia possibile.
// Queste sono vere
new Number(10) == 10; // l'oggetto Number viene convertito
// in una primitiva numero tramite chiamata implicita
// al metodo Number.prototype.valueOf
10 == '10'; // String viene convertita in Number
10 == '+10 '; // Stringa più assurda
10 == '010'; // a ancora di più
isNaN(null) == false; // null viene convertito in 0
// che ovviamente non è NaN
// Queste sono false
10 == 010;
10 == '-10';
Per evitare i problemi appena visti, l'uso
dell'operatore di uguaglianza stretta è altamente
raccomandato. Sebbene questo eviti molti dei comuni problemi, ci sono ancora
molti ulteriori problemi che possono essere generati dal sistema debolmente
tipizzato di JavaScript.
Costruttori di tipi interni
I costruttori dei tipi interni del linguaggio, come Number e String,
funzionano in modo differente a seconda che venga usata o meno la
parola chiave new.
new Number(10) === 10; // False, Object e Number
Number(10) === 10; // True, Number e Number
new Number(10) + 0 === 10; // True, a causa della conversione implicita
L'uso di un tipo di dato interno come Number come costruttore, creerà un
nuovo oggetto Number, ma l'omissione della parola chiave new farà sì
che la funzione Number agisca da convertitore.
Inoltre, il passaggio di valori letterali o non oggetto risulterà in un'ancora
maggiore conversione di tipo.
La miglior opzione è quella di fare esplicitamente la conversione ad uno
dei tre possibili tipi.
Convertire in una stringa
'' + 10 === '10'; // true
Anteponendo una stringa vuota, un valore può facilmente essere convertito in
una stringa.
Convertire in un numero
+'10' === 10; // true
Usando l'operatore unario di addizione, è possibile convertire in un numero.
Convertire in un booleano
Usando due volte l'operatore not, un valore può essere convertito in un
booleano.
La funzione eval eseguirà una stringa di codice JavaScript nello scope locale.
var number = 1;
function test() {
var number = 2;
eval('number = 3');
return number;
}
test(); // 3
number; // 1
Comunque, eval esegue solo nello scope locale quando viene chiamata
direttamente e quando il nome della funzione chiamata è eval.
var number = 1;
function test() {
var number = 2;
var copyOfEval = eval;
copyOfEval('number = 3');
return number;
}
test(); // 2
number; // 3
L'uso di eval dovrebbe essere evitato. Il 99.9% dei suoi "utilizzi" può
essere ottenuto senza di essa.
eval sotto mentite spoglie
Le funzioni di timeoutsetTimeout e setInterval possono
entrambe accettare una stringa come loro primo argomento. Questa stringa verrà
sempre eseguita nello scope globale dato che eval non viene chiamato
direttamente in questo caso.
Problemi di sicurezza
eval è anche un problema di sicurezza, perché essa esegue qualsiasi
codice le viene passato. Non si dovrebbe mai usare con stringhe di origine
sconosciuta o inaffidabile.
In conclusione
eval non dovrebbe mai essere usata. Qualsiasi codice che ne faccia uso dovrebbe
essere messo in discussione sotto l'aspetto della funzionalità, della performance
e della sicurezza. Se qualcosa richiede eval per poter funzionare, allora non
dovrebbe essere usato in primo luogo, ma si dovrebbe prevedere una
miglior progettazione che non richieda l'uso di eval.
undefined e null
JavaScript usa due valori distinti per il nulla, null e undefined, e
quest'ultimo è il più utile.
Il valore undefined
undefined è un tipo con esattamente un valore: undefined.
Il linguaggio definisce anche una variabile globale che ha il valore di undefined.
Questa variabile è anche chiamata undefined. Comunque, questa variabile non è
né una costante né una parola chiave del linguaggio. Ciò significa che il suo valore
può facilmente essere sovrascritto.
Ecco alcuni esempi di quando il valore undefined viene ritornato:
Accedendo la variabile globale (non modificata) undefined.
Accedendo una variabile dichiarata ma non ancora inizializzata.
Ritorno implicito da funzioni che non hanno l'istruzione return.
Istruzioni return che non ritornano esplicitamente alcun valore.
Ricerca di proprietà inesistenti.
Parametri funzione a cui non viene esplicitamente passato alcun valore.
Qualsiasi cosa a cui sia stato assegnato il valore undefined.
Qualsiasi espressione nella forma di void(espressione).
Gestire le modifiche al valore di undefined
Dato che la variabile globale undefined mantiene solo una copia dell'attuale
valore di undefined, assegnandole un nuovo valore non cambia il valore del
tipoundefined.
Inoltre, per confrontare qualcosa con il valore di undefined, è necessario
ottenere prima il valore di undefined.
Per proteggere il codice da possibili sovrascritture della variabile undefined,
viene usata una comune tecnica che prevede l'aggiunta di un ulteriore parametro
ad un contenitore anonimo al quale non viene passato alcun
argomento.
var undefined = 123;
(function(something, foo, undefined) {
// ora undefined nello scope locale
// fa nuovamente riferimento al valore `undefined`
})('Hello World', 42);
Un altro modo per ottenere lo stesso effetto sarebbe quello di usare una
dichiarazione all'interno del contenitore.
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
La sola differenza è che questa versione si traduce in 4 byte in più quando
minificata, e non c'è nessun'altra istruzione var al'interno del contenitore
anonimo.
Utilizzi di null
Mentre undefined nel contesto del linguaggio JavaScript viene principalmente
usato come un tradizionale null, l'attuale null (sia letterale che tipo di
dati) è più o meno solo un altro tipo di dato.
Viene usato in alcune funzioni interne al JavaScript (come la dichiarazione
del termine della catena di prototipi, impostando Foo.prototype = null), ma
nella maggior parte dei casi, può essere rimpiazzato da undefined.
Inserimento automatico dei punti-e-virgola
Sebbene JavaScript utilizzi lo stile di sintassi del C, esso non
obbliga l'uso dei punti-e-virgola nel codice sorgente, perciò è possibile
ometterli.
Detto questo, JavaScript non è un linguaggio che fa a meno dei punti-e-virgola.
Infatti, esso necessita di punti-e-virgola per poter comprendere il codice
sorgente. Quindi, il parser del JavaScript li inserisce automaticamente
ogni volta che incontra un errore di analisi dato dalla mancanza di un
punto-e-virgola.
var foo = function() {
} // errore di analisi, atteso punto-e-virgola
test()
Quindi avviene l'inserimento, ed il parser prova nuovamente.
var foo = function() {
}; // nessun errore, il parser continua
test()
L'inserimento automatico dei punti-e-virgola è considerato essere uno dei
più grandi errori di progettazione del linguaggio, perché può
modificare il comportamento del codice.
Come funziona
Il codice qui sotto non ha punti-e-virgola, quindi sta al parser decidere dove
inserirli.
(function(window, undefined) {
function test(options) {
log('testing!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'long string to pass here',
'and another long string to pass'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
Di seguito il risultato del gioco da "indovino" del parser.
(function(window, undefined) {
function test(options) {
// Non inserito, linee unite
log('testing!')(options.list || []).forEach(function(i) {
}); // <- inserito
options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- inserito
return; // <- inserito, invalida l'istruzione return
{ // trattato come un blocco
// un'etichetta e una singola espressione
foo: function() {}
}; // <- inserito
}
window.test = test; // <- inserito
// Le linee vengono unite nuovamente
})(window)(function(window) {
window.someLibrary = {}; // <- inserito
})(window); //<- inserito
Il parser ha drasticamente modificato il comportamento del codice. In alcuni casi,
questo porta ad eseguire cose sbagliate.
Parentesi ad inizio riga
Nel caso di parentesi ad inizio riga, il parser non inserirà un punto-e-virgola.
Le possibilità che lognon ritorni una funzione sono veramente alte,
perciò il codice qui sopra porterà ad un TypeError dichiarando che
undefined is not a function (undefined non è una funzione).
In conclusione
È fortemente raccomandato di non omettere mai i punti-e-virgola.
Si raccomanda anche di mantenere le parentesi sulla stessa linea della
corrispondente istruzione, e di non ometterle mai in istruzioni if / else
a linea singola. Queste misure precauzionali non solo miglioreranno la
consistenza del codice, ma preverranno anche che il parser JavaScript
modifichi il comportamento del codice in modo inaspettato.
L'operatore delete
In breve, è impossibile eliminare variabili globali, funzioni e qualche
altra cosa in JavaScript che ha l'attributo DontDelete impostato.
Codice globale e codice funzione
Quando una variabile o una funzione viene definita in un scope globale o
funzione, essa è una proprietà dell'oggetto Activation
o dell'oggetto Global. Queste proprietà hanno un set di attributi, tra i quali
DontDelete. Dichiarazioni di variabile o funzione nel codice globale o
funzione, creano sempre proprietà con DontDelete, e quindi non possono essere
eliminate.
// variabile globale:
var a = 1; // DontDelete è impostato
delete a; // false
a; // 1
// funzione normale:
function f() {} // DontDelete è impostato
delete f; // false
typeof f; // "function"
// la riassegnazione non aiuta:
f = 1;
delete f; // false
f; // 1
Proprietà esplicite
Proprietà esplicitamente impostate possono essere eliminate normalmente.
Nel codice qui sopra, obj.x e obj.y possono essere eliminate perché
non hanno l'attributo DontDelete. Ecco perché anche l'esempio seguente
funziona.
// questo funziona, tranne che per IE:
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true - solo una variabile globale
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined
Qui usiamo un trucco per eliminare a. this qui fa
riferimento all'oggetto Global e noi dichiariamo esplicitamente la
variabile a come sua proprietà, il che ci permette di eliminarla.
IE (almeno 6-8) ha alcuni bug, quindi il codice precedente non funziona.
Argomenti funzione e proprietà interne
Anche i normali argomenti delle funzioni, gli
oggetti arguments e le proprietà interne hanno
DontDelete impostato.
Il comportamento dell'operatore delete può essere inaspettato con gli oggetti
non nativi. A causa delle specifiche, agli oggetti non nativi è permesso di
implementare qualsiasi tipo di funzionalità.
In conclusione
L'operatore delete spesso ha un comportamento inaspettato e può solo essere
usato con sicurezza per eliminare proprietà esplicitamente impostate in oggetti
normali.
Varie
setTimeout e setInterval
Dato che JavaScript è asincrono, è possibile programmare l'esecuzione di una
funzione usando le funzioni setTimeout e setInterval.
function foo() {}
var id = setTimeout(foo, 1000); // ritorna un Number > 0
Quando chiamato, setTimeout ritorna l'ID del timeout e programma foo per
essere eseguito approssimativamente un migliaio di millisecondi nel futuro.
foo verrà quindi eseguito una volta.
Dipendendo dalla risoluzione del timer del motore JavaScript che esegue il codice,
come anche dal fatto che JavaScript è single threaded e quindi altro codice
potrebbe essere eseguito bloccando il thread, non è mai sicuro scommettere
che una funzione verrà eseguita esattamente al ritardo specifiato nella chiamata
a setTimeout.
La funzione che è stata passata come primo parametro verrà chiamata dall'oggetto globale,
e ciò significa che this all'interno della funzione chiamata
farà riferimento all'oggetto globale.
function Foo() {
this.value = 42;
this.method = function() {
// this fa riferimento all'oggetto globale
console.log(this.value); // stamperà undefined
};
setTimeout(this.method, 500);
}
new Foo();
Sovrapposizione di chiamate con setInterval
Mentre setTimeout esegue solo una volta la funzione, setInterval (come il
nome suggerisce) eseguirà la funzione ogniX millisecondi, ma il suo
utilizzo è sconsigliato.
Quando il codice che viene eseguito blocca la chiamata timeout, setInterval
eseguirà ancora più chiamate alla specifica funzione. Questo può, specialmente
con intervalli molto brevi, tradursi in chiamate a funzione che si sovrappongono.
function foo(){
// qualcosa che blocca per 1 secondo
}
setInterval(foo, 1000);
Nel codice precedente, foo verrà chiamato una volta e quindi bloccherà per
un secondo.
Mentre foo blocca il codice, setInterval continuerà a programmare ulteriori
chiamate ad essa. Ora, quando foo ha finito, ci saranno già dieci ulteriori
chiamate ad essa in attesa per essere eseguite.
Gestione di potenziale codice bloccante
La soluzione più semplice, come anche la più controllabile, è quella di usare
setTimeout all'interno di se stessa.
function foo(){
// qualcosa che blocca per 1 secondo
setTimeout(foo, 1000);
}
foo();
Non solo questo incapsula la chiamata a setTimeout, ma previene anche la
sovrapposizione delle chiamate e da un controllo addizionale. foo stessa
può ora decidere se vuole continuare ad essere eseguita oppure no.
Pulizia manuale dei timeout
La pulizia di timeout ed intervalli funziona passando il rispettivo ID a
clearTimeout o clearInterval, in base a quale set di funzioni è stato
usato precedentemente.
var id = setTimeout(foo, 1000);
clearTimeout(id);
Pulizia di tutti i timeout
Dato che non c'è un metodo interno per la pulizia di tutti i timeout e/o
intervalli, è necessario usare la forza bruta per poter raggiungere questo
scopo.
// pulisce "tutti" i timeout
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
Ma ci potrebbero ancora essere timeout che non vengono toccati da questo
numero arbitrario. Un altro modo per ottenere ciò, è considerare che l'ID
dato ad un timeout viene incrementato di uno ogni volta che si chiama
setTimeout.
// pulisce "tutti" i timeout
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
clearTimeout(i);
}
Sebbene questo funzioni con la maggior parte dei browser odierni, non è
specificato che gli ID debbano essere ordinati in quel modo e ciò potrebbe
anche cambiare in futuro. Perciò, si raccomanda di tener traccia di tutti
gli ID dei timeout, così che possano essere puliti in modo specifico.
Uso nascosto di eval
setTimeout e setInterval possono anche accettare una stringa come loro
primo parametro. Questa caratteristica non dovrebbe essere mai usata
perché internamente fa uso di eval.
function foo() {
// verrà chiamata
}
function bar() {
function foo() {
// non verrà mai chiamata
}
setTimeout('foo()', 1000);
}
bar();
Dal momento che eval non viene chiamata direttamente in questo
caso, la stringa passata a setTimeout verrà eseguita nello scope globale.
Quindi, non verrà usata la variabile locale foo dallo scope di bar.
Si raccomanda inoltre di non usare una stringa per passare argomenti alla
funzione che verrà chiamata da una delle funzioni di timeout.
function foo(a, b, c) {}
// non usare MAI questo
setTimeout('foo(1, 2, 3)', 1000)
// Usare invece una funzione anonima
setTimeout(function() {
foo(1, 2, 3);
}, 1000)
In conclusione
Una stringa non dovrebbe mai essere usata come parametro di setTimeout o
setInterval. È un chiaro segno di codice veramente pessimo, quando
gli argomenti necessitano di essere passati alla funzione che deve essere
chiamata. Dovrebbe invece essere passata una funzione anonima che si incarichi
di gestire l'effettiva chiamata.
Inoltre, l'uso di setInterval dovrebbe essere evitato perché il suo schedulatore
non viene bloccato dall'esecuzione di JavaScript.