Interrompi le funzioni di annidamento! (Ma non tutti loro)

JavaScript ha più di quindici anni; tuttavia, la lingua è ancora fraintesa da quella che è forse la maggior parte degli sviluppatori e dei designer che usano il linguaggio. Uno degli aspetti più potenti, ma fraintesi, di JavaScript sono le funzioni. Sebbene sia estremamente vitale per JavaScript, il loro uso improprio può introdurre inefficienze e ostacolare le prestazioni di un'applicazione.


Preferisci un video tutorial?


Le prestazioni sono importanti

Nell'infanzia del Web, le prestazioni non erano molto importanti.

Nell'infanzia del Web, le prestazioni non erano molto importanti. Dalle connessioni dial-up 56K (o peggio) al computer Pentium 133 MHz di un utente finale con 8 MB di RAM, il Web avrebbe dovuto essere lento (anche se ciò non ha impedito a tutti di lamentarsi). Per questo motivo JavaScript è stato creato per iniziare, per scaricare l'elaborazione semplice, come la convalida del modulo, per il browser, rendendo certe attività più facili e veloci per l'utente finale. Invece di compilare un modulo, fare clic su Invia e attendere almeno trenta secondi per comunicarti di aver immesso dati non corretti in un campo, gli autori Web abilitati JavaScript devono convalidare il tuo input e avvisarti di eventuali errori prima dell'invio del modulo.

Avanti veloce ad oggi. Gli utenti finali apprezzano i computer multi-core e multi-GHz, l'abbondanza di RAM e velocità di connessione elevate. JavaScript non è più relegato alla convalida del modulo, ma può elaborare grandi quantità di dati, modificare al volo qualsiasi parte di una pagina, inviare e ricevere dati dal server e aggiungere interattività a una pagina altrimenti statica, tutto nel nome di migliorare l'esperienza dell'utente. È un modello abbastanza noto in tutto il settore informatico: una quantità crescente di risorse di sistema consente agli sviluppatori di scrivere sistemi operativi e software più sofisticati e dipendenti dalle risorse. Ma anche con questa quantità abbondante e sempre crescente di risorse, gli sviluppatori devono essere consapevoli della quantità di risorse che la loro app consuma, specialmente sul web.

I motori JavaScript di oggi sono avanti di anni luce rispetto ai motori di dieci anni fa, ma non ottimizzano tutto. Quello che non ottimizzano è lasciato agli sviluppatori.

C'è anche un nuovo set di dispositivi abilitati al web, smartphone e tablet, in esecuzione su un numero limitato di risorse. I loro sistemi operativi e app ridimensionati sono certamente un successo, ma i principali fornitori di sistemi operativi mobili (e anche fornitori di sistemi operativi desktop) guardano alle tecnologie Web come piattaforma di scelta degli sviluppatori, spingendo gli sviluppatori JavaScript a garantire che il loro codice sia efficiente e performante.

Un'applicazione poco performante rovinerà una buona esperienza.

Soprattutto, l'esperienza dell'utente dipende dalle buone prestazioni. Le belle e naturali interfacce utente aggiungono sicuramente all'esperienza dell'utente, ma un'applicazione scadente può rovinare una buona esperienza. Se gli utenti non vogliono usare il tuo software, allora che senso ha scriverlo? Quindi, è assolutamente vitale che, in questo periodo di sviluppo Web-centrico, gli sviluppatori JavaScript scrivano il miglior codice possibile.

Quindi cosa c'entra tutto questo con le funzioni?

Dove definisci le tue funzioni ha un impatto sulle prestazioni della tua applicazione.

Esistono molti anti-pattern JavaScript, ma una delle funzioni coinvolge è diventata piuttosto popolare, specialmente nella folla che si sforza di costringere JavaScript ad emulare funzionalità in altre lingue (funzionalità come la privacy). Ha funzioni di nidificazione in altre funzioni e, se eseguito in modo errato, può avere effetti negativi sulla tua applicazione.

È importante notare che questo anti-pattern non si applica a tutte le istanze di funzioni annidate, ma in genere è definito da due caratteristiche. Innanzitutto, la creazione della funzione in questione viene di solito differita, ovvero la funzione nidificata non viene creata dal motore JavaScript in fase di caricamento. Quello di per sé non è una cosa negativa, ma è la seconda caratteristica che ostacola le prestazioni: la funzione annidata viene ripetutamente creata a causa di ripetute chiamate alla funzione esterna. Quindi, anche se può essere facile dire "tutte le funzioni annidate sono cattive", questo non è certo il caso, e sarai in grado di identificare le funzioni annidate problematiche e risolverle per velocizzare la tua applicazione.


