JavaScript voi tuntua aluksi erittäin helposti opittavalta kieleltä. Ehkä se johtuu sen joustavasta syntaksista. Tai ehkä se johtuu sen samankaltaisuudesta muihin tunnettuihin kieliin, kuten Java. Tai ehkä siksi, että sillä on niin vähän tietotyyppejä kuin Java, Ruby tai .NET.
Mutta todellisuudessa JavaScript on paljon vähemmän yksinkertainen ja vivahteikkaampi kuin useimmat kehittäjät aluksi ymmärtää. Jopa kehittäjät, joilla on enemmän kokemusta , joitain JavaScriptin keskeisimpiä ominaisuuksia ymmärretään edelleen väärin ja ne aiheuttavat sekaannusta. Yksi tällainen ominaisuus on tapa, jolla data (ominaisuus ja muuttuja) haetaan, ja JavaScriptin suorituskyvyn seuraukset, joista on oltava tietoinen.
JavaScriptissä tietojen hakua hallitaan kahdella tavalla: prototyyppinen perintö ja laajuusketju . Kehittäjänä näiden kahden mekanismin ymmärtäminen on välttämätöntä, koska se voi parantaa koodisi rakennetta ja usein suorituskykyä.
Kun käytät omaisuutta prototyyppipohjaisella kielellä, kuten JavaScript, dynaaminen haku etsii paikkoja, joihin liittyy erilaisia kerroksia objektin prototyyppipuussa.
JavaScriptissä jokainen toiminto on objekti. Kun toiminto käynnistetään new
-näppäimellä operaattori, luodaan uusi objekti. Esimerkiksi:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var p1 = new Person('John', 'Doe'); var p2 = new Person('Robert', 'Doe');
Yllä olevassa esimerkissä p1
ja p2
ovat kaksi erilaista objektia, joista kukin on luotu Person
toimii rakentajana. Ne ovat Person
: n itsenäisiä esiintymiä, kuten tämä koodinpätkä osoittaa:
console.log(p1 instanceof Person); // prints 'true' console.log(p2 instanceof Person); // prints 'true' console.log(p1 === p2); // prints 'false'
Koska JavaScript-toiminnot ovat objekteja, niillä voi olla ominaisuuksia. Jokaisen toiminnon erityisen tärkeää ominaisuutta kutsutaan prototype
.
prototype
, joka on itse objekti, perii vanhempiensa prototyypistä, joka perii vanhempiensa prototyypistä, ja niin edelleen. Tätä kutsutaan usein nimellä prototyyppiketju . Object.prototype
, joka on aina prototyyppiketjun lopussa (ts. prototyyppisen perintöpuun yläosassa), sisältää menetelmiä, kuten toString()
, hasProperty()
, isPrototypeOf()
, ja niin edelleen.
Kunkin toiminnon prototyyppiä voidaan laajentaa määrittelemään omat mukautetut menetelmät ja ominaisuudet.
Kun teet objektin (kutsumalla funktion operaattorilla new
), se perii kaikki toiminnon prototyypin ominaisuudet. Muista kuitenkin, että näillä instansseilla ei ole suoraa pääsyä prototype
esine, mutta vain sen ominaisuudet. Esimerkiksi:
// 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
Tässä on tärkeä ja hieman hienovarainen asia: vaikka p1
luotiin ennen getFullName
menetelmä määritettiin, sillä on silti pääsy siihen, koska sen prototyyppi on Person
prototyyppi.
(On syytä huomata, että selaimet tallentavat myös viitteen minkä tahansa objektin prototyyppiin __proto__
-ominaisuudessa, mutta se on todella huono käytäntö päästäksesi prototyyppiin suoraan __proto__
-palvelun kautta omaisuutta, koska se ei ole osa standardia ECMAScript-kielimääritys , joten älä tee sitä! )
Koska p1
Person
esineellä ei itsellään ole suoraa pääsyä prototype
objekti, jos haluamme korvata getFullName
p1
: ssa tekisimme sen seuraavasti:
// We reference p1.getFullName, *NOT* p1.prototype.getFullName, // since p1.prototype does not exist: p1.getFullName = function(){ return 'I am anonymous'; }
Nyt p1
on oma getFullName
omaisuus. Mutta p2
esiintymä (luotu aikaisemmassa esimerkissämme) tekee ei omistaa tällaista omaisuutta. Siksi p1.getFullName()
käyttää getFullName
menetelmä p1
itse, samalla kun vedotaan p2.getFullName()
menee prototyyppiketjussa ylöspäin Person
prototyypin objekti ratkaistavaksi getFullName
:
console.log(p1.getFullName()); // prints 'I am anonymous' console.log(p2.getFullName()); // prints 'Robert Doe'
Toinen tärkeä asia on olla tietoinen siitä, että se on myös mahdollista dynaamisesti muuttaa kohteen prototyyppiä. Esimerkiksi:
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'
Kun käytät prototyyppistä perintöä, muista määritellä ominaisuudet prototyypissä jälkeen joko perinyt vanhempaluokasta tai määrittänyt vaihtoehtoisen prototyypin.
Yhteenvetona voidaan todeta, että ominaisuushaut JavaScript-prototyyppiketjun kautta toimivat seuraavasti:
hasOwnProperty
-menetelmää voidaan käyttää tarkistamaan, onko objektilla tietty nimetty ominaisuus.)Object.prototype
on saavutettu eikä sillä ole omaisuutta, ominaisuus katsotaan undefined
.Prototyyppisen perimisen ja omaisuuden hakutoiminnan ymmärtäminen on yleensä tärkeää kehittäjille, mutta on myös välttämätöntä sen (joskus merkittävien) JavaScript-suorituskyvyn seurausten vuoksi. Kuten asiakirjoissa mainitaan V8 (Googlen avoimen lähdekoodin, korkean suorituskyvyn JavaScripti-moottori), useimmat JavaScript-moottorit käyttävät sanakirjamaista tietorakennetta objektiominaisuuksien tallentamiseen. Jokainen ominaisuuden käyttö edellyttää siis dynaamista hakua kyseisestä tietorakenteesta ominaisuuden ratkaisemiseksi. Tämän lähestymistavan ansiosta ominaisuuksien käyttö JavaScriptissä on yleensä paljon hitaampaa kuin ohjelmointikielien, kuten Java ja Smalltalk, ilmentymämuuttujien käyttö.
Toinen JavaScript-hakumekanismi perustuu laajuuteen.
Jotta voisimme ymmärtää, miten tämä toimii, on otettava käyttöön käsite suoritusyhteys .
JavaScriptissä on kahden tyyppisiä suoritusyhteyksiä:
Suorituskontekstit on järjestetty pinoksi. Pinon alaosassa on aina globaali konteksti, joka on ainutlaatuinen jokaiselle JavaScript-ohjelmalle. Joka kerta kun toiminto havaitaan, luodaan uusi suoritusyhteys ja työnnetään pinon yläosaan. Kun toiminto on suoritettu loppuun, sen konteksti ponnahtaa pois pinosta.
Harkitse seuraavaa koodia:
// 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 Jokaisessa suorituskontekstissa on erityinen objekti nimeltä a laajuusketju jota käytetään muuttujien ratkaisemiseen. Laajuusketju on pohjimmiltaan pino tällä hetkellä käytettävissä olevia ulottuvuuksia välittömimmästä kontekstista globaaliin kontekstiin. (Tarkemmin sanottuna pinon yläosassa olevaa kohdetta kutsutaan Aktivointiobjekti joka sisältää viittauksia suoritettavan funktion paikallisiin muuttujiin, nimetyt funktion argumentit ja kaksi 'erityistä' objektia: this
ja arguments
.) Esimerkiksi:

