Nástrahy (prototypové) dědičnosti v JavaScriptu
Tohle asi nebude pro zkušenější programátory nic nového. Spíše jde o občané (znovu)připomenutí těchto vlastností JavaScriptu.
Obsah
Ukázka
Následující ukázka je jen edukativní (neošetřuje dostatečně reálné situace) a demonstruje několik situací, které mohou být matoucí.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const pseudoArray= {
_status: "empty",
push(...prvek){
if(!this._array) this._array= [];
if(this._status==="empty") this._status= "not_empty";
return this._array.push(...prvek);
},
next(){ // !4
if(typeof this._index==="undefined") this._index= 0;
if(this._array.length===this._index) return null;
return this._array[this._index++];
},
get status(){
return this._status;
}
};
const $array= Object.create(pseudoArray);
$array.push("A");
console.log($array.next());
//="A"
console.log($array.status);
//="not_empty"
delete $array._status; // !1
console.log($array.status);
//="empty" !1
console.log($array.next());
//=null
$array.push("B");
console.log($array.next());
//="B"
Object.assign($array, // !3
{ _array: [ "C", "D" ], _index: 1 });
console.log($array.next());
//="D" !3
pseudoArray.push("?"); // !2
const $array_wtf= Object.create(pseudoArray);
$array_wtf.push("??");
console.log($array_wtf.next());
//="?" !2
console.log($array_wtf.next());
//="??" !2
Ukázky vybraných nástrah, týkajících se OOP, v JavaScriptu
pseudoArray
zde pracuje s internímy vlastnostmi _array
, _status
a _index
. Ukázka je úmyslně napsána takto1 aby vynikla prototypová dědičnost typická pro JavaScript.
Prototypová dědičnost
Jde o to, že v JavaScriptu nejsou třídy, ale objekty. Ty mají vlastní atributy a metody, další pak dědí z „rodiče” (klíč prototype
resp. __proto__
) [mdn_dedicnost][exploringjs_dedictnost]:
1
2
3
4
5
6
7
8
9
10
11
const rodic= { a: "A", b: "B" };
const potomek= Object.assign(Object.create(rodic),
{ b: "b", c: "c" });
console.log(potomek.a);
//="A" ⇐ JS se podiva na `potomek.a`,
// ten neexistuje, pokracuje tedy na `rodic.a`
console.log(potomek.b);
//="b" ⇐ JS se podiva na `potomek.b` a ten existuje,
// ignoruje tedy existenci `rodic.b`
console.log(potomek.c);
//="c" ⇐ analogicky jako predchozi
Ukázka prototypové dědičnosti
Nyní by tedy měli být jasné situace !1
a !2
. U !2
navíc nastala situace, kdy k _array
jen přistupujeme a tedy pokud jsme jej (asi omylem?) vytvořili na rodiči, náš kód se chová „podivně”!2 (viz skutečná implementace Array
popsaná např. v [kirupa_dedicnost]). Na to navazuje !3
, kdy v JavaScriptu neexistují3 privátní atributy, tj. lze je přepsat.
Vnitřní stav „třídy”
V OOP je časté, že si třída udržuje nějaký vnitřní stav (viz !4
). Důležité je, si toto chování uvědomit (číst/psát poctivě dokumentace!), aby nás toto nepřekvapilo, viz například [exploringjs_regexp]!
Míchání zápisů v JavaScriptu
Kromně dříve ukázaného zápisu1 se v JS setkáváme:
1
2
3
4
5
6
7
function Trida(name) {
this.name= name;
}
Trida.prototype.logName= function(){
console.log(this.name);
}
const instance= new Trida("jméno");
Klasické „třídy” v JS
1
2
3
4
5
6
7
8
9
class Trida{
constructor(name){
this.name= name;
}
logName(){
console.log(this.name);
}
}
const instance= new Trida("jméno");
Nové „třídy” v JS
1
2
3
4
5
6
7
const Trida= {
logName(){
console.log(this.name);
}
};
const instance= Object.assign(Object.create(Trida),
{ name: "jméno" });
Prototypový zápis alá „třída” v JS
Nový zápis je Syntaktický cukr starého zápisu s několika změnami (přesněji [mdn_classes]). Potíž může nastat, pokud bychom z nějakého důvodu potřebovali použít starý postup, ale dědit ze třídy již přepsané do nového zápisu, viz [Reflect.construct()].
Dále klíčové slovo super
ve skutečnosti pracuje s prototypovou dědičností – lze jej používat i u objektů viz Using super.prop
in object literals4.
Objekty jsou předávány referencí!
Častou potíží je, že v JS je skoro vše objekt, který se předává referecní a navíc díky dříve zmíněné dědičnosti se většinou neknonuje:
1
2
3
4
const a= { A: "text" };
const b= Object.assign(a, { A: "!!!" });
console.log(a.A);
//="!!!"
Object.assign
argumenty neklonuje!
… podobné je to pro Array.prototype.sort(), knihovnu Moment.js apod. Obecná rada je, na to myslet, číst/psát dokumentaci, počítat s možností klonování třídy (pokud dává smysl) [clone]. Dále být obecně opatrný (vědět co dělám) [copy], protože můžu zbytečne alokovat místo v paměti [memory].
Reference
- [mdn_dedictnost] Inheritance and the prototype chain
- [exploringjs_dedictnost] Prototype chains and classes
- [kirupa_dedicnost] JS Tip of the Day: Prototypes as Instance of Type
- [exploringjs_regexp] Pitfalls of `/g` and `/y`
- [mdn_classes] Classes
- [Reflect.construct()] Mixing class and function Constructors
- [clone] copy-constructor.js
- [copy] Kopírování objektů v JS
- [memory] 4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
-
Často se používá populárnější zápis („z Javy”):
class pseudoArray{ … }; const $array= new pseudoArray();
. Technicky tyto zápisy dělají něco jiného, ale pro potřeby tohoto článku můžeme říci, že se „chovají stejně”. Správný popis viz [mdn_dedicnost] a [mdn_classes]! ↩ ↩2 -
Šlo by opravit ručním vytvořením atributu na potomkovi:
Object.assign($array_wtf, { _array: [ "OK" ], _index: 0 });
. ↩ -
Viz podpora JavaScript classes: Private class fields. ↩
-
A closer look at super-references in JavaScript and ECMAScript 6 ↩