Stop met nesten Functies! (Maar niet allemaal)

JavaScript is ouder dan vijftien jaar; desondanks wordt de taal nog steeds verkeerd begrepen door wat misschien de meerderheid van ontwikkelaars en ontwerpers is die de taal gebruiken. Een van de meest krachtige, maar toch onbegrepen aspecten van JavaScript zijn functies. Hoewel het van vitaal belang is voor JavaScript, kan misbruik ervan inefficiëntie veroorzaken en de prestaties van een toepassing hinderen.


Liever een video-zelfstudie?


Prestaties zijn belangrijk

In de kindertijd van het web waren de prestaties niet erg belangrijk.

In de kindertijd van het web waren de prestaties niet erg belangrijk. Van de 56K (of erger) dial-up verbindingen naar een 133MHz Pentium computer van een eindgebruiker met 8MB RAM, werd verwacht dat het web traag zou zijn (hoewel dat niet iedereen ervan weerhield om erover te klagen). Om deze reden werd JavaScript om te beginnen gemaakt, om eenvoudige verwerking, zoals formuliervalidatie, aan de browser over te dragen, waardoor bepaalde taken eenvoudiger en sneller voor de eindgebruiker werden. In plaats van een formulier in te vullen, te klikken op verzenden en ten minste dertig seconden wachten om te worden verteld dat u onjuiste gegevens in een veld hebt ingevoerd, heeft JavaScript webauteurs toegestaan ​​uw invoer te valideren en u te waarschuwen voor eventuele fouten voorafgaand aan het indienen van het formulier.

Snel vooruit naar vandaag. Eindgebruikers genieten van multi-core en multi-GHz computers, een overvloed aan RAM en snelle verbindingssnelheden. JavaScript is niet langer gedegradeerd tot officiële formuliervalidatie, maar het kan grote hoeveelheden gegevens verwerken, elk deel van een pagina on the fly wijzigen, gegevens verzenden en ontvangen van de server en interactiviteit toevoegen aan een anders statische pagina - alles in de naam van het verbeteren van de gebruikerservaring. Het is een patroon dat algemeen bekend is in de computerindustrie: een groeiend aantal systeembronnen stelt ontwikkelaars in staat om meer geavanceerde en bronafhankelijke besturingssystemen en software te schrijven. Maar zelfs met deze overvloedige en steeds groeiende hoeveelheid bronnen moeten ontwikkelaars zich bewust zijn van de hoeveelheid bronnen die hun app verbruikt, vooral op internet.

De huidige JavaScript-engines lopen lichtjaren voor op de motoren van tien jaar geleden, maar ze optimaliseren niet alles. Wat ze niet optimaliseren, wordt overgelaten aan ontwikkelaars.

Er is ook een hele nieuwe set web-enabled apparaten, smartphones en tablets, die op een beperkte reeks bronnen draaien. Hun afgeslankte besturingssystemen en apps zijn zeker een hit, maar de grote leveranciers van mobiele OS (en zelfs desktop OS-verkopers) kijken naar webtechnologieën als hun ontwikkelaarsplatform naar keuze, waardoor JavaScript-ontwikkelaars ervoor zorgen dat hun code efficiënt en performant is.

Een slecht presterende applicatie zal een goede ervaring verwoesten.

Het belangrijkste is dat de gebruikerservaring afhankelijk is van goede prestaties. Mooie en natuurlijke gebruikersinterfaces dragen zeker bij aan de ervaring van een gebruiker, maar een slecht presterende toepassing zal een goede ervaring tenietdoen. Als gebruikers uw software niet willen gebruiken, wat heeft het dan voor zin om het te schrijven? Het is dus absoluut noodzakelijk dat JavaScript-ontwikkelaars in de huidige tijd van webcentrische ontwikkeling de best mogelijke code schrijven.

Wat heeft dit allemaal te maken met functies?

Waar u uw functies definieert, is van invloed op de prestaties van uw toepassing.

Er zijn veel JavaScript-anti-patronen, maar een met functies is enigszins populair geworden, vooral in de menigte die ernaar streeft JavaScript te dwingen om functies in andere talen te emuleren (functies zoals privacy). Het is nesten functies in andere functies, en als dit verkeerd wordt gedaan, kan dit een nadelig effect hebben op uw toepassing.

