Stopp Nesting Funksjoner! (Men ikke alle av dem)

JavaScript er over femten år gammel; Likevel er språket fortsatt misforstått av det som kanskje er flertallet av utviklere og designere som bruker språket. En av de mest kraftfulle, men likevel misforstått, aspektene av JavaScript er funksjoner. Selv om det er veldig viktig for JavaScript, kan deres feilbruk introdusere ineffektivitet og hindre et programs ytelse.


Foretrekker en videoopplæring?


Ytelsen er viktig

I nettets barndom var ytelsen ikke så viktig.

I nettets barndom var ytelsen ikke så viktig. Fra 56K (eller verre) oppringt tilkobling til en sluttbrukers 133MHz Pentium-datamaskin med 8 MB RAM, ble weben forventet å være sakte (selv om det ikke stoppet alle fra å klage på det). Det var derfor JavaScript ble opprettet til å begynne med, for å avlaste enkel behandling, for eksempel form validering, til nettleseren, noe som gjør enkelte oppgaver enklere og raskere for sluttbrukeren. I stedet for å fylle ut et skjema, klikke på innlevering og venter i minst tretti sekunder å bli fortalt, har du skrevet feil data i et felt, JavaScript-aktiverte webforfattere kan validere innspillet og varsle deg om eventuelle feil før skjemaets innsending.

Rask frem til i dag. Sluttbrukere kan nyte multi-core og multi-GHz-datamaskiner, en overflod av RAM og hurtige tilkoblingshastigheter. JavaScript er ikke lenger forflyttet til validering av menyformularer, men det kan behandle store mengder data, endre hvilken som helst del av en side i fly, sende og motta data fra serveren, og legg til interaktivitet til en ellers statisk side - alt i navnet for å øke brukerens opplevelse. Det er et mønster som er ganske godt kjent i hele datamaskinen. En økende mengde systemressurser gjør det mulig for utviklere å skrive mer sofistikerte og ressursavhengige operativsystemer og programvare. Men selv med denne rikelige og stadig voksende mengden ressurser, må utviklerne være oppmerksom på hvor mange ressurser deres app bruker - spesielt på nettet.

Dagens JavaScript-motorer er lysår foran motorene for ti år siden, men de optimaliserer ikke alt. Det de ikke optimaliserer, er igjen for utviklere.

Det er også et helt nytt sett med nettaktiverte enheter, smarte telefoner og tabletter, som kjører på et begrenset sett med ressurser. Deres trimmede operativsystemer og applikasjoner er sikkert en hit, men de store operatørene av mobiloperatører (og til og med PC-operatører) ser på webteknologi som deres utviklingsplattform av valg, og skyver JavaScript-utviklere for å sikre at koden er effektiv og effektiv..

En dårlig utførende søknad vil søppel en god opplevelse.

Viktigst av alt er brukerens opplevelse avhengig av god ytelse. Ganske og naturlige brukergrensesnitt legger sikkert til en brukers opplevelse, men en dårlig utførelse vil gi en god opplevelse. Hvis brukerne ikke vil bruke programvaren, hva er meningen med å skrive det? Så det er helt avgjørende at i denne dag og alder av web-sentrisk utvikling skriver JavaScript-utviklere den beste koden mulig.

Så hva har alt dette å gjøre med funksjoner?

Hvor du definerer dine funksjoner, har en innvirkning på programmets ytelse.

Det er mange JavaScript-antimønstre, men en som involverer funksjoner har blitt noe populært, spesielt i mengden som strever for å tvinge JavaScript til å etterligne funksjoner på andre språk (funksjoner som personvern). Det er nesting funksjoner i andre funksjoner, og hvis gjort feil, kan det ha en negativ innvirkning på din søknad.

Det er viktig å merke seg at dette antimønsteret ikke gjelder for alle forekomster av nestede funksjoner, men det er vanligvis definert av to egenskaper. For det første blir opprettelsen av den aktuelle funksjonen vanligvis utsatt, noe som betyr at den nestede funksjonen ikke opprettes av JavaScript-motoren på load-time. Det som i seg selv er ikke en dårlig ting, men det er den andre egenskapen som hindrer ytelse: Den nestede funksjonen opprettes gjentatte ganger på grunn av gjentatte anrop til den ytre funksjonen. Så mens det kan være lett å si at "alle nestede funksjoner er dårlige", er det sikkert ikke tilfelle, og du vil kunne identifisere problematiske nestede funksjoner og fikse dem for å øke hastigheten på søknaden din.