Nesting Functions in Normal Functions

Il primo esempio di questo anti-pattern è l'annidamento di una funzione all'interno di una normale funzione. Ecco un esempio semplificato:

function foo (a, b) function bar () return a + b;  barra di ritorno ();  foo (1, 2);

Non è possibile scrivere questo codice esatto, ma è importante riconoscere il modello. Una funzione esterna, foo (), contiene una funzione interiore, bar(), e chiama quella funzione interiore per fare il lavoro. Molti sviluppatori dimenticano che le funzioni sono valori in JavaScript. Quando si dichiara una funzione nel codice, il motore JavaScript crea un oggetto funzione corrispondente, un valore che può essere assegnato a una variabile o passato a un'altra funzione. L'atto di creare un oggetto funzione assomiglia a quello di qualsiasi altro tipo di valore; il motore JavaScript non lo crea fino a quando non è necessario. Quindi, nel caso del codice precedente, il motore JavaScript non crea l'interno bar() funzione fino foo () esegue. quando foo () esce, il bar() l'oggetto funzione è distrutto.

Il fatto che foo () ha un nome implica che sarà chiamato più volte in tutta l'applicazione. Mentre una esecuzione di foo () sarebbe considerato OK, le chiamate successive causano il lavoro non necessario per il motore JavaScript perché deve ricreare a bar() funzione oggetto per ogni foo () esecuzione. Quindi, se chiami foo () 100 volte in un'applicazione, il motore JavaScript deve creare e distruggere 100 bar() oggetti funzione. Grande affare, vero? Il motore deve creare altre variabili locali all'interno di una funzione ogni volta che viene chiamato, quindi perché preoccuparsi delle funzioni?

A differenza di altri tipi di valori, le funzioni in genere non cambiano; una funzione viene creata per eseguire un'attività specifica. Quindi non ha molto senso sprecare cicli di CPU ricreando un valore un po 'statico più e più volte.

Idealmente, il bar() l'oggetto funzione in questo esempio dovrebbe essere creato una sola volta e questo è facile da ottenere, anche se naturalmente le funzioni più complesse potrebbero richiedere un ampio refactoring. L'idea è di spostare il bar() dichiarazione al di fuori di foo () in modo che l'oggetto funzione venga creato una sola volta, in questo modo:

function foo (a, b) return bar (a, b);  barra delle funzioni (a, b) return a + b;  foo (1, 2);

Si noti che il nuovo bar() la funzione non è esattamente come era dentro foo (). Perché il vecchio bar() la funzione ha usato il un e B parametri in foo (), la nuova versione aveva bisogno di refactoring per accettare quegli argomenti per fare il suo lavoro.

A seconda del browser, questo codice ottimizzato è ovunque dal 10% al 99% più veloce rispetto alla versione annidata. Puoi visualizzare ed eseguire il test per te stesso su jsperf.com/nested-named-functions. Tieni a mente la semplicità di questo esempio. Un guadagno di prestazioni del 10% (al livello più basso dello spettro delle prestazioni) non sembra molto, ma sarebbe più alto in quanto sono coinvolte più funzioni nidificate e complesse.

Per confondere il problema, avvolgere questo codice in una funzione anonima, autoeseguibile, in questo modo:

(function () function foo (a, b) return bar (a, b); function bar (a, b) return a + b; foo (1, 2); ());

Il codice di avvolgimento in una funzione anonima è un modello comune e, a prima vista, potrebbe sembrare che questo codice replica il problema di prestazioni sopra descritto avvolgendo il codice ottimizzato in una funzione anonima. Anche se si verifica una leggera performance con l'esecuzione della funzione anonima, questo codice è perfettamente accettabile. La funzione autoesposta serve solo a contenere e proteggere il foo () e bar() funzioni, ma ancora più importante, la funzione anonima viene eseguita solo una volta, quindi l'interno foo () e bar() le funzioni vengono create una sola volta. Tuttavia, ci sono alcuni casi in cui le funzioni anonime sono altrettanto (o più) problematiche delle funzioni con nome.


