JavaScript objektinis modelis

JavaScript yra unikalus. Unikalus ir jo objektinis modelis. Tikriausiai esi pripratęs prie C++ objektinio modelio, kuris yra paremtas duomenų struktūros aprašymu — klasėmis, tačiau pasaulyje yra žmonių, teigiančių, kad tai yra neteisingas sprendimas.
JavaScript mylėtojai yra vieni iš tokių. Jie teigia, kad duomenys yra svarbiausi ir būtent duomenys (o ne duomenų struktūros aprašas — klasės) turėtų nulemti objekto struktūrą. Iš to gimsta laisvė.

Šį kartą paaiškinsiu kaip veikia prototipinis (JavaScript’o) objektinis modelis. O viskas yra labai paprasta: iš pradžių yra sukuriamas objektas. Objektui yra priskiriami atributai:

1
2
3
4
5
6
var obj = {};
obj.atributas1 = 'abc';
obj.atributas2 = 'antras';
obj.atributas3 = function() {
console.log('Labas, pasauli!');
}

Trūksta paveldėjimo? Yra toks ypatingas atributas __proto__. Per šį atributą galima sujungti du objektus.
Kitaip sakant kai nurodomas __proto__, tai objekte ieškant kokio nors atributo yra ieškoma tokia tvarka:

  1. pačiame objekte;
  2. jeigu nėra — ieškoma __proto__ objekte;
  3. jeigu nėra — ieškoma __proto__.__proto__ objekte;
  4. jeigu nėra — ieškoma __proto__.__proto__.__proto__ objekte;
1
2
3
4
5
6
7
var kitas = {};
kitas.atributas2 = 2;
kitas.__proto__ = obj;

console.log(kitas.atributas1); //atspausdins: 'abc' (iš obj)
console.log(kitas.atributas2); //atspausdins: 2 (iš kitas)
kitas.atributas3(); //atspausdins: 'Labas, pasauli!' (iškviečiama obj funkcija)

*IR TAI YRA VISKAS. *

Jūs ką tik išmokote visą JavaScript objektinio modelio esmę. Žinoma, ne viskas yra taip paprasta. Kabliukas yra tas, kad pikti dėdės atributo __proto__ naudoti neleidžia. Aš irgi neleidžiu. Toliau paaiškinsiu kaip išsisukti be jo.

Teisingas prototipų naudojimas

Kaip jau išsiaiškinome, atributo __proto__ naudoti nevalia. Jis yra nestandartizuotas ir negražus. Iš tikro reikia naudoti new operatorių arba Object.setPrototypeOf arba Object.create.

operatorius new

Panagrinėkime pavyzdį:

1
2
3
var Klasė = function(kintamasis){
this.atributas1 = kintamasis;
};

Ką mes čia parašėme? Viskas labai paprasta: sukūrėme naują funkciją Klasė. Iš tikro ta funkcija yra objektas (JavaScript’e *beveik* viskas yra objektas). Gal ir nematyti, bet mes tam objektui automagiškai priskyrėme dar ir prototype bei __proto__ atributus, kurie atrodo taip:

1
2
3
4
5
6
Klasė.__proto__ = Function.prototype; 

Klasė.prototype = {
constructor: Klasė,
__proto__: Function.prototype
};

Atributas prototype kol kas nieko nedaro. Nesvarbu ką mes jame nurodysime, jame atributų nebus ieškoma. Tai tik pagalbinis atributas, kurį naudoja operatorius new.

Pasižiūrėkime kaip jis veikia:

1
var obj = new Klasė('parametras');

Atrodo tik viena eilutė? Pažiūrėkite ką iš tikro parašėme:

1
2
3
var obj = {};
obj.__proto__ = Klasė.prototype;
obj.__proto__.constructor.call(obj, 'parametras');

Kaip matome viskas vyksta tiesiai per aplinkui, bet tokia jau ta JavaScript.

Object.create