Nestende funksjoner i normale funksjoner

Det første eksemplet på dette anti-mønsteret er å nesting en funksjon i en normal funksjon. Her er et forenklet eksempel:

funksjon foo (a, b) funksjonsfelt () return a + b;  returlinje ();  foo (1, 2);

Du kan ikke skrive denne nøyaktige koden, men det er viktig å gjenkjenne mønsteret. En ytre funksjon, foo (), inneholder en indre funksjon, bar (), og kaller den indre funksjon for å gjøre arbeid. Mange utviklere glemmer at funksjoner er verdier i JavaScript. Når du erklære en funksjon i koden, oppretter JavaScript-motoren et tilsvarende funksjonsobjekt - en verdi som kan tilordnes en variabel eller overføres til en annen funksjon. Handlingen med å skape en funksjonsobjekt ligner den av enhver annen type verdi; JavaScript-motoren oppretter ikke den før den trenger. Så i tilfelle av ovennevnte kode, skaper ikke JavaScript-motoren det indre bar () fungere til foo () utfører. Når foo () Utganger, bar () funksjonsobjektet er ødelagt.

Det faktum at foo () Har et navn innebærer at det vil bli kalt flere ganger i hele applikasjonen. Mens en utførelse av foo () vil bli vurdert OK, etterfølgende anrop forårsaker unødvendig arbeid for JavaScript-motoren fordi den må gjenskape en bar () funksjonsobjekt for hver foo () henrettelse. Så, hvis du ringer foo () 100 ganger i et program, må JavaScript-motoren lage og ødelegge 100 bar () funksjonsobjekter. Big deal, ikke sant? Motoren må skape andre lokale variabler i en funksjon hver gang den kalles, så hvorfor bryr seg om funksjoner?

I motsetning til andre typer verdier, endres funksjonene vanligvis ikke; en funksjon er opprettet for å utføre en bestemt oppgave. Så det gir ikke stor mening å kaste bort CPU-sykluser som gjenskaper en noe statisk verdi igjen og igjen.

Ideelt sett er det bar () Funksjonsobjektet i dette eksemplet bør bare opprettes en gang, og det er lett å oppnå - selv om det naturligere kan mer komplekse funksjoner kreve omfattende refaktoring. Tanken er å flytte bar () erklæring utenfor foo () slik at funksjonsobjektet kun opprettes en gang, slik:

funksjon foo (a, b) returfelt (a, b);  funksjonslinjen (a, b) return a + b;  foo (1, 2);

Legg merke til at den nye bar () funksjonen er ikke akkurat som den var inne i foo (). Fordi den gamle bar () funksjonen brukes en og b parametre i foo (), den nye versjonen trengte refactoring for å akseptere disse argumentene for å kunne utføre sitt arbeid.

Avhengig av nettleseren, er denne optimaliserte koden alt fra 10% til 99% raskere enn den nestede versjonen. Du kan se og kjøre testen for deg selv på jsperf.com/nested-named-functions. Husk enkelheten i dette eksemplet. En 10% (ved laveste ende av ytelsespekteret) virker ikke som mye, men det ville være høyere som mer innlejrede og komplekse funksjoner er involvert.

For å kanskje forveksle problemet, pakk denne koden i en anonym, selvutførende funksjon, slik:

(funksjon () funksjon foo (a, b) returfelt (a, b); funksjonsfelt (a, b) return a + b; foo (1,2); ());

Innpakningskode i en anonym funksjon er et vanlig mønster, og ved første øyekast kan det oppstå at denne koden replikerer det nevnte ytelsesproblemet ved å pakke inn den optimaliserte koden i en anonym funksjon. Mens det er en liten ytelse som treffes ved å utføre den anonyme funksjonen, er denne koden helt akseptabel. Den selvutførende funksjonen tjener kun til å inneholde og beskytte foo () og bar () Funksjoner, men enda viktigere, utfører den anonyme funksjonen bare en gang-dermed den indre foo () og bar () Funksjoner opprettes kun én gang. Det er imidlertid noen tilfeller der anonyme funksjoner er like (eller flere) problematiske som navngitte funksjoner.