Het is belangrijk op te merken dat dit antipatroon niet van toepassing is op alle instanties van geneste functies, maar het wordt meestal gedefinieerd door twee kenmerken. Ten eerste wordt het maken van de betreffende functie meestal uitgesteld, wat betekent dat de geneste functie tijdens het laden niet door de JavaScript-engine is gemaakt. Dat is op zich geen slechte zaak, maar het is het tweede kenmerk dat de prestaties belemmert: de geneste functie wordt herhaaldelijk gecreëerd als gevolg van herhaalde oproepen naar de buitenfunctie. Dus hoewel het misschien gemakkelijk is om te zeggen dat "alle geneste functies slecht zijn", is dat zeker niet het geval, en kunt u problematische geneste functies identificeren en deze repareren om uw toepassing te versnellen.


Nesten van functies in normale functies

Het eerste voorbeeld van dit anti-patroon is het nesten van een functie binnen een normale functie. Hier is een versimpeld voorbeeld:

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

Je mag deze exacte code niet schrijven, maar het is belangrijk om het patroon te herkennen. Een uiterlijke functie, foo (), bevat een innerlijke functie, bar(), en noemt die innerlijke functie om werk te doen. Veel ontwikkelaars vergeten dat functies waarden zijn in JavaScript. Wanneer u een functie in uw code declareert, maakt de JavaScript-engine een overeenkomstig functieobject: een waarde die aan een variabele kan worden toegewezen of aan een andere functie kan worden doorgegeven. De handeling van het maken van een functieobject lijkt op die van een ander type waarde; de JavaScript-engine maakt het niet totdat het nodig is. Dus in het geval van de bovenstaande code maakt de JavaScript-engine niet de innerlijke bar() functie tot foo () uitvoert. Wanneer foo () uitgangen, de bar() functieobject is vernietigd.

Het feit dat foo () heeft een naam betekent dat het meerdere keren zal worden aangeroepen gedurende de hele applicatie. Terwijl een uitvoering van foo () wordt als OK beschouwd, volgende aanroepen veroorzaken onnodig werk voor de JavaScript-engine omdat deze a moet recreëren bar() functieobject voor elke foo () uitvoering. Dus, als je belt foo () 100 keer in een toepassing moet de JavaScript-engine 100 maken en vernietigen bar() functie objecten. Grote deal, toch? De engine moet elke keer dat deze wordt aangeroepen andere lokale variabelen binnen een functie creëren, dus waarom zou je om functies geven?

In tegenstelling tot andere typen waarden, veranderen functies meestal niet; er wordt een functie gemaakt om een ​​specifieke taak uit te voeren. Het heeft dus weinig zin om CPU-cycli te verspillen door opnieuw een enigszins statische waarde te creëren.

Idealiter is de bar() functieobject in dit voorbeeld mag maar één keer worden gemaakt, en dat is eenvoudig te bereiken, hoewel natuurlijk complexere functies mogelijk uitgebreide refactoring vereisen. Het idee is om de bar() verklaring buiten foo () zodat het functieobject slechts één keer wordt gemaakt, zoals dit:

functie foo (a, b) retourbalk (a, b);  functiebalk (a, b) return a + b;  foo (1, 2);

Merk op dat het nieuwe bar() functie is niet precies zoals het was binnenin foo (). Omdat het oud is bar() functie gebruikte de een en b parameters in foo (), de nieuwe versie moest worden aangepast om die argumenten te accepteren om zijn werk te doen.

Afhankelijk van de browser is deze geoptimaliseerde code overal 10% tot 99% sneller dan de geneste versie. U kunt de test zelf bekijken en uitvoeren op jsperf.com/nested-named-functionctions. Houd rekening met de eenvoud van dit voorbeeld. Een 10% (aan het laagste uiteinde van het prestatiespectrum) prestatiewinst lijkt niet veel, maar zou hoger zijn naarmate meer geneste en complexe functies betrokken zijn.

Om het probleem misschien te verwarren, wikkel deze code in een anonieme, zichzelf uitvoerende functie, zoals deze:

(functie () functie foo (a, b) retourbalk (a, b); functiebalk (a, b) return a + b; foo (1, 2); ());

