Opriți funcțiile de încadrare! (Dar nu toate)

JavaScript are peste 15 ani; cu toate acestea, limba este încă înțeles greșit de ceea ce este probabil majoritatea dezvoltatorilor și designerilor care folosesc limba. Una dintre cele mai puternice aspecte ale JavaScript este funcțiile. În timp ce sunt extrem de vitale pentru JavaScript, utilizarea lor necorespunzătoare poate introduce ineficiență și poate împiedica performanța unei aplicații.


Prefer un Video Tutorial?


Performanța este importantă

În copilăria Web-ului, performanța nu era foarte importantă.

În copilăria Web-ului, performanța nu era foarte importantă. Din conexiunile dial-up de 56K (sau mai rău) la un computer Pentium de 133MHz cu 8MB de memorie RAM, Web-ul era așteptat să fie lent (deși acest lucru nu a împiedicat pe toți să se plângă). Acesta a fost motivul pentru care JavaScript a fost creat pentru a începe, pentru a descărca procesarea simplă, cum ar fi validarea formularului, pentru ca browser-ul să facă anumite sarcini mai ușor și mai rapid pentru utilizatorul final. În loc să completați un formular, dați clic pe trimiteți și așteptați cel puțin treizeci de secunde pentru a vă spune că ați introdus date incorecte într-un câmp, autorii de Web autorizați de JavaScript pentru a vă valida datele și pentru a vă avertiza asupra oricărei erori înainte de depunerea formularului.

Repede până astăzi. Utilizatorii finali se bucură de computere multi-core și multi-GHz, o abundență de memorie RAM și viteze rapide de conectare. JavaScript nu mai este retrogradat la validarea formularului menial, dar poate procesa cantități mari de date, poate schimba orice parte a unei pagini în zbor, trimite și primi date de la server și poate adăuga interactivitate unei alte pagini statice - toate în nume de a îmbunătăți experiența utilizatorului. Este un model destul de bine cunoscut în toată industria calculatoarelor: o cantitate tot mai mare de resurse de sistem permite dezvoltatorilor să scrie mai multe sisteme de operare și software mai sofisticate și dependente de resurse. Dar chiar și cu această sumă abundentă și din ce în ce mai mare de resurse, dezvoltatorii trebuie să aibă grijă de cantitatea de resurse pe care o consumă aplicația lor - mai ales pe web.

Motoarele JavaScript de astăzi sunt cu ani lumină înainte de motoarele de acum zece ani, dar nu optimizează totul. Ceea ce nu optimizează este lăsat dezvoltatorilor.

Există, de asemenea, un set complet de noi dispozitive web, telefoane inteligente și tablete, care rulează pe un set limitat de resurse. Sistemele lor de operare și aplicațiile care au fost decupate sunt cu siguranță o lovitură, însă principalii furnizori de sisteme de operare mobile (și chiar vânzătorii de sisteme de operare desktop) se uită la tehnologiile Web ca platformă dezvoltatoare ale acestora, împingând dezvoltatorii JavaScript pentru a asigura că codul lor este eficient și performant.

O aplicație de performanță slabă va distruge o experiență bună.

Cel mai important, experiența utilizatorului depinde de performanța bună. Interfațele uimitoare și naturale contribuie cu siguranță la experiența unui utilizator, însă o aplicație slabă performantă va distruge o experiență bună. Dacă utilizatorii nu vor să utilizeze software-ul dvs., atunci care este punctul de scriere a acestuia? Deci, este absolut necesar ca, în această zi și la vârsta dezvoltării bazate pe Web, dezvoltatorii JavaScript să scrie cel mai bun cod posibil.

Deci, ce are de-a face cu funcțiile?

Unde definiți funcțiile dvs. are un impact asupra performanței aplicației dvs..

Există multe anti-șabloane JavaScript, însă funcțiile care au implicat au devenit oarecum populare - în special în mulțimea care încearcă să-i constrângă pe JavaScript pentru a emula funcții în alte limbi (caracteristici precum confidențialitatea). Este funcții de cuibărit în alte funcții și, dacă este făcută incorect, poate avea un efect negativ asupra aplicației dvs..

