Niin monimutkaisilta kuin JavaScriptin suoritusaikaiset tietorakenteet ehkä näyttävätkin, oikeastaan kaiken takana on vain kolmenlaisia oliota: funktio-olioita, niihin liittyviä prototyyppiolioita ja "tavallisia" olioita. Kaikilla olioilla – paitsi yhdellä – on oma prototyyppinsä, josta se perii ominaisuuksia. Periytyminen on kuitenkin erilaista kuin luokkapohjaisissa oliokielissä.
Huom: Vaikka olion prototyyppikin on olio, tällä kurssilla käytämme sanaa prototyyppiolio tarkoittamaan vain funktio-olioon liitettyä oliota. Tuon sanan yhteydessä on aina mainittava minkä funktion prototyyppioliosta on kysymys!
Jokaisella funktio-oliolla on siis oma prototyyppiolionsa, joka kuitenkaan EI OLE kyseisen funktion oma prototyyppi – paitsi yhdellä funktiolla: Funktiolla Function prototyyppi ja prototyyppiolio ovat sama asia, koska Function-olio perii funktio-ominaisuutensa juuri omalta prototyyppiolioltaan!
Konstruktorifunktioiden prototyyppiolioista muodostetaan linkitettyjä ketjuja, joista oliot perivät ominaisuuksia. Normaalisti jokaisen olion prototyyppien ketju päättyy Object-funktion prototyyppiolioon.
Kuulostaako monimutkaiselta? Proosatekstin kirjoittaminen aiheesta on aika hankalaa! Kyllä asiat selvenevät, aletaan piirrellä...
assign: function assign() { [native code] } getPrototypeOf: function getPrototypeOf() { [native code] } setPrototypeOf: function setPrototypeOf() { [native code] } getOwnPropertyDescriptor: function getOwnPropertyDescriptor() { [native code] } ... ... seal: function seal() { [native code] } isSealed: function isSealed() { [native code] } fromEntries: function fromEntries() { [native code] } prototype: [object Object] length: 1 name: Object
Object.defineProperty(opiskelija, 'arvo', {writable: true});Samaan tapaan myös itse ohjelmoitaviin konstruktorifunktioihin voidaan ohjelmoida "staattisia" kenttiä ja "luokkametodeita"!
toSource: function toSource() { [native code] } toString: function toString() { [native code] } toLocaleString: function toLocaleString() { [native code] } valueOf: function valueOf() { [native code] } ... ... __lookupGetter__: function __lookupGetter__() { [native code] } __lookupSetter__: function __lookupSetter__() { [native code] } __proto__: null constructor: function Object() { [native code] }
kissantassu.toString()
prototype: function () { } length: 1 name: Function
write(Object.toString.call(Function) ) // function Function() { // [native code] // }
Function.prototype.constructor===Function Function.prototype.__proto__===Object.prototype Function.prototype===Function.__proto__Tuosta __proto__-kentästä kohta lisää.
toSource: function toSource() { [native code] } toString: function toString() { [native code] } apply: function apply() { [native code] } call: function call() { [native code] } bind: function bind() { [native code] } isGenerator: function isGenerator() { [native code] } arguments: null caller: null constructor: function Function() { [native code] } length: 0 name:
var x = {a:1} oK(Object.getPrototypeOf(x))Tuloksena saadaan Object-konstruktorifunktion prototyyppiolio Kyseessä on siis olio, johon Object.prototype-kenttä viittaa:
toSource: function toSource() { [native code] } toString: function toString() { [native code] } ... ... __proto__: null constructor: function Object() { [native code] }Tämä on juuri se olio, johon muiden olioiden prototyyppiketjut päättyvät (ellei niitä ole itse katkaistu).
Lisäksi – kuten arvata saattaa:
var x = {a:1} write(Object.getPrototypeOf(x)===x.__proto__) // true!
var x = {a:1} oK(Object.getPrototypeOf(x)) ok(x.__proto__) Object.getPrototypeOf(Object.prototype)===null Object.prototype.__proto__===null
function F() {this.a=2} var x = new F() var y = new F()
Tämä on siis oman konstruktorifunktion F prototyyppiketju. F perii yleiset funktio-ominaisuutensa Function-funktion prototyyppioliolta ja sen jälkeen yleiset olio-ominaisuutensa Object-funktion prototyyppioliolta.
F-funktion prototype-kentän viittaaman F-funktion prototyyppiolion prototyyppi on siis Object.prototype-kentän viittaama Object-funktion prototyyppiolio. Kyseessä on siis "tavallinen" olio, ei funktio-olio.
Funktion F.prototype-kentän viittaamassa prototyyppioliossa on kenttä constructor, joka viittaa suoraan takaisin omaan funktio-olioonsa F. Näin ovat asiat kaikilla funktoilla, ellei linkitystä itse muuteta.
Olioiden x ja y prototyyppiketjun ensimmäinen prototyyppiolio on yhteinen, F.prototype-olio. Molemmilla on oma versionsa kentästä a. Muut ominaisuutensa ne perivät ketjusta F.prototype —> Object.prototype —> null.
function Yli() {this.a=2} function Ali() {this.b=3} Ali.prototype = new Yli() // tällä tekniikalla konstruktoriin // liitetään "ylikonstruktori" var x = new Ali() var y = new Ali() x.a = 77
Ali.prototype.constructor===Yli // kenttä constructor löytyy linkin päästä!
Ali.prototype.hasOwnProperty("constructor")===false
Ali-olion prototype-kentän viittaaman olion prototyyppiketju on siis: Yli.prototype —> Object.prototype —> null.
function Yli() {this.a=2} function Ali() {this.b=3} Ali.prototype = new Yli() function AliAli() {this.c=4} AliAli.prototype = new Ali() var x = new AliAli() kK(x) // kaikki kentät, omat ja peritytTulostus:
-------- c: 4 -------- b: 3 -------- a: 2 -------- constructor: function Yli() {this.a=2} -------- toSource: function toSource() { [native code] } toString: function toString() { [native code] } ... ... __proto__: null constructor: function Object() { [native code] }
function Elain() { this.paino = 0 } Elain.prototype.asetaPaino = function (maara) {this.paino=maara} Elain.prototype.syo = function (maara) {this.paino+=maara} Elain.prototype.kuluta = function (maara) {this.paino-=maara} function Tuotantoelain() { this.tuotto = 0 } Tuotantoelain.prototype = new Elain() Tuotantoelain.prototype.asetaTuotto = function (maara) {this.tuotto=maara} Tuotantoelain.prototype.tuota = function () {return this.tuotto} function Lehma(nimi) { this.nimi = nimi || "nimetön"; // kelvoton nimi => oletusarvoksi } // (tämä on JavaScript-idiomi!) Lehma.prototype = new Tuotantoelain() Lehma.prototype.toString = function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } var m = new Lehma("Mansikki") oK(m) // nimi: Mansikki (vain yksi oma kenttä!) write(m) // Mansikki: 0 Kg, 0 litraa m.asetaPaino(500) oK(m) // nimi: Mansikki (kaksi omaa kenttää) // paino: 500 write(m) // Mansikki: 500 Kg, 0 litraa m.asetaTuotto(20) m.syo(15) oK(m) // nimi: Mansikki (kolme omaa kenttää) // paino: 515 // tuotto: 20 write(m) // Mansikki: 515 Kg, 20 litraa m.kuluta(100) write(m) // Mansikki: 415 Kg, 20 litraa write("Tuotetaan "+m.tuota()) // Tuotetaan 20
-------- nimi: Mansikki paino: 415 tuotto: 20 -------- tuotto: 0 toString: function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } -------- paino: 0 asetaTuotto: function (maara) {this.tuotto=maara} tuota: function () {return this.tuotto} -------- constructor: function Elain() { this.paino = 0 } asetaPaino: function (maara) {this.paino=maara} syo: function (maara) {this.paino+=maara} kuluta: function (maara) {this.paino-=maara} -------- toSource: function toSource() { [native code] } toString: function toString() { [native code] } ... __proto__: null constructor: function Object() { [native code] } //... siis taas vanha tuttu Object.prototype // eli Object-funktion prototyyppiolio
function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } Elain.prototype.syo = function (maara) {this.paino+=maara} Elain.prototype.kuluta = function (maara) {this.paino-=maara} function Tuotantoelain(kg, litr) { this.super = Elain this.super(kg) this.tuotto = litr || 0; // kelvoton tuotto => nollaksi } Tuotantoelain.prototype = new Elain() Tuotantoelain.prototype.tuota = function () {return this.tuotto} function Lehma(nimi, kg, litr) { this.super = Tuotantoelain this.super(kg,litr) this.nimi = nimi || "nimetön"; // kelvoton nimi => oletusarvoksi } Lehma.prototype = new Tuotantoelain() Lehma.prototype.toString = function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } var m = new Lehma("Mansikki", 500, 12) write(m) // Mansikki: 500 Kg, 12 litraa kK(m)
-------- m ------------------------------- super: function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } paino: 500 tuotto: 12 nimi: Mansikki -------- Lehma.prototype ----------------- super: function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } paino: 0 tuotto: 0 toString: function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } -------- Tuotantoelain.prototype ---------- paino: 0 tuota: function () {return this.tuotto} -------- Elain.prototype ------------------ constructor: function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } syo: function (maara) {this.paino+=maara} kuluta: function (maara) {this.paino-=maara} -------- Object.prototype ----------------- toSource: function toSource() { [native code] } toString: function toString() { [native code] } ... __proto__: null constructor: function Object() { [native code] } //... siis taas vanha tuttu Object.prototype // eli Object-funktion prototyyppiolio
function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } Elain.prototype.syo = function (maara) {this.paino+=maara} Elain.prototype.kuluta = function (maara) {this.paino-=maara} function Tuotantoelain(kg, litr) { Elain.call(this,kg) this.tuotto = litr || 0; // kelvoton tuotto => nollaksi } Tuotantoelain.prototype = new Elain() Tuotantoelain.prototype.tuota = function () {return this.tuotto} function Lehma(nimi, kg, litr) { Tuotantoelain.call(this,kg,litr) this.nimi = nimi || "nimetön"; // kelvoton nimi => oletusarvoksi } Lehma.prototype = new Tuotantoelain() Lehma.prototype.toString = function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } var m = new Lehma("Mansikki", 500, 12) write(m) // Mansikki: 500 Kg, 12 litraa kK(m)
-------- paino: 500 tuotto: 12 nimi: Mansikki -------- paino: 0 tuotto: 0 toString: function () { return this.nimi+ ": "+this.paino+" Kg, "+ this.tuotto+" litraa" } -------- paino: 0 tuota: function () {return this.tuotto} -------- constructor: function Elain(kg) { this.paino = kg || 0; // kelvoton paino => nollaksi } syo: function (maara) {this.paino+=maara} kuluta: function (maara) {this.paino-=maara} -------- toSource: function toSource() { [native code] } toString: function toString() { [native code] } ... __proto__: null constructor: function Object() { [native code] } //... siis taas vanha tuttu Object.prototype // eli Object-funktion prototyyppiolio
m instanceof Lehma m instanceof Tuotantoelain m instanceof Elain m instanceof Object
m.__proto__ —> Lehma.prototype Lehma.prototype.__proto__ —> Tuotantoelain.prototype Tuotantoelain.prototype.__proto__ —> Elain.prototype Elain.prototype.__proto__ —> Object.prototype Object.prototype.__proto__ —> null
var x = {a:1} var y = Object.create(x) var z = Object.create(x) y.a=3 x.a=666 write(x.a) // 666 write(y.a) // 3 write(z.a) // 666 (!)
create-funktio on tietääkseni alunperin Douglas Crockfordin idea,
ks.hänen artikkelinsa
Prototypal Inheritance in JavaScript.
Hän esitti sen lisättäväksi ohjelmaan muodossa, jossa mahdollisesti
jo varusfunktiona toteutettua Object.create-funktiota ei
korvata:
if (typeof Object.create !== 'function') { // jos ei ole, lisätään
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
Vastaavalla tekniikalla voidaan pyrkiä varmistamaan että
jossain vanhassa selaimessakin on käytettävissä funktio, jota
oma sovellus käyttää!
Object.create = function (o) { function F() {} F.prototype = o; return new F(); };ja tutkitaan, mitä äskeisessä esimerkissä tapahtuu.
var x = {a:1} var y = {} y.__proto__=x var z = {} z.__proto__=x y.a=3 x.a=666 write(x.a) // 666 write(y.a) // 3 write(z.a) // 666 (!)
function Yli() {this.a=2} function Ali() {this.b=3} Ali.prototype = new Yli() var x = new Ali() var y = new Ali() write(x.a) // 2 Ali.prototype.a=666 write(x.a) // 666
Tällaista ohjelmoija tuskin kuitenkaan tekee vahingossa!
var Animal = { traits: {} } var lion = Object.create(Animal); lion.traits.legs = 4; var bird = Object.create(Animal); bird.traits.legs = 2; write(lion.traits.legs) // 2
function Animal() {this.traits = {} } var lion = new Animal(); lion.traits.legs = 4; var bird = new Animal(); bird.traits.legs = 2; write(lion.traits.legs) // 4