Anonyme funksjoner

Når det gjelder dette emnet med ytelse, har anonyme funksjoner potensialet til å være farligere enn navngitte funksjoner.

Det er ikke anonymiteten til funksjonen som er farlig, men det er hvordan utviklere bruker dem. Det er ganske vanlig å bruke anonyme funksjoner når du konfigurerer hendelsesbehandlere, tilbakeringingsfunksjoner eller iteratorfunksjoner. For eksempel tilordner følgende kode a klikk hendelseslytter på dokumentet:

document.addEventListener ("klikk", funksjon (evt) alert ("Du klikket på siden."););

Her sendes en anonym funksjon til addEventListener () metode for å koble opp klikk hendelse på dokumentet; Funksjonen utføres hver gang brukeren klikker hvor som helst på siden. For å demonstrere en annen vanlig bruk av anonyme funksjoner, vurder dette eksemplet som bruker jQuery-biblioteket til å velge alt elementer i dokumentet og iterate over dem med Hver() metode:

$ ("a"). hver (funksjon (indeks) this.style.color = "rød";);

I denne koden, gikk den anonyme funksjonen til jQuery-objektets Hver() Metoden kjører for hver element funnet i dokumentet. I motsetning til navngitte funksjoner, hvor de antas å bli gjentatte ganger kalt, er gjentatt utførelse av et stort antall anonyme funksjoner ganske eksplisitte. Det er avgjørende, for ytelses skyld, at de er effektive og optimalisert. Ta en titt på følgende (enda en gang oversimplified) jQuery-plugin:

$ .fn.myPlugin = funksjon (alternativer) return this.each (funksjon () var $ this = $ (dette); funksjonsendringColor () $ this.css (color: options.color); changeColor ();); ;

Denne koden definerer et ekstremt enkelt plugin som heter myPlugin; det er så enkelt at mange vanlige pluggegenskaper er fraværende. Vanligvis er plugindefinisjoner pakket inn i selvutførende anonyme funksjoner, og vanligvis leveres standardverdier for alternativer for å sikre at gyldige data er tilgjengelige for bruk. Disse tingene har blitt fjernet for klarhetens skyld.

Dette pluginets formål er å endre de valgte elementernes farge til det som er angitt i opsjoner objekt bestått til myPlugin () metode. Det gjør det ved å sende en anonym funksjon til Hver() iterator, slik at denne funksjonen utføres for hvert element i jQuery-objektet. I den anonyme funksjonen kalles en indre funksjon changeColor () gjør det faktiske arbeidet med å endre elementets farge. Som skrevet, er denne koden ineffektiv fordi du gjettet det, den changeColor () Funksjonen er definert inne i itereringsfunksjonen? gjør JavaScript-motoren gjenskapt changeColor () med hver iterasjon.

Å gjøre denne koden mer effektiv er ganske enkel og følger samme mønster som før: refactor the changeColor () Funksjonen skal defineres utenfor eventuelle funksjoner, og tillate den å motta den informasjonen den trenger for å utføre sitt arbeid. I dette tilfellet, changeColor () trenger jQuery-objektet og den nye fargeværdien. Den forbedrede koden ser slik ut:

funksjon changeColor ($ obj, farge) $ obj.css (color: color);  $ .fn.myPlugin = funksjon (alternativer) return this.each (funksjon () var $ this = $ (dette); changeColor ($ dette, options.color);); ;

Interessant, denne optimaliserte koden øker ytelsen med en mye mindre margin enn foo () og bar () Eksempel, med Chrome som leder pakken med en ytelsesgevinst på 15% (jsperf.com/function-nesting-with-jquery-plugin). Sannheten er at du får tilgang til DOM og bruker jQuery API til å legge til egen treff til ytelse, spesielt jQuery s Hver(), som er notorisk sakte i forhold til JavaScript's native looper. Men som før, husk enkelheten i dette eksemplet. Jo mer innkapslede funksjoner, desto større ytelse gevinst fra optimalisering.

Nesting Funksjoner i Constructor Funksjoner