Funzioni anonime

Per quanto riguarda questo argomento di prestazioni, le funzioni anonime possono essere più pericolose delle funzioni con nome.

Non è l'anonimità della funzione che è pericolosa, ma è il modo in cui gli sviluppatori li usano. È abbastanza comune utilizzare le funzioni anonime quando si impostano i gestori di eventi, le funzioni di callback o le funzioni di iteratore. Ad esempio, il codice seguente assegna a clic ascoltatore di eventi sul documento:

document.addEventListener ("click", function (evt) alert ("Hai cliccato sulla pagina."););

Qui, una funzione anonima viene passata al addEventListener () metodo per cablare il clic evento sul documento; quindi, la funzione viene eseguita ogni volta che l'utente fa clic in qualsiasi punto della pagina. Per dimostrare un altro uso comune delle funzioni anonime, considera questo esempio che utilizza la libreria jQuery per selezionare tutto elementi nel documento e scorrere su di essi con il ogni() metodo:

$ ("a"). each (function (index) this.style.color = "red";);

In questo codice, la funzione anonima passata agli oggetti di jQuery ogni() il metodo viene eseguito per ciascuno elemento trovato nel documento. A differenza delle funzioni con nome, dove sono implicitamente chiamate ripetutamente, l'esecuzione ripetuta di un gran numero di funzioni anonime è piuttosto esplicita. È imperativo, per la prestazione, che siano efficienti e ottimizzati. Dai un'occhiata al plug-in jQuery seguente (ancora più semplificato):

$ .fn.myPlugin = function (options) return this.each (function () var $ this = $ (this); function changeColor () $ this.css (color: options.color); changeColor ();); ;

Questo codice definisce un plugin estremamente semplice chiamato myplugin; è così semplice che molti tratti di plugin comuni sono assenti. Normalmente, le definizioni dei plugin sono racchiuse in funzioni anonime autoeseguite e di solito vengono forniti valori predefiniti per le opzioni per garantire che i dati validi siano disponibili per l'uso. Queste cose sono state rimosse per motivi di chiarezza.

Lo scopo di questo plugin è di cambiare il colore degli elementi selezionati in qualsiasi cosa sia specificata nel opzioni oggetto passato al myplugin () metodo. Lo fa passando una funzione anonima al ogni() iteratore, eseguendo questa funzione per ogni elemento nell'oggetto jQuery. All'interno della funzione anonima, una funzione interna chiamata cambia colore() fa il vero lavoro di cambiare il colore dell'elemento. Come scritto, questo codice è inefficiente perché, avete indovinato, il cambia colore() la funzione è definita all'interno della funzione di iterazione? facendo ricreare il motore JavaScript cambia colore() con ogni iterazione.

Rendere questo codice più efficiente è piuttosto semplice e segue lo stesso schema di prima: refactoring cambia colore() funzione da definire al di fuori di qualsiasi funzione di contenimento e consentirgli di ricevere le informazioni di cui ha bisogno per svolgere il proprio lavoro. In questo caso, cambia colore() necessita l'oggetto jQuery e il nuovo valore del colore. Il codice migliorato assomiglia a questo:

function changeColor ($ obj, color) $ obj.css (color: color);  $ .fn.myPlugin = function (options) return this.each (function () var $ this = $ (this); changeColor ($ this, options.color);); ;

È interessante notare che questo codice ottimizzato aumenta le prestazioni di un margine molto più piccolo rispetto al foo () e bar() esempio, con Chrome che guida il pacchetto con un guadagno di prestazioni del 15% (jsperf.com/function-nesting-with-jquery-plugin). La verità è che l'accesso al DOM e l'utilizzo dell'API di jQuery aggiungono il loro successo alle prestazioni, specialmente jQuery ogni(), che è notoriamente lento rispetto ai loop nativi di JavaScript. Ma come prima, tieni a mente la semplicità di questo esempio. Le funzioni più nidificate, maggiore è il guadagno di prestazioni dall'ottimizzazione.

Nesting Functions in Constructor Functions

Un'altra variante di questo anti-pattern è la funzione di nidificazione all'interno dei costruttori, come mostrato di seguito:

function Person (firstName, lastName) this.firstName = firstName; this.lastName = lastName; this.getFullName = function () return this.firstName + "" + this.lastName; ;  var jeremy = new Person ("Jeremy", "McPeak"), jeffrey = new Person ("Jeffrey", "Way");

Questo codice definisce una funzione di costruzione chiamata Persona(), e rappresenta (se non era ovvio) una persona. Accetta argomenti contenenti il ​​nome e il cognome di una persona e memorizza tali valori in nome di battesimo e cognome proprietà, rispettivamente. Il costruttore crea anche un metodo chiamato getFullName (); concatena il nome di battesimo e cognome proprietà e restituisce il valore stringa risultante.

Quando si crea un oggetto in JavaScript, l'oggetto viene memorizzato

Questo schema è diventato abbastanza comune nella comunità JavaScript di oggi perché può emulare la privacy, una funzione per cui JavaScript non è attualmente progettato (si noti che la privacy non è nell'esempio sopra, la vedremo più avanti). Ma nell'uso di questo modello, gli sviluppatori creano inefficienza non solo nei tempi di esecuzione, ma anche nell'utilizzo della memoria. Quando si crea un oggetto in JavaScript, l'oggetto viene memorizzato. Rimane in memoria fino a quando tutti i riferimenti ad esso sono impostati su nullo o sono fuori portata. Nel caso del jeremy oggetto nel codice precedente, la funzione assegnata a getFullName è in genere memorizzato in memoria fino a quando il jeremy l'oggetto è in memoria. Quando il jeffrey oggetto viene creato, un nuovo oggetto funzione viene creato e assegnato a jeffrey'S getFullName membro, e anch'esso consuma memoria per tutto il tempo jeffrey è in memoria. Il problema qui è questo jeremy.getFullName è un oggetto funzione diverso da jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName risultati in falso; esegui questo codice su http://jsfiddle.net/k9uRN/). Entrambi hanno lo stesso comportamento, ma sono due oggetti funzione completamente diversi (e quindi ognuno consuma memoria). Per maggiore chiarezza, dai un'occhiata alla Figura 1:

Figura 1

Qui, vedi il jeremy e jeffrey oggetti, ognuno dei quali ha il suo getFullName () metodo. Quindi, ciascuno Persona l'oggetto creato ha il suo unico getFullName () metodo - ognuno dei quali consuma il proprio pezzo di memoria. Immagina di creare 100 Persona oggetti: se ciascuno getFullName () il metodo consuma 4KB di memoria, quindi 100 Persona gli oggetti consumerebbero almeno 400 KB di memoria. Questo può sommarsi, ma può essere drasticamente ridotto usando il prototipo oggetto.

Usa il prototipo

Come accennato in precedenza, le funzioni sono oggetti in JavaScript. Tutti gli oggetti funzione hanno a prototipo proprietà, ma è utile solo per le funzioni di costruzione. In breve, il prototipo la proprietà è letteralmente un prototipo per la creazione di oggetti; qualunque cosa sia definita sul prototipo di una funzione di costruzione è condivisa tra tutti gli oggetti creati da quella funzione di costruzione.

Sfortunatamente, i prototipi non sono abbastanza stressati nell'educazione di JavaScript.

Sfortunatamente, i prototipi non sono abbastanza stressati nell'educazione di JavaScript, ma sono assolutamente essenziali per JavaScript perché è basato su e costruito con prototipi - è un linguaggio prototipale. Anche se non hai mai digitato la parola prototipo nel tuo codice, vengono utilizzati dietro le quinte. Ad esempio, ogni metodo nativo basato su stringhe, come Diviso(), substr (), o sostituire(), sono definiti su Stringa()Il prototipo. I prototipi sono così importanti per il linguaggio JavaScript che, se non si abbraccia la natura prototipale di JavaScript, si scrive codice inefficiente. Si consideri l'implementazione sopra del Persona tipo di dati: creazione di a Persona oggetto richiede il motore JavaScript per fare più lavoro e allocare più memoria.

Quindi, come si può usare il prototipo la proprietà rende questo codice più efficiente? Bene, prima diamo un'occhiata al codice refactored:

function Person (firstName, lastName) this.firstName = firstName; this.lastName = lastName;  Person.prototype.getFullName = function () return this.firstName + "" + this.lastName; ; var jeremy = new Person ("Jeremy", "McPeak"), jeffrey = new Person ("Jeffrey", "Way");

Qui, il getFullName () la definizione del metodo viene spostata dal costruttore e sul prototipo. Questo semplice cambiamento ha i seguenti effetti:

  • Il costruttore esegue meno lavoro e, quindi, esegue più velocemente (18% -96% più veloce). Esegui il test nel tuo browser, se lo desideri.
  • Il getFullName () il metodo viene creato una sola volta e condiviso tra tutti Persona oggetti (jeremy.getFullName === jeffrey.getFullName risultati in vero; esegui questo codice su http://jsfiddle.net/Pfkua/). Per questo motivo, ciascuno Persona l'oggetto usa meno memoria.

Fai riferimento alla Figura 1 e osserva come ogni oggetto ha il suo getFullName () metodo. Ora che getFullName () è definito sul prototipo, il diagramma dell'oggetto cambia ed è mostrato in Figura 2:

figura 2

Il jeremy e jeffrey gli oggetti non hanno più il loro getFullName () metodo, ma il motore JavaScript lo troverà Persona()Il prototipo. Nei vecchi motori JavaScript, il processo di ricerca di un metodo sul prototipo poteva comportare un calo di prestazioni, ma non così nei motori JavaScript di oggi. La velocità con cui i motori moderni trovano i metodi prototipati è estremamente veloce.

vita privata

Ma per quanto riguarda la privacy? Dopotutto, questo anti-modello è nato da un bisogno percepito di membri di oggetti privati. Se non hai familiarità con il modello, dai un'occhiata al seguente codice:

function Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = new Foo ("Hello, Privacy!"); alert (foo.bar ()); // avvisi "Ciao, Privacy!"

Questo codice definisce una funzione di costruzione chiamata Foo (), e ha un parametro chiamato paramOne. Il valore passato a Foo () è memorizzato in una variabile locale chiamata thisIsPrivate. Nota che thisIsPrivate è una variabile, non una proprietà; quindi, è inaccessibile al di fuori di Foo (). C'è anche un metodo definito all'interno del costruttore, e viene chiamato bar(). Perché bar() è definito all'interno Foo (), ha accesso al thisIsPrivate variabile. Quindi quando crei un foo oggetto e chiamata bar(), il valore assegnato a thisIsPrivate viene restituito.

Il valore assegnato a thisIsPrivate è conservato Non è possibile accedere al di fuori di Foo (), e quindi, è protetto da modifiche esterne. È grandioso, vero? Bene, sì e no. È comprensibile il motivo per cui alcuni sviluppatori desiderano emulare la privacy in JavaScript: è possibile garantire che i dati di un oggetto siano protetti da manomissioni esterne. Ma allo stesso tempo, si introduce inefficienza al proprio codice non utilizzando il prototipo.

Quindi, ancora, per quanto riguarda la privacy? Bene, è semplice: non farlo. Al momento, la lingua non supporta ufficialmente i membri degli oggetti privati, sebbene ciò possa cambiare in una futura revisione della lingua. Invece di usare chiusure per creare membri privati, la convenzione per indicare "membri privati" è quella di anteporre l'identificatore a un carattere di sottolineatura (es .: _thisIsPrivate). Il seguente codice riscrive l'esempio precedente utilizzando la convenzione:

funzione Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = function () return this._thisIsPrivate; ; var foo = new Foo ("Hello, Convention to Denote Privacy!"); alert (foo.bar ()); // avvisi "Salve, convenzione per denotare la privacy!"

No, non è privato, ma la convenzione di sottolineatura dice fondamentalmente "non toccarmi". Fino a quando JavaScript non supporta pienamente proprietà e metodi privati, я non preferiresti avere un codice più efficiente e performante della privacy? La risposta corretta è: sì!


Sommario

Dove definisci le funzioni nel tuo codice influisce sulle prestazioni della tua applicazione; tienilo a mente mentre scrivi il tuo codice. Non annidare le funzioni all'interno di una funzione chiamata di frequente. Fare così spreca i cicli della CPU. Per quanto riguarda le funzioni del costruttore, abbraccia il prototipo; in caso contrario si ottiene un codice inefficiente. Dopo tutto, gli sviluppatori scrivono software per gli utenti da utilizzare e le prestazioni di un'applicazione sono altrettanto importanti per l'esperienza dell'utente come l'interfaccia utente.