Prototyper i JavaScript

När du definierar en funktion inom JavaScript, kommer den med några fördefinierade egenskaper; en av dessa är den illusiva prototypen. I den här artikeln ska jag beskriva vad det är och varför ska du använda det i dina projekt.


Vad är prototyp?

Prototypegenskapen är ursprungligen ett tomt objekt och kan få medlemmar tillagt det - som du skulle något annat objekt.

var myObject = funktion (namn) this.name = name; returnera detta; ; console.log (typ av myObject.prototype); // objekt myObject.prototype.getName = function () returnera detta.namn; ;

I utgåvan ovan har vi skapat en funktion, men om vi ringer myObject (), det kommer helt enkelt att returnera fönster objekt, eftersom det definierades inom det globala området. detta kommer därför att returnera det globala objektet, eftersom det ännu inte har blivit instantiated (mer om detta senare).

console.log (myObject () === fönster); // Sann

Den hemliga länken

Varje objekt inom JavaScript har en "hemlig" egenskap.

Innan vi fortsätter vill jag diskutera den "hemliga" länken som gör prototyparbetet så som det gör.

Varje objekt inom JavaScript har en "hemlig" egenskap som läggs till den när den är definierad eller instantierad, namngiven __proto__; Så här får man tillgång till prototypkedjan. Det är dock inte en bra idé att komma åt __proto__ inom din ansökan, eftersom den inte är tillgänglig i alla webbläsare.

De __proto__ egendom bör inte förväxlas med ett objekts prototyp, eftersom de är två separata egenskaper; som sagt, de går hand i hand. Det är viktigt att göra denna skillnad, eftersom det kan vara ganska förvirrande först! Vad betyder detta exakt? Låt mig förklara. När vi skapade myObject funktion definierade vi ett objekt av typen Fungera.

console.log (typ av myObject); // funktion

För de omedvetna, Fungera är ett fördefinierat objekt i JavaScript, och som ett resultat har dess egna egenskaper (t.ex.. längd och argument) och metoder (t.ex.. ring upp och tillämpa). Och ja, det har också sitt eget prototypobjekt, liksom hemligheten __proto__ länk. Det betyder att någonstans i JavaScript-motorn finns en del kod som kan likna följande:

 Function.prototype = arguments: null, längd: 0, call: function () // secret code, tillämpa: funktion () // hemlig kod ...

I sannhet skulle det förmodligen inte vara så så förenklat; Detta är bara för att illustrera hur prototypkedjan fungerar.

Så vi har definierat myObject som en funktion och gav den ett argument, namn; men vi ställer aldrig några egenskaper, till exempel längd eller metoder, såsom ring upp. Så varför fungerar följande?

console.log (myObject.length); // 1 (det är antalet tillgängliga argument)

Detta beror på när vi definierade myObject, det skapade en __proto__ egendom och sätta sitt värde till Function.prototype (illustrerad i koden ovan). Så när vi kommer åt myObject.length, det letar efter en egendom av myObject kallad längd och finner inte en; Det reser sig sedan upp i kedjan, via __proto__ länk, hittar fastigheten och returnerar den.

Du kanske undrar varför längd är satt till 1 och inte 0 - eller något annat nummer för det faktumet. Det här är för att myObject är faktiskt en förekomst av Fungera.

console.log (myObject instanceof Function); // true console.log (myObject === Funktion); // false

När en förekomst av ett objekt skapas, __proto__ egenskapen är uppdaterad för att peka på konstruktörens prototyp, vilket i detta fall är Fungera.

console.log (myObject .__ proto__ === Function.prototype) // true

Dessutom, när du skapar en ny Fungera objekt, den inbyggda koden inuti Fungera konstruktören räknar antalet argumenter och uppdateringar this.length följaktligen, vilket i detta fall är 1.

Om vi ​​emellertid skapar en ny instans av myObject använda ny nyckelord, __proto__ kommer att peka på myObject.prototype som myObject är konstruktören i vår nya instans.

 var myInstance = ny myObject ("foo"); console.log (myInstance .__ proto__ === myObject.prototype); // Sann

Förutom att ha tillgång till de inhemska metoderna inom Fungera.prototyp, t.ex. ring upp och tillämpa, vi har nu tillgång till myObjects metod, hämta namn.

 console.log (myInstance.getName ()); // foo var mySecondInstance = ny myObject ("bar"); console.log (mySecondInstance.getName ()); // bar console.log (myInstance.getName ()); // foo

Som du kan tänka dig är det här ganska praktiskt, eftersom det kan användas för att ritning ett objekt och skapa så många instanser som behövs - vilket leder mig till nästa ämne!


Varför använder Prototype bättre?

Säg till exempel att vi utvecklar ett kanvasspel och behöver flera (möjligen hundratals) objekt på skärmen samtidigt. Varje objekt kräver egna egenskaper, till exempel x och y koordinater, bredd,höjd, och många andra.