En annen variant av dette anti-mønsteret er nesting funksjoner innenfor konstruktører, som vist nedenfor:

funksjon Person (fornavn, etternavn) this.firstName = firstName; this.lastName = lastName; this.getFullName = function () return this.firstName + "" + this.lastName; ;  var jeremy = ny person ("Jeremy", "McPeak"), jeffrey = ny person ("Jeffrey", "Way");

Denne koden definerer en konstruktørfunksjon som kalles Person(), og det representerer (hvis det ikke var åpenbart) en person. Den aksepterer argumenter som inneholder en persons for- og etternavn og lagrer disse verdiene i fornavn og etternavn eiendommer, henholdsvis. Konstruktøren oppretter også en metode som kalles getFullName (); det sammenkaller fornavn og etternavn egenskaper og returnerer den resulterende strengverdien.

Når du lager et objekt i JavaScript, lagres objektet i minnet

Dette mønsteret har blitt ganske vanlig i dagens JavaScript-fellesskap fordi det kan etterligne personvern, en funksjon som JavaScript ikke er for tiden designet for (merk at privatliv ikke er i eksempelet ovenfor, du vil se på det senere). Men ved å bruke dette mønsteret utvikler utviklere ineffektivitet, ikke bare i kjøretid, men i minnebruk. Når du lager et objekt i JavaScript, lagres objektet i minnet. Den forblir i minnet til alle referanser til det er enten satt til null eller er ute av bruk. I tilfelle av jeremy objekt i ovennevnte kode, funksjonen tilordnet til getFullName lagres vanligvis i minnet så lenge som jeremy objektet er i minnet. Når jeffrey objekt er opprettet, et nytt funksjonsobjekt blir opprettet og tildelt til jeffrey's getFullName medlem, og det bruker også minne for så lenge som jeffrey er i minnet. Problemet her er det jeremy.getFullName er et annet funksjonsobjekt enn jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName resulterer i falsk; Kjør denne koden på http://jsfiddle.net/k9uRN/). De har begge samme oppførsel, men de er to helt forskjellige funksjonsobjekter (og dermed hver forbruker minne). For klarhet, ta en titt på figur 1:

Figur 1

Her ser du jeremy og jeffrey objekter, som hver har sin egen getFullName () metode. Så, hver Person objekt opprettet har sin egen unike getFullName () metode som hver bruker sin egen del av minne. Tenk deg å skape 100 Person objekter: hvis hver getFullName () Metoden bruker 4KB minne, deretter 100 Person objekter vil forbruke minst 400 kb minne. Det kan legge opp, men det kan reduseres drastisk ved å bruke prototype gjenstand.

Bruk prototypen

Som nevnt tidligere er funksjonene objekter i JavaScript. Alle funksjonsobjekter har a prototype eiendom, men det er bare nyttig for konstruktørfunksjoner. Kort sagt, prototype eiendom er ganske bokstavelig talt en prototype for å skape objekter; hva som er definert på en konstruktørfunksjonens prototype deles mellom alle objekter opprettet av den konstruktørfunksjonen.

Dessverre er prototyper ikke stresset nok i JavaScript-utdanning.

Dessverre er prototyper ikke stresset nok i JavaScript-utdanning, men de er helt avgjørende for JavaScript fordi det er basert på og bygget med prototyper-det er et prototypisk språk. Selv om du aldri skrev ordet prototype i koden din blir de brukt bak kulissene. For eksempel, hver innfødt strengbasert metode, som dele(), substr (), eller erstatte(), er definert på Streng ()prototype. Prototyper er så viktige for JavaScript-språket at hvis du ikke omfavner JavaScript's prototypiske natur, skriver du ineffektiv kode. Tenk over implementeringen av denne Person datatype: opprette a Person objekt krever JavaScript-motoren for å gjøre mer arbeid og tildele mer minne.

Så, hvordan kan du bruke prototype eiendom gjør denne koden mer effektiv? Vel, ta en titt på refactored koden:

funksjon Person (fornavn, etternavn) this.firstName = firstName; this.lastName = lastName;  Person.prototype.getFullName = funksjon () return this.firstName + "" + this.lastName; ; var jeremy = ny person ("Jeremy", "McPeak"), jeffrey = ny person ("Jeffrey", "Way");