Inpakcode in een anonieme functie is een gebruikelijk patroon en op het eerste gezicht lijkt het erop dat deze code het bovengenoemde prestatieprobleem repliceert door de geoptimaliseerde code in een anonieme functie te verpakken. Hoewel er een lichte performance hit is bij het uitvoeren van de anonieme functie, is deze code perfect acceptabel. De zelf-uitvoerende functie dient alleen om de te bevatten en te beschermen foo () en bar() functies, maar wat nog belangrijker is, de anonieme functie wordt slechts eenmaal uitgevoerd, dus de innerlijke foo () en bar() functies worden slechts één keer gemaakt. Er zijn echter enkele gevallen waarin anonieme functies net zo (of meer) problematisch zijn als benoemde functies.


Anonieme functies

Wat dit onderwerp betreft, hebben anonieme functies het potentieel om gevaarlijker te zijn dan benoemde functies.

Het is niet de anonimiteit van de functie die gevaarlijk is, maar hoe ontwikkelaars ze gebruiken. Het is vrij gebruikelijk om anonieme functies te gebruiken bij het instellen van gebeurtenishandlers, callback-functies of iteratorfuncties. De volgende code kent bijvoorbeeld een toe Klik gebeurtenis luisteraar op het document:

document.addEventListener ("klik", functie (evt) alert ("U hebt op de pagina geklikt."););

Hier wordt een anonieme functie doorgegeven aan de addEventListener () methode om de Klik gebeurtenis op het document; dus, de functie wordt uitgevoerd elke keer dat de gebruiker ergens op de pagina klikt. Om een ​​ander algemeen gebruik van anonieme functies te demonstreren, overweeg dan dit voorbeeld dat de jQuery-bibliotheek gebruikt om alles te selecteren elementen in het document en itereer ze met de elk() methode:

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

In deze code wordt de anonieme functie doorgegeven aan de jQuery-objecten elk() methode wordt voor elk uitgevoerd element gevonden in het document. In tegenstelling tot benoemde functies, waar deze impliciet herhaaldelijk worden aangeroepen, is de herhaalde uitvoering van een groot aantal anonieme functies nogal expliciet. Het is voor de prestaties absoluut noodzakelijk dat ze efficiënt en geoptimaliseerd zijn. Bekijk de volgende (maar alweer versimpelde) jQuery-plug-in:

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

Deze code definieert een extreem eenvoudige plugin genaamd myPlugin; het is zo eenvoudig dat veel veelgebruikte plug-ins niet aanwezig zijn. Normaal gesproken worden definities van plug-ins ingepakt in zelfuitvoerende anonieme functies en meestal worden standaardwaarden geleverd voor opties om ervoor te zorgen dat geldige gegevens beschikbaar zijn om te gebruiken. Deze dingen zijn voor de duidelijkheid verwijderd.

Het doel van deze plug-in is om de kleur van de geselecteerde elementen te wijzigen in de kleur die is opgegeven in de opties object doorgegeven aan de myPlugin () methode. Het doet dit door een anonieme functie door te geven aan de elk() iterator, waardoor deze functie wordt uitgevoerd voor elk element in het jQuery-object. In de anonieme functie wordt een innerlijke functie genoemd verander kleur() doet het eigenlijke werk van het veranderen van de kleur van het element. Zoals geschreven, is deze code inefficiënt omdat, u raadt het al, de verander kleur() functie is gedefinieerd in de iteratiefunctie? het maken van de JavaScript-engine opnieuw maken verander kleur() bij elke iteratie.

Het efficiënter maken van deze code is vrij eenvoudig en volgt hetzelfde patroon als eerder: refactor de verander kleur() functie die moet worden gedefinieerd buiten alle functies die deze bevatten, en laat deze de informatie ontvangen die hij nodig heeft om zijn werk te doen. In dit geval, verander kleur() heeft het jQuery-object en de nieuwe kleurwaarde nodig. De verbeterde code ziet er zo uit:

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

Interessant is dat deze geoptimaliseerde code de prestaties verbetert met een veel kleinere marge dan de foo () en bar() bijvoorbeeld, waarbij Chrome het pakket leidt met een prestatiewinst van 15% (jsperf.com/function-nesting-with-jquery-plugin). De waarheid is dat toegang tot de DOM en het gebruik van de API van jQuery hun eigen hit toevoegen aan de prestaties, vooral jQuery's elk(), dat is notoir traag in vergelijking met de native loops van JavaScript. Maar houd zoals eerder de eenvoud van dit voorbeeld in het oog. Hoe meer geneste functies, hoe groter de prestatiewinst bij optimalisatie.

