JavaScript kan virke som et veldig enkelt språk å lære først. Kanskje det er på grunn av den fleksible syntaksen. Eller kanskje det er på grunn av likheten med andre kjente språk som Java. Eller kanskje det er fordi den har så få datatyper i forhold til språk som Java, Ruby eller .NET.
Men i sannhet er JavaScript mye mindre forenklet og mer nyansert enn de fleste utviklere innledningsvis innse. Selv for utviklere med mer erfaring , noen av JavaScript's fremtredende funksjoner blir fortsatt misforstått og fører til forvirring. En slik funksjon er måten data (eiendom og variabel) oppslag utføres, og JavaScript-ytelsesforgreninger å være klar over.
I JavaScript styres dataoppslag av to ting: prototypisk arv og omfangskjede . Som utvikler er det helt klart å forstå disse to mekanismene, siden du kan forbedre strukturen og ofte ytelsen til koden din.
Når du får tilgang til en eiendom på et prototypebasert språk som JavaScript, tar en dynamisk oppslag steder som involverer forskjellige lag i objektets prototypetre.
I JavaScript er hver funksjon et objekt. Når en funksjon påkalles med new
operatør, opprettes et nytt objekt. For eksempel:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var p1 = new Person('John', 'Doe'); var p2 = new Person('Robert', 'Doe');
I eksemplet ovenfor, p1
og p2
er to forskjellige objekter, hver laget med Person
fungere som en konstruktør. De er uavhengige forekomster av Person
, som demonstrert av denne kodebiten:
console.log(p1 instanceof Person); // prints 'true' console.log(p2 instanceof Person); // prints 'true' console.log(p1 === p2); // prints 'false'
Siden JavaScript-funksjoner er objekter, kan de ha egenskaper. En spesielt viktig egenskap som hver funksjon har kalles prototype
.
prototype
, som i seg selv er et objekt, arver fra foreldrenes prototype, som arver fra foreldrenes prototype, og så videre. Dette blir ofte referert til som prototype kjede . Object.prototype
, som alltid er på slutten av prototypekjeden (dvs. øverst på det prototypiske arvetreet), inneholder metoder som toString()
, hasProperty()
, isPrototypeOf()
, og så videre.
Hver funksjons prototype kan utvides for å definere sine egne tilpassede metoder og egenskaper.
Når du instantierer et objekt (ved å påkalle funksjonen ved hjelp av new
operatoren), arver den alle egenskapene i prototypen til den funksjonen. Husk imidlertid at disse tilfellene ikke vil ha direkte tilgang til prototype
objekt, men bare mot dets egenskaper. For eksempel:
// Extending the Person prototype from our earlier example to // also include a 'getFullName' method: Person.prototype.getFullName = function() { return this.firstName + ' ' + this.lastName; } // Referencing the p1 object from our earlier example console.log(p1.getFullName()); // prints 'John Doe' // but p1 can’t directly access the 'prototype' object... console.log(p1.prototype); // prints 'undefined' console.log(p1.prototype.getFullName()); // generates an error
Det er et viktig og litt subtilt punkt her: Selv om p1
ble opprettet før getFullName
metoden ble definert, vil den fremdeles ha tilgang til den fordi prototypen er Person
prototype.
(Det er verdt å merke seg at nettlesere også lagrer en referanse til prototypen til ethvert objekt i en __proto__
-egenskap, men det er virkelig dårlig praksis for å få direkte tilgang til prototypen via __proto__
eiendom, siden det ikke er en del av standarden ECMAScript Language Specification , så ikke gjør det! )
Siden p1
forekomst av Person
objektet har ikke i seg selv direkte tilgang til prototype
objekt, hvis vi vil overskrive getFullName
i p1
, vil vi gjøre det som følger:
// We reference p1.getFullName, *NOT* p1.prototype.getFullName, // since p1.prototype does not exist: p1.getFullName = function(){ return 'I am anonymous'; }
Nå p1
har sitt eget getFullName
eiendom. Men p2
forekomst (opprettet i vårt tidligere eksempel) gjør ikke har noen slik egen eiendom. Derfor påkaller p1.getFullName()
får tilgang til getFullName
metoden for p1
selve forekomsten mens den påkaller p2.getFullName()
går opp prototypekjeden til Person
prototype objekt å løse getFullName
:
console.log(p1.getFullName()); // prints 'I am anonymous' console.log(p2.getFullName()); // prints 'Robert Doe'
En annen viktig ting å være klar over er at det også er mulig dynamisk endre prototypen til et objekt. For eksempel:
function Parent() { this.someVar = 'someValue'; }; // extend Parent’s prototype to define a 'sayHello' method Parent.prototype.sayHello = function(){ console.log('Hello'); }; function Child(){ // this makes sure that the parent's constructor is called and that // any state is initialized correctly. Parent.call(this); }; // extend Child's prototype to define an 'otherVar' property... Child.prototype.otherVar = 'otherValue'; // ... but then set the Child's prototype to the Parent prototype // (whose prototype doesn’t have any 'otherVar' property defined, // so the Child prototype no longer has ‘otherVar’ defined!) Child.prototype = Object.create(Parent.prototype); var child = new Child(); child.sayHello(); // prints 'Hello' console.log(child.someVar); // prints 'someValue' console.log(child.otherVar); // prints 'undefined'
Når du bruker prototypisk arv, husk å definere egenskaper i prototypen etter enten å ha arvet fra foreldreklassen eller spesifisert en alternativ prototype.
For å oppsummere fungerer eiendomsoppslag gjennom JavaScript-prototypkjeden som følger:
hasOwnProperty
metoden kan brukes til å sjekke om et objekt har en bestemt navngitt eiendom.)Object.prototype
er nådd, og den har heller ikke eiendommen, eiendommen regnes som undefined
.Å forstå hvordan prototypisk arv og eiendomsoppslag fungerer, er generelt viktig for utviklere, men er også viktig på grunn av dens (noen ganger betydelige) JavaScript-ytelsesforgreninger. Som nevnt i dokumentasjonen for V8 (Googles open source, JavaScript-motor med høy ytelse), bruker de fleste JavaScript-motorer en ordlignende datastruktur for å lagre objektegenskaper. Hver eiendomstilgang krever derfor en dynamisk oppslag i datastrukturen for å løse eiendommen. Denne tilnærmingen gjør at tilgang til egenskaper i JavaScript vanligvis er mye langsommere enn tilgang til forekomstvariabler i programmeringsspråk som Java og Smalltalk.
En annen oppslagsmekanisme i JavaScript er basert på omfang.
For å forstå hvordan dette fungerer, er det nødvendig å introdusere begrepet utførelseskontekst .
I JavaScript er det to typer kjøringskontekster:
Gjennomføringskontekster er organisert i en bunke. Nederst i bunken er det alltid den globale konteksten, som er unik for hvert JavaScript-program. Hver gang en funksjon påtreffes, opprettes en ny utførelseskontekst og skyves på toppen av bunken. Når funksjonen er ferdig utført, poppes konteksten av bunken.
Vurder følgende kode:
// global context var message = 'Hello World'; var sayHello = function(n){ // local context 1 created and pushed onto context stack var i = 0; var innerSayHello = function() { // local context 2 created and pushed onto context stack console.log((i + 1) + ': ' + message); // local context 2 popped off of context stack } for (i = 0; i Innenfor hver utførelsessammenheng er det et spesielt objekt som kalles a omfangskjede som brukes til å løse variabler. En omfangskjede er egentlig en stabel med nåværende tilgjengelige omfang, fra den mest umiddelbare konteksten til den globale konteksten. (For å være litt mer presis kalles objektet øverst i bunken en Aktiveringsobjekt som inneholder referanser til de lokale variablene for funksjonen som utføres, de nevnte funksjonsargumentene og to 'spesielle' objekter: this
og arguments
.) For eksempel:

Legg merke til i diagrammet ovenfor hvordan this
peker på window
objekt som standard og også hvordan den globale konteksten inneholder eksempler på andre objekter som console
og location
.
Når du prøver å løse variabler via omfangskjeden, blir den umiddelbare konteksten først sjekket for en samsvarende variabel. Hvis ingen samsvar blir funnet, blir neste kontekstobjekt i omfangskjeden sjekket, og så videre, til en samsvar er funnet. Hvis ingen treff blir funnet, vil en ReferenceError
blir kastet.
Det er viktig å også merke seg at et nytt omfang legges til omfangskjeden når et try-catch
blokk eller en with
blokkering oppstår. I begge disse tilfellene opprettes et nytt objekt og plasseres øverst i omfangskjeden:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }; function persist(person) { with (person) { // The 'person' object was pushed onto the scope chain when we // entered this 'with' block, so we can simply reference // 'firstName' and 'lastName', rather than person.firstName and // person.lastName if (!firstName) { throw new Error('FirstName is mandatory'); } if (!lastName) { throw new Error('LastName is mandatory'); } } try { person.save(); } catch(error) { // A new scope containing the 'error' object is accessible here console.log('Impossible to store ' + person + ', Reason: ' + error); } } var p1 = new Person('John', 'Doe'); persist(p1);
For å forstå hvordan omfangsbaserte variable oppslag forekommer, er det viktig å huske at det for øyeblikket ikke er noen blokkeringsnivåer i JavaScript. For eksempel:
for (var i = 0; i <10; i++) { /* ... */ } // 'i' is still in scope! console.log(i); // prints '10'
På de fleste andre språk vil koden ovenfor føre til en feil fordi 'livet' (dvs. omfanget) til variabelen i
ville være begrenset til for blokkering. I JavaScript er dette imidlertid ikke tilfelle. Snarere i
blir lagt til aktiveringsobjektet øverst i omfangskjeden, og det vil forbli der til objektet er fjernet fra omfanget, noe som skjer når den tilsvarende utførelseskonteksten fjernes fra bunken. Denne oppførselen er kjent som variabel heising.
Det er imidlertid verdt å merke seg at støtte for omfang på blokknivå er på vei inn i JavaScript gjennom det nye let
nøkkelord. let
nøkkelord er allerede tilgjengelig i JavaScript 1.7 og er planlagt å bli et offisielt støttet JavaScript-nøkkelord fra og med ECMAScript 6.
JavaScript-ytelsesutvidelser
Måten egenskap og variabel oppslag bruker henholdsvis prototypekjede og omfangskjede, fungerer i JavaScript, er en av språkets viktigste funksjoner, men det er en av de vanskeligste og mest subtile å forstå.
Oppslagsoperasjonene vi har beskrevet i dette eksemplet, enten basert på prototypekjeden eller omfangskjeden, gjentas hver gang en eiendom eller variabel er tilgjengelig. Når dette oppslaget skjer i sløyfer eller andre intensive operasjoner, kan det ha betydelige forgreninger av JavaScript-ytelsen, spesielt i lys av språket med en tråd, som forhindrer at flere operasjoner skjer samtidig.
Tenk på følgende eksempel:
var start = new Date().getTime(); function Parent() { this.delta = 10; }; function ChildA(){}; ChildA.prototype = new Parent(); function ChildB(){} ChildB.prototype = new ChildA(); function ChildC(){} ChildC.prototype = new ChildB(); function ChildD(){}; ChildD.prototype = new ChildC(); function ChildE(){}; ChildE.prototype = new ChildD(); function nestedFn() { var child = new ChildE(); var counter = 0; for(var i = 0; i <1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += child.delta; } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
I dette eksemplet har vi et langt arvetre og tre nestede løkker. Inne i den dypeste sløyfen økes tellervariabelen med verdien av delta
. Men delta
ligger nesten øverst på arvetreet! Dette betyr at hver gang child.delta
er tilgjengelig, må hele treet navigeres fra bunn til topp. Dette kan ha en virkelig negativ innvirkning på ytelsen.
Når vi forstår dette, kan vi enkelt forbedre ytelsen til ovenstående nestedFn
funksjon ved å bruke en lokal delta
variabel for å cache verdien i child.delta
(og dermed unngå behovet for repeterende gjennomkjøring av hele arvetreet) som følger:
function nestedFn() { var child = new ChildE(); var counter = 0; var delta = child.delta; // cache child.delta value in current scope for(var i = 0; i <1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += delta; // no inheritance tree traversal needed! } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
Selvfølgelig er denne spesielle teknikken bare levedyktig i et scenario der det er kjent at verdien av child.delta
endres ikke mens for løkkene kjøres; Ellers må den lokale kopien oppdateres med gjeldende verdi.
OK, la oss kjøre begge versjonene av nestedFn
metode og se om det er noen merkbar ytelsesforskjell mellom de to.
Vi starter med å kjøre det første eksemplet i a node.js REPL :
[email protected] :~$ node test.js Final result: 10000000000 Total time: 8270 milliseconds
Så det tar omtrent 8 sekunder å kjøre. Det er lenge.
La oss nå se hva som skjer når vi kjører den optimaliserte versjonen:
[email protected] :~$ node test2.js Final result: 10000000000 Total time: 1143 milliseconds
Denne gangen tok det bare ett sekund. Mye raskere!
Merk at bruk av lokale variabler for å unngå dyre oppslag er en teknikk som kan brukes både for oppslag av eiendom (via prototypekjeden) og for variabel oppslag (via omfangskjeden).
Dessuten kan denne typen 'caching' av verdier (dvs. i variabler i lokalt omfang) også være gunstig når du bruker noen av de vanligste JavaScript-bibliotekene. Ta jQuery , for eksempel. jQuery støtter begrepet 'selektorer', som i utgangspunktet er en mekanisme for å hente et eller flere samsvarende elementer i DØMMEKRAFT . Den enkle man kan spesifisere velgere i jQuery kan føre til at man glemmer hvor kostbart (sett fra ytelsesmessig synspunkt) hver velgeroppslag kan være. Følgelig kan lagring av oppslagsresultater i en lokal variabel være ekstremt gunstig for ytelsen. For eksempel:
// this does the DOM search for $('.container') 'n' times for (var i = 0; i ”); } // this accomplishes the same thing... // but only does the DOM search for $('.container') once, // although it does still modify the DOM 'n' times var $container = $('.container'); for (var i = 0; i '); } // or even better yet... // this version only does the DOM search for $('.container') once // AND only modifies the DOM once var $html = ''; for (var i = 0; i '; } $('.container').append($html);
Spesielt på en webside med et stort antall elementer, kan den andre tilnærmingen i kodeeksemplet ovenfor potensielt resultere i betydelig bedre ytelse enn den første.
Innpakning
Dataoppslag i JavaScript er ganske annerledes enn det er på de fleste andre språk, og det er svært nyansert. Det er derfor viktig å forstå og forstå riktig disse konseptene for virkelig å mestre språket. Dataoppslag og andre vanlige JavaScript-feil bør unngås når det er mulig. Denne forståelsen gir sannsynligvis renere, mer robust kode som oppnår forbedret JavaScript-ytelse.
I slekt: Som JS-utvikler er dette det som holder meg oppe om natten / gir mening om ES6 klasse forvirring