Her, den getFullName () Metoddefinisjonen flyttes ut av konstruktøren og på prototypen. Denne enkle endringen har følgende effekter:

  • Konstruktøren utfører mindre arbeid, og utfører dermed raskere (18% -96% raskere). Kjør testen i nettleseren din hvis du vil.
  • De getFullName () Metoden er opprettet bare en gang og deles blant alle Person objekter (jeremy.getFullName === jeffrey.getFullName resulterer i ekte; Kjør denne koden på http://jsfiddle.net/Pfkua/). På grunn av dette, hver Person objektet bruker mindre minne.

Se tilbake til Figur 1 og noter hvordan hver gjenstand har sin egen getFullName () metode. Nå som getFullName () er definert på prototypen, endres objektdiagrammet og vises i figur 2:

Figur 2

De jeremy og jeffrey objekter har ikke lenger sin egen getFullName () metode, men JavaScript-motoren vil finne den på Person()prototype. I eldre JavaScript-motorer kan prosessen med å finne en metode på prototypen påføre en ytelses hit, men ikke så i dagens JavaScript-motorer. Hastigheten der moderne motorer finner prototypede metoder, er ekstremt rask.

Personvern

Men hva med privatliv? Tross alt ble dette anti-mønsteret født ut av et oppfattet behov for private objektmedlemmer. Hvis du ikke er kjent med mønsteret, ta en titt på følgende kode:

funksjon Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = ny Foo ("Hei, Personvern!"); alert (foo.bar ()); // varsler "Hei, Personvern!"

Denne koden definerer en konstruktørfunksjon som kalles Foo (), og den har en parameter kalt paramOne. Verdien gikk til Foo () lagres i en lokal variabel som kalles thisIsPrivate. Noter det thisIsPrivate er en variabel, ikke en eiendom; så det er utilgjengelig utenfor Foo (). Det er også en metode som er definert i konstruktøren, og den kalles bar (). Fordi bar () er definert i Foo (), den har tilgang til thisIsPrivate variabel. Så når du lager en foo objekt og ring bar (), verdien tilordnet thisIsPrivate returneres.

Verdien tildelt thisIsPrivate er bevart. Det kan ikke nås utenfor Foo (), og dermed er den beskyttet mot utvendig modifikasjon. Det er flott, ikke sant? Vel, ja og nei. Det er forståelig hvorfor noen utviklere ønsker å etterligne personvern i JavaScript: Du kan sikre at en objekts data er sikret fra utenfor manipulering. Men samtidig presenterer du ineffektivitet til koden din ved ikke å bruke prototypen.

Så igjen, hva med privatliv? Vel det er enkelt: ikke gjør det. Språket støtter for øyeblikket ikke offisielt medlemmene til private medlemmer, selv om det kan endres i en fremtidig revisjon av språket. I stedet for å bruke nedleggelser for å opprette private medlemmer, er konvensjonen for å betegne "private medlemmer" å legge inn identifikatoren med et understreke (dvs. _thisIsPrivate). Følgende kode omskriver forrige eksempel ved bruk av konvensjonen:

funksjon Foo (paramOne) this.thisIsPrivate = paramOne;  Foo.prototype.bar = function () returner dette._thisIsPrivate; ; var foo = ny Foo ("Hei, konvensjon for å betegne personvern!"); alert (foo.bar ()); // varsler "Hei, Konvensjon for å betegne personvern!"

Nei, det er ikke privat, men underskriftskonvensjonen sier i utgangspunktet "ikke rør meg." Inntil JavaScript fullt ut støtter private eiendommer og metoder, vil du ikke ha mer effektiv og effektiv kode enn personvern? Det riktige svaret er: ja!


Sammendrag

Hvor du definerer funksjoner i koden påvirker programmets ytelse; Husk at du skriver koden din. Ikke nest funksjonene inne i en ofte kalt funksjon. Gjøre det så avfall CPU sykluser. Når det gjelder konstruktørfunksjoner, omfavn prototypen; manglende å gjøre det resulterer i ineffektiv kode. Tross alt, utviklere skrive programvare for brukere å bruke, og en applikasjon ytelse er like viktig for brukerens opplevelse som brukergrensesnittet.