Nesten van functies in constructorfuncties

Een andere variant van dit anti-patroon is nesting-functies binnen constructeurs, zoals hieronder getoond:

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");

Deze code definieert een constructorfunctie genaamd Persoon(), en het vertegenwoordigt (als het niet duidelijk was) een persoon. Het accepteert argumenten die de voor- en achternaam van een persoon bevatten en slaat deze waarden op Voornaam en achternaam eigenschappen, respectievelijk. De constructor maakt ook een methode genaamd getFullName (); het verbindt het Voornaam en achternaam eigenschappen en retourneert de resulterende tekenreekswaarde.

Wanneer u een object in JavaScript maakt, wordt het object in het geheugen opgeslagen

Dit patroon is vrij gebruikelijk geworden in de JavaScript-community van vandaag omdat het privacy kan nabootsen, een functie waarvoor JavaScript momenteel niet is ontworpen (merk op dat privacy niet in het bovenstaande voorbeeld staat, daar zult u later naar kijken). Maar door dit patroon te gebruiken, creëren ontwikkelaars inefficiëntie, niet alleen in uitvoeringstijd, maar ook in geheugengebruik. Wanneer u een object in JavaScript maakt, wordt het object in het geheugen opgeslagen. Het blijft in het geheugen totdat alle verwijzingen ernaar zijn ingesteld nul of zijn buiten bereik. In het geval van de jeremy object in de bovenstaande code, de functie die is toegewezen aan getFullName wordt meestal zo lang in het geheugen opgeslagen als de jeremy object bevindt zich in het geheugen. Wanneer de jeffrey object is gemaakt, een nieuw functieobject is gemaakt en toegewezen jeffrey's getFullName lid, en het verbruikt ook geheugen voor zo lang als jeffrey is in het geheugen. Het probleem hier is dat jeremy.getFullName is een ander functieobject dan jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName resulteert in vals; voer deze code uit op http://jsfiddle.net/k9uRN/). Ze hebben allebei hetzelfde gedrag, maar het zijn twee compleet verschillende functieobjecten (en dus verbruiken ze elk geheugen). Voor de duidelijkheid, bekijk figuur 1:

Figuur 1

Hier zie je de jeremy en jeffrey objecten, elk met hun eigen objecten getFullName () methode. Dus, elk Persoon gemaakt object heeft zijn eigen unieke getFullName () methode-elk waarvan zijn eigen stuk geheugen verbruikt. Stel je voor dat je er 100 creëert Persoon objecten: als elk getFullName () methode verbruikt 4 KB geheugen en vervolgens 100 Persoon objecten zouden ten minste 400 KB geheugen verbruiken. Dat kan kloppen, maar het kan drastisch worden verminderd door de prototype voorwerp.

Gebruik het prototype

Zoals eerder vermeld, zijn functies objecten in JavaScript. Alle functie-objecten hebben een prototype eigenschap, maar het is alleen nuttig voor constructorfuncties. Kortom, de prototype eigendom is vrij letterlijk een prototype voor het maken van objecten; alles wat is gedefinieerd in het prototype van een constructorfunctie wordt gedeeld door alle objecten die door die constructorfunctie zijn gemaakt.

Helaas zijn prototypen niet genoeg gestrest in het JavaScript-onderwijs.

Helaas zijn prototypen niet genoeg gestrest in JavaScript-onderwijs, maar ze zijn absoluut essentieel voor JavaScript, omdat het is gebaseerd op en gebouwd met prototypen - het is een prototypische taal. Zelfs als je het woord nooit hebt getypt prototype in uw code worden ze achter de schermen gebruikt. Bijvoorbeeld elke native string-gebaseerde methode, zoals split (), substr (), of vervangen(), zijn gedefinieerd op Draad()het prototype. Prototypes zijn zo belangrijk voor de JavaScript-taal dat als je de prototypische aard van JavaScript niet omarmt, je inefficiënte code schrijft. Overweeg de bovenstaande implementatie van de Persoon data type: create a Persoon object vereist dat de JavaScript-engine meer werk doet en meer geheugen toewijst.

Dus, hoe kan het gebruiken van de prototype eigenschap maakt deze code efficiënter? Welnu, kijk eerst eens naar de refactored code:

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");