Funkcija Object.create sukuria naują objektą, panašiai kaip ir var x = {}, tačiau tuo pačiu leidžia ir nurodyti prototipą:

1
2
3
4
5
6
7
8
9
Object.create = function(prototype, properties){
var result = {
__proto__: prototype
};

Object.defineProperties(result, properties);

return result;
}

Antras argumentas — atributų sąrašas, kuris veikia taip pat kaip ir Object.defineProperties, tad jis daugeliu atvejų nereikalingas.

Object.setPrototypeOf

Kadangi buvo uždrausta tiesiogiai naudoti __proto__ atributą, buvo sukurtos funkcijos tam apeiti: Object.setPrototypeOf ir Object.getPrototypeOf. Nors šias funkcijas pikti dėdės ir leidžia naudoti, bet nerekomenduoja, nes atseit lėtai veikia.

Praktinis pavyzdys

Išmėginkime standartinį atvejį su gyvūnėliais. Iš pradžių sukuriame bazinę klasę:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Gyvūnas = function(vardas){
this.vardas = vardas;
}

Gyvūnas.prototype = {
constructor: Gyvūnas,

balsas: function(){
console.log('pyrst');
},

prisistatymas: function(){
console.log('Labas! Aš esu ' + this.gautiTipą() +
'. Mano vardas: ' + this.vardas + '.');
}
}

Pavyzdyje Gyvūnas.prototype priskiriamas visiškai naujam objektui, todėl reikia iš naujo nurodyti konstruktorių. Galimas ir alternatyvus variantas: naudoti jau automatiškai sukurta Gyvūnas.prototype ir jam priskirti funkcijas (pvz. Gyvūnas.prototype.balsas = function(){...}), bet man gražesnis JSONinis užrašymo stilius.

Toliau kuriame vaikinę klasę pasinaudodami Object.create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Variantas naudojant Object.create
var Katinukas = function(vardas){
Object.getPrototypeOf(Katinukas.prototype).constructor.call(this, vardas);
//Tas pats kaip: Gyvūnas.call(this, vardas);
}

Katinukas.prototype = Object.create(Gyvūnas.prototype);
Katinukas.prototype.constructor = Katinukas;
Katinukas.prototype.gautiTipą = function(){
return "katinukas";
}
Katinukas.prototype.balsas = function(){
console.log('Miau!');
}

var katė = new Katinukas('Rainiukas');
katė.balsas(); //Atspausdins: Miau!
katė.prisistatymas(); //Atspausdins: Labas! Aš esu katinukas. Mano vardas: Rainiukas.

Kaip matome, kodas nėra labai gražus, nes naudojant Object.create neišeina naudoti JSONinio užrašymo stilius. Kaip ir pirmu atveju vėl prarandame numatytąjį Katinukas.prototype objektą, tad vėl reikia atstatyti konstruktorių. Visa kita savaime suprantama.

O dabar panaudosime Object.setPrototypeOf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Variantas naudojant Object.setPrototypeOf
var Šuo = function(vardas){
Object.getPrototypeOf(Object.getPrototypeOf(this)).constructor.call(this, vardas);
//Tas pats kaip: Gyvūnas.call(this, vardas);
}

Šuo.prototype = Object.setPrototypeOf({
constructor: Šuo,
gautiTipą: function(){
return "šuo";
},
balsas: function(){
console.log('Au!');
}
}, Gyvūnas.prototype);

var amsius = new Šuo('Amsiukas');
amsius.balsas(); //Atspausdins: Au!
amsius.prisistatymas(); //Atspausdins: Labas! Aš esu šuo. Mano vardas: Amsiukas.

Šis variantas atrodo daug gražiau. Bet pikti dėdės sako, kad veikia lėčiau.

Svarbiausia, kad dabar tu jau visiškai supranti kaip naudoti JavaScript’o objektinį modelį. Tikiuosi taip ir darysi, o jeigu iškils neaiškumų, visada gali pasikliauti dokumentacija.