Este important să rețineți că acest model anti-model nu se aplică tuturor instanțelor funcțiilor imbricate, dar este de obicei definit de două caracteristici. În primul rând, crearea funcției în cauză este de obicei amânată - ceea ce înseamnă că funcția imbricată nu este creată de motorul JavaScript la încărcare. Acest lucru nu este un lucru rău, însă este a doua caracteristică care împiedică performanța: funcția imbricată este creată în mod repetat din cauza apelurilor repetate la funcția exterioară. Deci, în timp ce poate fi ușor să spunem că "toate funcțiile imbricate sunt rele", cu siguranță nu este cazul și veți putea identifica funcțiile imbricate problematice și le puteți repara pentru a vă accelera aplicația.


Funcțiile de asamblare în funcții normale

Primul exemplu al acestui model anti-șablon este inserarea unei funcții în interiorul unei funcții normale. Iată un exemplu prea simplificat:

funcția foo (a, b) funcția bar () return a + b;  bară de întoarcere ();  foo (1, 2);

Este posibil să nu scrieți acest cod exact, dar este important să recunoașteți modelul. O funcție externă, foo (), conține o funcție interioară, bar(), și face apel la această funcție interioară pentru a face muncă. Mulți dezvoltatori uită că funcțiile sunt valori în JavaScript. Când declarați o funcție în codul dvs., motorul JavaScript creează un obiect funcțional corespunzător - o valoare care poate fi atribuită unei variabile sau transferată unei alte funcții. Actul de creare a unui obiect funcțional seamănă cu orice alt tip de valoare; motorul JavaScript nu-l creează până nu are nevoie. Deci, în cazul codului de mai sus, motorul JavaScript nu creează interiorul bar() funcția până la foo () execută. Cand foo () ieșiri, bar() obiect de activitate este distrus.

Faptul că foo () are un nume presupune că va fi numit de mai multe ori în întreaga aplicație. În timp ce o execuție a foo () ar fi considerat OK, apelurile ulterioare cauzează munca inutilă pentru motorul JavaScript, deoarece trebuie să recreeze o bar() funcție de obiect pentru fiecare foo () execuţie. Deci, dacă sunați foo () De 100 de ori într-o aplicație, motorul JavaScript trebuie să creeze și să distrugă 100 bar() funcții. O afacere mare, nu? Motorul trebuie să creeze alte variabile locale într-o funcție de fiecare dată când se numește, deci de ce să îngrijești de funcții?

Spre deosebire de alte tipuri de valori, funcțiile de obicei nu se schimbă; o funcție este creată pentru a îndeplini o sarcină specifică. Deci, nu are sens să pierdeți ciclurile CPU, recreând o anumită valoare statică de peste și peste din nou.

În mod ideal, bar() funcția obiect în acest exemplu ar trebui să fie create doar o singură dată, și este ușor de realizat - deși în mod natural, funcțiile mai complexe pot necesita refactorizări extinse. Ideea este de a muta bar() declarație în afara foo () astfel încât obiectul funcției să fie creat o singură dată, astfel:

funcția foo (a, b) bara de întoarcere (a, b);  bara de funcții (a, b) return a + b;  foo (1, 2);

Observați că noul bar() funcția nu este exact așa cum a fost în interior foo (). Deoarece vechiul bar() a folosit funcția A și b parametrii în foo (), noua versiune avea nevoie de refactorizare pentru a accepta aceste argumente pentru a-și face munca.

În funcție de browser, acest cod optimizat este oriunde de la 10% la 99% mai rapid decât versiunea imbricată. Puteți vizualiza și executa testul pentru tine la adresa jsperf.com/nested-named-functions. Țineți minte simplitatea acestui exemplu. Un câștig de performanță de 10% (la cel mai mic nivel al spectrului de performanță) nu pare a fi o mulțime, dar ar fi mai mare, deoarece sunt implicate mai multe funcții imbricate și complexe.

Pentru a confunda problema, împachetați acest cod într-o funcție anonimă, de auto-execuție, cum ar fi:

(funcția () funcția foo (a, b) bara de retur (a, b); bara de funcții (a, b) return a + b; foo (1, 2);

Codul de codificare într-o funcție anonimă este un model obișnuit și, la prima vedere, acest cod poate replica problema de performanță menționată mai sus prin împachetarea codului optimizat într-o funcție anonimă. Deși există o ușoară performanță lovită de executarea funcției anonime, acest cod este perfect acceptabil. Funcția de auto-execuție servește numai pentru a conține și a proteja foo () și bar() funcții, dar mai important, funcția anonimă este executată doar o singură dată - astfel, interiorul foo () și bar() funcțiile sunt create doar o singură dată. Cu toate acestea, există unele cazuri în care funcțiile anonime sunt la fel (sau mai mult) problematice ca funcții numite.


Funcții anonime

În ceea ce privește acest subiect de performanță, funcțiile anonime au potențialul de a fi mai periculoase decât funcțiile numite.

Nu este anonimatul funcției periculoase, ci modul în care dezvoltatorii le folosesc. Este destul de obișnuit să utilizați funcții anonime când configurați funcții de gestionare a evenimentelor, funcții de apel invers sau funcții iterator. De exemplu, următorul cod atribuie a clic eveniment ascultător pe document:

document.addEventListener ("faceți clic pe", funcția (evt) alert ("Ați făcut clic pe pagină"););

Aici este transmisă o funcție anonimă addEventListener () metodă de conectivitate clic evenimentul pe document; astfel încât funcția se execută de fiecare dată când utilizatorul face clic pe oriunde pe pagină. Pentru a demonstra o altă utilizare obișnuită a funcțiilor anonime, luați în considerare acest exemplu care utilizează biblioteca jQuery pentru a selecta toate elemente în document și iterați cu ele fiecare() metodă:

$ ("a") fiecare (funcție (index) this.style.color = "roșu";);

În acest cod, funcția anonimă a trecut la obiectul jQuery fiecare() metoda se execută pentru fiecare element găsit în document. Spre deosebire de funcțiile numite, unde se presupune că sunt numite în mod repetat, executarea repetată a unui număr mare de funcții anonime este destul de explicită. Este imperativ, din motive de performanță, că acestea sunt eficiente și optimizate. Aruncați o privire la pluginul jQuery care urmează (încă o dată simplificat):

$ .fn.myPlugin = funcția (opțiuni) return this.each (funcția () var $ this = $ (aceasta); funcția changeColor () $ this.css (color: options.color ();); ;

Acest cod definește un plugin extrem de simplu numit myPlugin; este atât de simplu încât multe trăsături comune de plugin-uri sunt absente. În mod normal, definițiile pluginului sunt înfășurate în cadrul funcțiilor anonime auto-executante și, de obicei, sunt furnizate valori implicite pentru opțiuni pentru a asigura că datele valide disponibile sunt disponibile. Aceste lucruri au fost eliminate din motive de claritate.

Scopul acestui plugin este de a schimba culoarea elementelor selectate la ceea ce este specificat în Opțiuni obiect trecut la myPlugin () metodă. Ea face acest lucru prin trecerea unei funcții anonime la fiecare() iterator, făcând această funcție executată pentru fiecare element din obiectul jQuery. În interiorul funcției anonime, se numește o funcție interioară schimba culoarea() face munca reală de a schimba culoarea elementului. Așa cum este scris, acest cod este ineficient, deoarece, ați ghicit-o, schimba culoarea() funcția este definită în interiorul funcției de iterare? făcând motorul JavaScript recreat schimba culoarea() cu fiecare iterație.

Efectuarea acestui cod mai eficient este destul de simplă și urmează același model ca înainte: refactorul schimba culoarea() funcția care trebuie definită în afara oricăror funcții care îi conțin și îi permite să primească informațiile necesare pentru a-și face munca. În acest caz, schimba culoarea() are nevoie de obiectul jQuery și de noua valoare de culoare. Codul îmbunătățit arată astfel:

funcția changeColor ($ obj, culoare) $ obj.css (culoare: culoare);  $ .fn.myPlugin = funcția (opțiuni) return this.each (funcția () var $ this = $ (aceasta); changeColor ($ this, options.color);); ;

Interesant, acest cod optimizat crește performanța cu o marjă mult mai mică decât cea foo () și bar() de exemplu, cu Chrome care duce pachetul cu un câștig de performanță de 15% (jsperf.com/function-nesting-with-jquery-plugin). Adevarul este ca accesarea DOM si folosirea API-ului jQuery adauga propriul hit la performanta - in special jQuery's fiecare(), care este notoriu lent în comparație cu buclele native ale JavaScript. Dar, ca și înainte, țineți minte simplitatea acestui exemplu. Cele mai multe funcții imbricate, cu atât este mai mare câștigul de performanță din optimizare.

Funcțiile de asamblare în funcțiile constructorului

O altă variantă a acestui model anti-model este funcțiile de cuibărit în cadrul constructorilor, după cum se arată mai jos:

funcția Persoană (nume, prenume) this.firstName = firstName; this.lastName = lastName; this.getFullName = funcția () return this.firstName + "" + this.lastName; ;  var jeremy = persoană nouă ("Jeremy", "McPeak"), jeffrey = persoană nouă ("Jeffrey", "Way");

Acest cod definește o funcție constructor numită Persoană(), și reprezintă (dacă nu era evident) o persoană. Acceptă argumente care conțin numele și prenumele unei persoane și stochează aceste valori în Nume și numele de familie proprietăți, respectiv. Constructorul creează, de asemenea, o metodă numită getFullName (); acesta concatenează Nume și numele de familie și returnează valoarea șirului rezultat.

Când creați orice obiect în JavaScript, obiectul este stocat în memorie

Acest model a devenit destul de comun în comunitatea de astăzi a JavaScript, deoarece poate emula confidențialitatea, o caracteristică pe care JavaScript nu este în prezent proiectată pentru (rețineți că confidențialitatea nu este în exemplul de mai sus, veți examina mai târziu acest lucru). Dar, folosind acest model, dezvoltatorii creează ineficiență nu numai în timpul de execuție, ci și în utilizarea memoriei. Când creați orice obiect în JavaScript, obiectul este stocat în memorie. Acesta rămâne în memorie până când toate referințele la acesta sunt fie setate nul sau sunt în afara domeniului de aplicare. În cazul jeremy obiect în codul de mai sus, funcția atribuită getFullName este de obicei stocat în memorie atâta timp cât jeremy obiect este în memorie. Cand jeffrey obiect este creat, un nou obiect de funcții este creat și atribuit jeffrey„s getFullName membru, și prea consumă memorie atât timp cât jeffrey este în memorie. Problema este aici jeremy.getFullName este un obiect de funcție diferit de jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName rezultatele în fals; rulați acest cod la http://jsfiddle.net/k9uRN/). Ambele au același comportament, dar ele sunt două obiecte funcționale complet diferite (și astfel fiecare consumă memorie). Pentru claritate, aruncați o privire la Figura 1:

figura 1

Aici vedeți jeremy și jeffrey obiecte, fiecare dintre ele având propriile lor getFullName () metodă. Deci, fiecare Persoană obiect creat are propriul său unic getFullName () - fiecare dintre ele consumându-și propria bucată de memorie. Imaginați-vă că ați creat 100 Persoană obiecte: dacă fiecare getFullName () metoda consumă 4KB de memorie, apoi 100 Persoană obiectele ar consuma cel puțin 400KB de memorie. Acest lucru se poate adăuga, dar poate fi redus drastic prin utilizarea prototip obiect.

Utilizați prototipul

Așa cum am menționat mai devreme, funcțiile sunt obiecte în JavaScript. Toate obiectele de funcții au a prototip proprietate, dar este utilă numai pentru funcțiile constructorului. Pe scurt, prototip proprietatea este literalmente un prototip pentru crearea obiectelor; ceea ce este definit pe prototipul unei funcții constructor este împărțit între toate obiectele create de funcția constructorului respectiv.

Din păcate, prototipurile nu sunt suficient de stresate în educația JavaScript.

Din păcate, prototipurile nu sunt suficient de stresate în educația JavaScript, totuși ele sunt absolut esențiale pentru JavaScript, deoarece sunt bazate pe și construite cu prototipuri - este un limbaj prototip. Chiar dacă nu ați scris niciodată cuvântul prototip în codul dvs., ele sunt folosite în spatele scenei. De exemplu, fiecare metodă nativă bazată pe șir, cum ar fi Despică(), substr (), sau a inlocui(), sunt definite pe Şir()e prototipul. Prototipurile sunt atât de importante pentru limbajul JavaScript, încât dacă nu îmbrățișați natura prototipală a JavaScript, scrieți un cod ineficient. Luați în considerare implementarea de mai sus a Persoană tip de date: crearea unui Persoană obiect necesită motorul JavaScript pentru a face mai multă muncă și a aloca mai multă memorie.

Deci, cum poate folosi prototip de proprietate face acest cod mai eficient? Ei bine, mai întâi aruncăm o privire asupra codului refactat:

funcția Persoană (nume, prenume) this.firstName = firstName; this.lastName = lastName;  Person.prototype.getFullName = funcția () return this.firstName + "" + this.lastName; ; var jeremy = persoană nouă ("Jeremy", "McPeak"), jeffrey = persoană nouă ("Jeffrey", "Way");

Aici getFullName () definiția metodei este mutată din constructor și pe prototip. Această modificare simplă are următoarele efecte:

  • Constructorul realizează mai puțină muncă și, prin urmare, execută mai repede (cu 18% -96% mai rapid). Rulați testul în browser dacă doriți.
  • getFullName () metoda este creată doar o singură dată și împărțită între toți Persoană obiecte (jeremy.getFullName === jeffrey.getFullName rezultatele în Adevărat; executați acest cod la http://jsfiddle.net/Pfkua/). Din cauza asta, fiecare Persoană obiect utilizează mai puțină memorie.

Reveniți la figura 1 și notați cum are fiecare obiect propriu getFullName () metodă. Acum că getFullName () este definit pe prototip, diagrama obiectului se modifică și este prezentată în Figura 2:

Figura 2

jeremy și jeffrey obiectele nu mai au propriile lor getFullName () , dar motorul JavaScript îl va găsi Persoană()e prototipul. În motoarele JavaScript mai vechi, procesul de găsire a unei metode pe prototip ar putea genera o lovitură de performanță, dar nu și în motoarele JavaScript de astăzi. Viteza la care motoarele moderne găsesc metode prototip este extrem de rapidă.

intimitate

Dar ce intimitate? La urma urmei, acest model anti-model a fost născut dintr-o nevoie percepută de membri de obiecte private. Dacă nu sunteți familiarizat cu modelul, aruncați o privire la următorul cod:

funcția Foo (paramOne) var thisIsPrivate = paramOne; this.bar = funcția () return thisIsPrivate; ;  var foo = nou Foo ("Bună ziua, confidențialitate!"); alert (foo.bar ()); // alerte "Bună ziua, confidențialitate!"

Acest cod definește o funcție constructor numită Foo (), și are un parametru numit paramOne. Valoarea a trecut Foo () este stocată într-o variabilă locală numită thisIsPrivate. Rețineți că thisIsPrivate este o variabilă, nu o proprietate; deci, este inaccesibil in afara Foo (). Există și o metodă definită în interiorul constructorului și se numește bar(). pentru că bar() este definit în cadrul Foo (), are acces la thisIsPrivate variabil. Atunci când creați un foo obiect și apel bar(), valoarea atribuită thisIsPrivate este returnat.

Valoarea atribuită thisIsPrivate este păstrată. Nu poate fi accesat în afara lui Foo (), și, prin urmare, este protejat de modificările din exterior. E minunat, nu? Ei bine, da și nu. Este de înțeles de ce unii dezvoltatori doresc să emuleze confidențialitatea în JavaScript: puteți să vă asigurați că datele unui obiect sunt securizate de la manipularea în exterior. Dar, în același timp, introduceți ineficiența codului dvs. prin faptul că nu utilizați prototipul.

Deci, din nou, cum rămâne cu intimitatea? Ei bine, e simplu: nu o face. În prezent, limba nu acceptă în mod oficial membrii obiectului privat - deși acest lucru se poate schimba într-o revizuire ulterioară a limbii. În loc să se utilizeze închideri pentru a crea membri privați, convenția de a denumi "membrii privați" este de a prefixa identificatorul cu o subliniere (adică: _thisIsPrivate). Următorul cod rescrie exemplul anterior utilizând convenția:

funcția Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = funcția () return this._thisIsPrivate; ; var foo = noul Foo ("Bună ziua, Convenția de a denumi confidențialitatea!"); alert (foo.bar ()); // alerte "Bună ziua, Convenția pentru a denumi confidențialitatea!"

Nu, nu este privat, dar convenția de subliniere spune în esență "nu mă atinge". Până când JavaScript suportă pe deplin proprietățile și metodele private, nu ați prefera să aveți un cod mai eficient și mai performant decât confidențialitatea? Răspunsul corect este: da!


rezumat

Unde definiți funcții în codul dvs. influențează performanța aplicației dvs.; Țineți minte în timp ce vă scrieți codul. Nu asamblați funcții în interiorul unei funcții numite frecvent. Acest lucru duce la risipa ciclurilor CPU. În ceea ce privește funcțiile constructorului, îmbrățișați prototipul; eșecul de a face acest lucru duce la un cod ineficient. La urma urmei, dezvoltatorii scriu software-ul pentru utilizatori, iar performanța unei aplicații este la fel de importantă pentru experiența utilizatorului ca UI.

Cod