Vi kan göra det enligt följande:

 var GameObject1 = x: Math.floor ((Math.random () * myCanvasWidth) + 1), y: Math.floor ((Math.random () * myCanvasHeight) + 1), bredd: 10, höjd: 10, rita: funktion () myCanvasContext.fillRect (this.x, this.y, this.width, this.height);  ...; var GameObject2 = x: Math.floor ((Math.random () * myCanvasWidth) + 1), y: Math.floor ((Math.random () * myCanvasHeight) + 1), bredd: 10, höjd: 10, rita: funktion () myCanvasContext.fillRect (this.x, this.y, this.width, this.height);  ...;

... gör detta 98 ​​fler gånger ...

Vad detta kommer att göra är att skapa alla dessa objekt i minnet - allt med separata definitioner för metoder, till exempel dra och vad andra metoder kan behövas. Detta är verkligen inte idealiskt, eftersom spelet kommer att uppblåsa webbläsarna tilldelade JavaScript-minne och få det att springa väldigt långsamt ... eller till och med sluta svara.

Även om detta förmodligen inte skulle hända med endast 100 objekt, kan det ändå fungera som en ganska bra prestation, eftersom det kommer att behöva leta upp ett hundra olika objekt snarare än bara singeln prototyp objekt.


Hur man använder prototyp

För att programmet ska köras snabbare (och följa bästa praxis) kan vi (åter) definiera egenskapen för prototypen för GameObject; varje förekomst av GameObject kommer då att referera till metoderna inom GameObject.prototype som om de var deras egna metoder.

 // definiera GameObject-konstruktörsfunktionen var GameObject = funktion (bredd, höjd) this.x = Math.floor ((Math.random () * myCanvasWidth) + 1); this.y = Math.floor ((Math.random () * myCanvasHeight) + 1); this.width = width; this.height = height; returnera detta; ; // (re) definiera GameObject prototypobjektet GameObject.prototype = x: 0, y: 0, bredd: 5, bredd: 5, rita: funktion () myCanvasContext.fillRect (this.x, this.y, detta . bredd, this.height); ;

Vi kan sedan inställa GameObject 100 gånger.

 var x = 100, arrayOfGameObjects = []; gör arrayOfGameObjects.push (nytt GameObject (10, 10));  medan (x--);

Nu har vi en uppsättning av 100 GameObjects, som alla delar samma prototyp och definition av dra metod som drastiskt sparar minne inom applikationen.

När vi kallar dra metod, kommer det att referera till exakt samma funktion.

 var GameLoop = funktion () för (gameObject in arrayOfGameObjects) gameObject.draw (); ;

Prototyp är ett Live Object

Ett objekts prototyp är ett levande objekt, så att säga. Detta innebär helt enkelt att om vi, efter att vi skapat alla våra GameObject-instanser, bestämmer oss för att vi istället för att rita en rektangel vill rita en cirkel, kan vi uppdatera vår GameObject.prototype.draw metod i enlighet med detta.

 GameObject.prototype.draw = function () myCanvasContext.arc (this.x, this.y, this.width, 0, Math.PI * 2, true); 

Och nu, alla tidigare instanser av GameObject och eventuella framtida fall kommer att rita en cirkel.


Uppdatera prototyper för infödda objekt

Ja, det här är möjligt. Du kanske är bekant med JavaScript-bibliotek, till exempel Prototype, som utnyttjar denna metod.

Låt oss använda ett enkelt exempel:

 String.prototype.trim = function () returnera this.replace (/ ^ \ s + | \ s + $ / g, ");;

Vi kan nu komma åt detta som en metod för vilken sträng som helst:

"Foo bar" .trim (); // "Foo bar"

Det finns dock en mindre nackdel med detta. Du kan till exempel använda detta i din ansökan; men ett år eller två på vägen kan en webbläsare genomföra en uppdaterad version av JavaScript som innehåller en infödd trimma metod inom Strängs prototyp. Det betyder att din definition av trimma kommer att åsidosätta den ursprungliga versionen! Usch! För att övervinna detta kan vi lägga till en enkel kontroll innan du definierar funktionen.

 om (! String.prototype.trim) String.prototype.trim = function () returnera this.replace (/ ^ \ s + | \ s + $ / g, ");;

Nu, om den existerar, kommer den att använda den inbyggda versionen av trimma metod.

Som en tumregel är det allmänt sett en bra metod för att undvika att utöka föremål. Men som vad som helst kan regler brytas om det behövs.


Slutsats

Förhoppningsvis har den här artikeln skingrat lite på ryggraden för JavaScript som är prototyp. Du borde nu vara på väg att skapa mer effektiva applikationer.

Om du har några frågor angående prototyp, låt mig veta i kommentarerna, och jag gör mitt bästa för att svara på dem.