Hier de getFullName () methode definitie wordt verplaatst uit de constructor en op het prototype. Deze eenvoudige wijziging heeft de volgende effecten:

  • De constructor voert minder werk uit en voert dus sneller uit (18% -96% sneller). Voer de test uit in uw browser als u dat wilt.
  • De getFullName () methode wordt slechts eenmaal gemaakt en gedeeld tussen alle Persoon voorwerpen (jeremy.getFullName === jeffrey.getFullName resulteert in waar; voer deze code uit op http://jsfiddle.net/Pfkua/). Vanwege dit, elk Persoon object gebruikt minder geheugen.

Raadpleeg figuur 1 en let erop hoe elk object zijn eigen object heeft getFullName () methode. Dat getFullName () is gedefinieerd op het prototype, het objectdiagram verandert en wordt getoond in Figuur 2:

Figuur 2

De jeremy en jeffrey objecten hebben er geen eigen meer getFullName () methode, maar de JavaScript-engine zal deze vinden Persoon()het prototype. In oudere JavaScript-engines kan het proces van het vinden van een methode voor het prototype een prestatieklimaat opleveren, maar niet zo in de JavaScript-engines van vandaag. De snelheid waarmee moderne motoren prototypen vinden, is extreem snel.

Privacy

Maar hoe zit het met privacy? Immers, dit antipatroon is geboren uit een waargenomen behoefte aan private objectleden. Als je niet bekend bent met het patroon, bekijk dan de volgende code:

function Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = new Foo ("Hallo, Privacy!"); alert (foo.bar ()); // waarschuwingen "Hallo, privacy!"

Deze code definieert een constructorfunctie genaamd Foo (), en het heeft één parameter genaamd paramOne. De waarde die is doorgegeven aan Foo () wordt opgeslagen in een lokale variabele genaamd thisIsPrivate. Let daar op thisIsPrivate is een variabele, geen eigenschap; dus is het buiten de deur ontoegankelijk Foo (). Er is ook een methode gedefinieerd in de constructor en deze wordt genoemd bar(). Omdat bar() is gedefinieerd in Foo (), het heeft toegang tot de thisIsPrivate variabel. Dus als je een maakt Foo object en oproep bar(), de waarde die is toegewezen aan thisIsPrivate wordt teruggestuurd.

De waarde die is toegewezen aan thisIsPrivate is bewaard. Het is niet toegankelijk buiten Foo (), en dus wordt het beschermd tegen wijziging van buitenaf. Dat is geweldig, toch? Wel, ja en nee. Het is begrijpelijk waarom sommige ontwikkelaars privacy willen emuleren in JavaScript: u kunt ervoor zorgen dat de gegevens van een object worden beveiligd tegen manipulatie van buitenaf. Maar tegelijkertijd introduceert u inefficiëntie in uw code door het prototype niet te gebruiken.

Dus nogmaals, hoe zit het met privacy? Nou dat is simpel: doe het niet. De taal ondersteunt momenteel niet officieel privé-objectleden, hoewel dat in een toekomstige revisie van de taal kan veranderen. In plaats van afsluitingen te gebruiken om privé-leden te maken, is de conventie om "privé-leden" aan te duiden de identifier aan te brengen met een onderstrepingsteken (dat wil zeggen: _thisIsPrivate). De volgende code herschrijft het vorige voorbeeld met behulp van de conventie:

function Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = function () return this._thisIsPrivate; ; var foo = new Foo ("Hello, Convention to Denote Privacy!"); alert (foo.bar ()); // waarschuwingen "Hallo, conventie om privacy aan te geven!"

Nee, het is niet privé, maar het onderstreepingscongres zegt in feite "raak me niet aan". Totdat JavaScript volledige privéeigenschappen en -methoden volledig ondersteunt, zou u dan niet liever een efficiëntere en performante code hebben dan privacy? Het juiste antwoord is: ja!


Samenvatting

Waar u functies in uw code definieert, heeft invloed op de prestaties van uw toepassing; houd daar rekening mee tijdens het schrijven van je code. Nest geen functies in een functie die vaak wordt genoemd. Als u dit doet, verspilt u CPU-cycli. Wat constructorfuncties betreft, omarm het prototype; Als u dit niet doet, resulteert dit in inefficiënte code. Ontwikkelaars schrijven immers software die gebruikers kunnen gebruiken en de prestaties van een toepassing zijn net zo belangrijk voor de gebruikerservaring als de gebruikersinterface.