Huomaa yllä olevassa kaaviossa, miten this
osoittaa window
oletusarvoisesti ja kuinka globaali konteksti sisältää esimerkkejä muista objekteista, kuten console
ja location
.
Kun yritetään ratkaista muuttujia laajuusketjun kautta, tarkistetaan ensin välittömässä kontekstissa vastaava muuttuja. Jos vastaavuutta ei löydy, tarkistetaan seuraava kontekstiobjekti ketjun ketjussa ja niin edelleen, kunnes vastaavuus löytyy. Jos vastaavuutta ei löydy, a ReferenceError
heitetään.
On myös tärkeää huomata, että uusi laajuus lisätään soveltamisalaketjuun, kun try-catch
lohko tai with
esto on havaittu. Kummassakin näistä tapauksista uusi objekti luodaan ja sijoitetaan kattavuusketjun yläosaan:
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);
Jotta voisimme täysin ymmärtää, miten laajuuspohjaiset muuttujahaut tapahtuvat, on tärkeää pitää mielessä, että JavaScriptissä ei tällä hetkellä ole lohkotason laajuuksia. Esimerkiksi:
for (var i = 0; i <10; i++) { /* ... */ } // 'i' is still in scope! console.log(i); // prints '10'
Useimmissa muissa kielissä yllä oleva koodi johtaisi virheeseen, koska muuttujan 'elämä' (ts. Laajuus) i
rajoitettaisiin for for -lohkoon. JavaScriptissä tämä ei kuitenkaan ole asia. Pikemminkin i
lisätään aktivointikohteeseen laajuusketjun yläosassa ja se pysyy siellä, kunnes kyseinen objekti poistetaan laajuudesta, mikä tapahtuu, kun vastaava suoritusyhteys poistetaan pinosta. Tämä käyttäytyminen tunnetaan vaihtelevana nostona.
On kuitenkin syytä huomata, että lohkotason laajuuksien tuki on siirtymässä JavaScriptiin uuden kautta let
avainsana. let
avainsana on jo saatavana JavaScript 1.7: ssä ja siitä on tarkoitus tulla virallisesti tuettu JavaScript-avainsana ECMAScript 6: sta lähtien.
JavaScript-suorituskyvyn haarat
Tapa, jolla ominaisuus- ja muuttuvahaku, prototyyppiketjua ja soveltamisalaketjua käyttäen, toimivat JavaScriptissä, on yksi kielen tärkeimmistä ominaisuuksista, mutta kuitenkin yksi hankalimmista ja hienovaraisimmista ymmärtää.
Tässä esimerkissä kuvatut hakutoiminnot toistetaan joko prototyyppiketjun tai laajuusketjun perusteella joka kun ominaisuutta tai muuttujaa käytetään. Kun tämä haku tapahtuu silmukoissa tai muissa intensiivisissä operaatioissa, sillä voi olla merkittäviä JavaScript-suorituskyvyn haittoja, etenkin kun otetaan huomioon kielen yksisäikeinen luonne, joka estää useita toimintoja samanaikaisesti.
Harkitse seuraavaa esimerkkiä:
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');
Tässä esimerkissä meillä on pitkä perintöpuu ja kolme sisäkkäistä silmukkaa. Syvimmän silmukan sisällä laskurimuuttujaa lisätään arvoon delta
Mutta delta
sijaitsee melkein perintöpuun yläosassa! Tämä tarkoittaa, että joka kerta child.delta
on käytettävissä, koko puussa on navigoitava alhaalta ylös. Tällä voi olla todella negatiivinen vaikutus suorituskykyyn.
Tämän ymmärtämisen avulla voimme helposti parantaa yllä mainittujen nestedFn
toiminto käyttämällä paikallista delta
muuttuja välimuistiin child.delta
(ja siten välttää koko perintöpuun toistuvan kulkemisen tarvetta) seuraavasti:
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');
Tietenkin tämä tekniikka on käyttökelpoinen vain tilanteessa, jossa tiedetään, että child.delta
: n arvo ei muutu for for -silmukoiden suorituksen aikana; muuten paikallinen kopio olisi päivitettävä nykyisellä arvolla.
OK, ajetaan nestedFn
: n molemmat versiot ja katso, onko näiden kahden välillä havaittavissa oleva suorituskykyero.
Aloitetaan suorittamalla ensimmäinen esimerkki kohdassa node.js REPL :
[email protected] :~$ node test.js Final result: 10000000000 Total time: 8270 milliseconds
Joten kestää noin 8 sekuntia. Se on pitkä aika.
Katsotaan nyt, mitä tapahtuu, kun suoritamme optimoidun version:
[email protected] :~$ node test2.js Final result: 10000000000 Total time: 1143 milliseconds
Tällä kertaa se kesti vain yhden sekunnin. Paljon nopeampi!
Huomaa, että paikallisten muuttujien käyttö kalliiden hakujen välttämiseksi on tekniikka, jota voidaan käyttää sekä ominaisuushakuun (prototyyppiketjun kautta) että muuttuviin hakuihin (laajuusketjun kautta).
Tämän tyyppinen arvojen 'välimuistiin tallentaminen' (ts. Paikallisen laajuuden muuttujiin) voi olla hyödyllistä myös käytettäessä joitain yleisimpiä JavaScript-kirjastoja. Ota jQuery , esimerkiksi. jQuery tukee 'valitsimien' käsitettä, jotka ovat periaatteessa mekanismi yhden tai useamman vastaavan elementin hakemiseksi TUOMIO . Valintojen määrittelyn helppous jQueryssä voi saada unohtamaan, kuinka kallis (suorituskyvyn kannalta) kukin valitsimen haku voi olla. Vastaavasti valintahaun tulosten tallentaminen paikalliseen muuttujaan voi olla erittäin hyödyllistä suorituskyvylle. Esimerkiksi:
// 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);
Erityisesti verkkosivulla, jossa on paljon elementtejä, yllä olevan koodinäytteen toinen lähestymistapa voi johtaa merkittävästi parempaan suorituskykyyn kuin ensimmäinen.
Paketoida
Datan haku JavaScriptissä on melko erilainen kuin useimmilla muilla kielillä, ja se on erittäin vivahteikas. Siksi on välttämätöntä ymmärtää nämä käsitteet täysin ja asianmukaisesti, jotta kieli todella oppisi. Tietojen haku ja muita yleisiä JavaScript-virheitä tulisi välttää aina kun mahdollista. Tämä ymmärrys tuottaa todennäköisesti puhtaamman ja vankemman koodin, joka parantaa JavaScript-suorituskykyä.
Liittyvät: JS-kehittäjänä tämä pitää minut yllä yöllä / tuntee ES6-luokan sekaannusta