ECMAScript-luokan tai objektin määrittäminen
- Edellinen sivu Objektin toimialue
- Seuraava sivu Muokkaa objektia
Predefinoitujen objektien käyttö on vain osa objektioiden suuntautuneen ohjelmointikieksen kykyjä, mutta sen todellinen voima on kyky luoda omia erityisiä luokkia ja objekteja.
ECMAScriptillä on monia tapoja luoda objekteja tai luokkia.
Tehtaan tapa
Alkuperäinen tapa
Koska objektin ominaisuudet voidaan määritellä dynaamisesti objektin luomisen jälkeen, monet kehittäjät kirjoittivat ensimmäisen JavaScript-version aikana seuraavanlaisia koodia:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
Yllä olevassa koodissa luodaan objekti car. Sitten sille asetetaan muutamia ominaisuuksia: sen väri on sininen, sillä on neljä ovea ja sen bensan kulutus on 25 mailia/gallona. Viimeinen ominaisuus on funktio viittaaja, mikä tarkoittaa, että ominaisuus on metodi. Kun tämä koodi suoritetaan, voidaan käyttää objektia car.
On kuitenkin ongelma, että saattaa tarvita useita car-objektien instansseja.
Ratkaisu: tehdasmenetelmä
Ongelman ratkaisemiseksi kehittäjät loivat voiman luoda ja palauttaa tietyn tyyppisiä objekteja tehdasfunktioiden avulla (factory function).
Esimerkiksi createCar() -funktio voidaan käyttää sisällyttämään edellä luetellut car-objektin luomisoperationit:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
Tässä ensimmäisessä esimerkissä kaikki koodi sisältyy createCar() -funktioon. Lisäksi on yksi ylimääräinen rivi, joka palauttaa car-objektin (oTempCar) funktioiden arvona. Kun kutsutaan tätä funktiota, luodaan uusi objekti, ja sille annetaan kaikki tarvittavat ominaisuudet, kopioitaan car-objekti, josta puhuimme aikaisemmin. Tällä tavalla voimme helposti luoda car-objektin kaksi versiota (oCar1 ja oCar2), joilla on täysin samat ominaisuudet.
Toimita parametreja funktiolle
Voimme myös muokata createCar() -funktiota ja antaa sille eri ominaisuuksien oletusarvot sen sijaan, että annamme vain oletusarvot ominaisuuksille:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // Tulostaa "red" oCar2.showColor(); // Tulostaa "blue"
Lisäämällä parametreja createCar() -funktioon, voit määrittää luotavan car-objektin color, doors ja mpg-ominaisuudet. Tämä mahdollistaa kahden objektin samojen ominaisuuksien määrittämisen, mutta eri arvoilla.
Objektien metodi määritellään ulkopuolella tehdasfunktiota
Vaikka ECMAScript yhä virallisempi, objektien luomistapa on jätetty huomiotta ja sen standardointi on edelleen vastustettu. Osittain se johtuu semantiikasta (se ei näytä yhtä viralliselta kuin käyttää konstruktoria new), osittain toiminnallisista syistä. Toiminnallinen syy on, että tämän tyyppisessä luomisessa on luotava objektin luomismetodi. Edellisessä esimerkissä jokaisen kutsun createCar() luo uuden funktion showColor(), mikä tarkoittaa, että jokaisella objektilla on oma showColor() versionsa. Toinen mahdollisuus on, että kaikki objektit jakavat saman funktion.
Jotkut kehittäjät määrittävät objektin metodit tehdasfunktion ulkopuolella ja viittaavat niihin ominaisuuden kautta, mikä estää tämän ongelman:}
function showColor() { alert(this.color); } function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // Tulostaa "red" oCar2.showColor(); // Tulostaa "blue"
Tässä muokatussa koodissa, showColor()-funktio määritellään createCar()-funktio ennen sen määrittämistä. createCar()-funktiossa annetaan objektille viittaus olemassa olevaan showColor()-funktioon. Tämä ratkaisee toistuvan funktion objektin luomisen ongelman toiminnallisesti; mutta semanttisesti tämä ei ole erityisen objektille ominainen metodi.
Kaikki nämä kysymykset ovat johtaneetkehittäjien määrittämäkonstruktorn esiintyminen.
Konstruktormenetelmä
Luodaan konstruktoria yhtä helposti kuin tehdasfunktiota. Ensimmäinen askel on valita luokan nimi, eli konstruktorn nimi. Perinteisesti tämän nimen ensimmäinen kirjain on ison kirjaimen muodossa, jotta se erottuu pienikirjaimisista muuttujien nimistä. Muuten konstruktorn näyttää paljon tehdasfunktiolta. Tarkastellaan seuraavaa esimerkkiä:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.showColor = function() { alert(this.color); }; } var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25);
Tässä selitetään yllä olevan koodin ja tehdasmenetelmän välinen ero. Ensimmäinen askel on, että konstruktoreissa ei luoda objekteja, vaan käytetään this-kirjainta. Kun käytetään new-laskinta konstruktoreissa, objekti luodaan ennen ensimmäisen rivin suorittamista, ja vain this-avaimella voidaan käyttää tätä objektia. Tämän jälkeen this-ominaisuus voidaan suoraan asettaa, oletusarvoisesti se on konstruktorn palauttama arvo (ei ole välttämätöntä käyttää return-laskinta).
Nyt, kun luodaan objekteja new-laskimella ja Car-luokalla, se on enemmän kuin yleisten ECMAScript-objektien luominen.
Saattaa olla, että kysyt, onko tämä tapa samassa ongelmassa kuin edellinen tapa hallita funktioita? Kyllä.
Kuten tehtaan funktio, rakentaja luo toistuvasti funktioita, luoden jokaiselle objektille erillisen funktioversio. Kuitenkin, kuten tehtaan funktiota, rakentajaa voidaan uudelleenkirjoittaa ulkoisella funktiona, mikä semanttisesti ei ole mielekästä. Tämä on juuri se, mikä tekee prototyypin tyylistä edun.
Prototyypin tyyli
Tämä tapa hyödyntää objektin prototype-ominaisuutta voidaan nähdä luovan uuden objektin perustana.
Tässä ensin asetetaan tyypin nimi tyhjällä rakentajalla. Sitten kaikki ominaisuudet ja metodit annetaan suoraan prototype-ominaisuudelle. Olemme uudelleenkirjoittaneet edellisen esimerkin seuraavasti:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
Tässä koodissa ensin määritellään rakentaja (Car), jossa ei ole minkäänlaista koodia. Seuraavat rivit lisäävät ominaisuuksia Car:n prototype-ominaisuuteen määrittääkseen Car-objektin ominaisuudet. Kun kutsutaan new Car(), kaikkien prototyypin ominaisuuksien arvot annetaan välittömästi luotavalle objektille, mikä tarkoittaa, että kaikki Car-oliot sisältävät viittauksen showColor()-funktioon. Semanttisesti kaikki ominaisuudet näyttävät kuuluvan yhteen objektiin, mikä ratkaisee edellisten kahden tavan ongelmat.
Lisäksi tällä tavalla voidaan käyttää instanceof-laskinta tarkistamaan annetun muuttujan osoittaman objektin tyyppiä. Siksi seuraava koodi tulostaa TRUE:
alert(oCar1 instanceof Car); // Tulostaa "true"
Prototyypin tyylin ongelmat
Prototyypin tyyli näyttää olevan hyvä ratkaisu. Valitettavasti, se ei ole täydellinen.
Ensimmäiseksi, tämä rakentaja ei ota parametreja. Prototyypin avulla ei voida antaa parametreja rakentajalle alustamaan ominaisuuksien arvoja, koska Car1 ja Car2:n color-ominaisuudet ovat molemmat "blue", doors-ominaisuudet molemmat 4 ja mpg-ominaisuudet molemmat 25. Tämä tarkoittaa, että ominaisuuksien oletusarvoja ei voi muuttaa ennen kuin objekti on luotu, mikä on erittäin ärsyttävää, mutta ei vielä kaikki. Todellinen ongelma ilmenee, kun ominaisuudet osoittavat objekteihin eikä funktioihin. Funktioiden jaetun käytön ei aiheuta ongelmia, mutta objektien jaettu käyttö on harvinaista. Ajattele seuraavaa esimerkkiä:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.drivers = new Array("Mike","John"); Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car(); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // Tulostaa "Mike,John,Bill" alert(oCar2.drivers); // Tulostaa "Mike,John,Bill"
Yllä olevassa koodissa ominaisuus drivers on osoitin Array-objektiin, joka sisältää kaksi nimeä "Mike" ja "John". Koska drivers on viittausarvo, Car:n kaksi instanssia osoittavat samaan taulukkoon. Tämä tarkoittaa, että kun oCar1.drivers:ään lisätään arvo "Bill", sitä voidaan nähdä myös oCar2.drivers:ssa. Näytettäessä joko näistä kahdesta osoittimesta, tuloksena on merkkijono "Mike,John,Bill".
Koska objektien luomisessa on niin monta kysymystä, varmasti ajattelet, onko olemassa järkevää tapaa luoda objekteja? Vastaus on kyllä, tarvitaan rakentajafunktioiden ja prototyyppejen yhdistelmä.
Yhdistetty rakentajafunktiot/prototyypit
Yhdistämällä rakentajafunktiota ja prototyyppejä voidaan luoda objekteja samalla tavalla kuin muilla ohjelmointikielillä. Tämä konsepti on erittäin yksinkertainen: määritellään objektin kaikki ei-funktiomäärittelyt rakentajafunktiolla, ja objektin funktiomäärittelyt (menetelmät) määritellään prototyyppeillä. Tulevaisuudessa kaikki funktiot luodaan vain kerran, ja jokaisella objektilla on oma objektimäärittelysä.
Olemme uudelleenkirjoittaneet edellisen esimerkin, koodi seuraavasti:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // Tulostaa "Mike,John,Bill" alert(oCar2.drivers); // Tulostaa "Mike,John"
Nyt se on enemmän kuin yleisen objektin luominen. Kaikki ei-funktiomäärittelyt luodaan rakentajafunktiolla, mikä tarkoittaa, että voimme antaa ominaisuuksille oletusarvot rakentajafunktion parametreilla. Koska luodaan vain showColor() -funktioyksilö, ei ole muistia tuhlausta. Lisäksi, kun oCar1:n drivers-taulukkoon lisätään arvo "Bill", se ei vaikuta oCar2:n taulukkoon, joten näytettäessä näiden taulukkojen arvoja, oCar1.drivers näyttää "Mike,John,Bill", ja oCar2.drivers näyttää "Mike,John". Koska käytetään prototyyppejä, voidaan edelleen käyttää instanceof-laskinta määrittääksemme objektin tyyppiä.
这种方式是ECMAScript采用的主要方式,它具有其他方式的特性,却没有他们的副作用。不过,有些开发者仍觉得这种方法不够完美。
Dynamic prototype method
对于习惯使用其他语言的开发者来说,使用mixed constructor/proto方式感觉不那么和谐。毕竟,定义类时,大多数面向对象语言都对属性和方法进行了视觉上的封装。请考虑下面的Java类:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java很好地打包了Car类的所有属性和方法,因此看到这段代码就知道它要实现什么功能,它定义了一个对象的信息。批评mixed constructor/proto方式的人认为,在constructor内部找属性,在其外部找方法的做法不合逻辑。因此,他们设计了dynamic prototype method,以提供更友好的编码风格。
Dynamic prototype method的基本思想与mixed constructor/proto方式相同,即在constructor内定义非function属性,而function属性则利用prototype属性定义。唯一的区别是赋予对象方法的位置。下面是使用dynamic prototype method重写的Car类:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined") { Car.prototype.showColor = function() { alert(this.color); }; Car._initialized = true; } }
Tämä rakentajafunktio ei muutu, ennen kuin tarkistetaan, onko typeof Car._initialized yhtä kuin "undefined". Tämä on dynaamisen alkuperäisen menetelmän tärkein osa. Jos tämä arvo on määritelty (ja sen arvo on true, typeof arvon arvo on Boolean), niin tätä metodia ei luoda. Yksinkertaistettuna tämä metodi käyttää merkkiä (_initialized) arvioimaan, onko prototyypille annettu mitään menetelmiä. Tämä metodi luodaan ja asetetaan vain kerran, ja perinteiset OOP-kehittäjät huomaavat, että tämä koodi näyttää enemmän muista kielistä luoduilta luokilta.
Yhdistetty tehdasfunktio tapa
Tämä tapa on yleensä vaihtoehto, kun edellinen tapa ei voi olla käytössä. Tarkoituksena on luoda väliaikainen rakentajafunktio, joka palauttaa vain toisen objektin uuden instanssin.
Tämä koodi näyttää hyvin samalta kuin tehdasfunktio:
function Car() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; }
Eri kuin klassinen tapa, tämä tapa käyttää new-oppausta, mikä tekee siitä näyttävän todelliselta rakentajafunktioilta:
var car = new Car();
Koska Car()-rakentajafunktiossa kutsutaan new-oppausta, toinen new-oppaus (konstruktorin ulkopuolella) sivuutetaan, ja rakentajafunktiosta luodut objektit siirretään muuttujaan car.
Tämä tapa kohtaa samat ongelmat objektin metodien sisäisessä hallinnassa kuin klassinen tapa. Tämä tapa suositellaan välttämään, ellei muuta ole mahdollista.
Mikä tapa?
Kuten edellä mainittiin, tällä hetkellä eniten käytetty on sekayhdistelmä rakentajafunktio/projekti. Lisäksi dynaamiset alkuperäiset menetelmät ovat suosittuja, ja ne ovat toiminnallisesti yhtäarvoisia rakentajafunktio/projekti-tyylille. Voit käyttää joko näistä kahdesta menetelmästä. Älä kuitenkaan käytä yksinomaan klassista rakentajafunktio/projekti-tyyliä, koska tämä voi aiheuttaa ongelmia koodissa.
Esimerkki
Objektin kiinnostava一面 on tapa, jolla ratkaisut löydetään. ECMAScriptissä yleisin ongelma on merkkijonkon yhdistämisen suorituskyky. Muiden kielten tavoin ECMAScriptin merkkijonot ovat muuttumattomia, eli niiden arvoja ei voi muuttaa. Tarkastele seuraavaa koodia:
var str = "hello "; str += "world";
Todellisuudessa tämä koodi suorittaa seuraavat vaiheet taustalla:
- Luo merkkijono, joka tallentaa "hello ".
- Luo merkkijono, joka tallentaa "world".
- Luo yhdistetyn tuloksen tallentava merkkijono.
- Kopioi str:n nykyisen sisällön tulokseen.
- Kopioi "world" tulokseen.
- Päivitä str, jotta se osoittaa tulokseen.
Jokainen merkkijonon yhdistäminen suorittaa vaiheet 2-6, mikä tekee tästä toiminnasta erittäin resursseja kuluttavan. Jos tämä prosessi toistetaan satoja tai jopa tuhansia kertoja, voi syntyä suorituskykyongelmia. Ratkaisuna on tallentaa merkkijonot Array-objektiin ja lopuksi luoda lopullinen merkkijono join() -menetelmän avulla (parametri on tyhjä merkkijono). Kuvailemme seuraavaa koodia korvaamaan edellisen koodin:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join('');
Näin ei ole ongelmia, vaikka taulukkoon lisätään kuinka monta merkkijonoa tahansa, koska yhdistäminen tapahtuu vain join() -menetelmän kutsun yhteydessä. Tässä vaiheessa suoritettavat vaiheet ovat seuraavat:
- Luo tulokseen tallentava merkkijono
- Kopioi jokainen merkkijono tulokseen sopivaan paikkaan
Vaikka tämä ratkaisu on hyvä, on olemassa parempia tapoja. Ongelma on, että tämä koodi ei tarkasti kuvaa sen tarkoitusta. Jotta se olisi helpommin ymmärrettävä, voit pakata tämän toiminnallisuuden StringBuffer-luokkaan:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(''); };
Tämä koodin ensimmäinen huomioitava osa on strings-ominaisuus, joka on alkuperäisesti yksityinen ominaisuus. sillä on vain kaksi metodia, nimittäin append() ja toString() -metodit. append() -metodi ottaa yhden parametrin, joka liittää kyseisen parametrin merkkijonon taulukkoon, toString() -metodi kutsuu taulukon join() -menetelmää ja palauttaa todellisesti yhdistetyn merkkijonon. Jotta voit yhdistää useita merkkijonoja StringBuffer-objektin avulla, voit käyttää seuraavaa koodia:
var buffer = new StringBuffer(); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
Voit testata StringBuffer-objektin ja perinteisen merkkijonojen yhdistämismetodin suorituskykyä seuraavalla koodilla:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenation with plus: ") + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("<br />Concatenation with StringBuffer: ") + (d2.getTime() - d1.getTime()) + " milliseconds");
Tämä koodi suorittaa kaksi testiä merkkijonojen yhdistämisestä, ensimmäinen plus-merkin avulla, toinen StringBuffer-luokan avulla. Jokainen operaatio yhdistää 10000 merkkijonoa. Päivämääräarvot d1 ja d2 käytetään arvioimaan operaation kestoa. Huomaa, että jos Date-objektia luodaan ilman parametreja, objekti saa nykyisen päivämäärän ja kellonajan. Yhdistämistoiminnan keston laskemiseksi riittää vähentämään päivämäärien millisekuntien esitykset (getTime() -menetelmän palauttaman arvon). Tämä on yleinen tapa mitata JavaScriptin suorituskykyä. Tämän testin tulokset voivat auttaa sinua vertailemaan StringBuffer-luokan ja plus-merkin tehokkuutta.
- Edellinen sivu Objektin toimialue
- Seuraava sivu Muokkaa objektia