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
  1. Ukázka
    1. Prototypová dědičnost
    2. Vnitřní stav „třídy”
  2. Míchání zápisů v JavaScriptu
  3. Objekty jsou předávány referencí!
  4. Reference

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!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”

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

  1. [mdn_dedictnost] Inheritance and the prototype chain
  2. [exploringjs_dedictnost] Prototype chains and classes
  3. [kirupa_dedicnost] JS Tip of the Day: Prototypes as Instance of Type
  4. [exploringjs_regexp] Pitfalls of `/g` and `/y`
  5. [mdn_classes] Classes
  6. [Reflect.construct()] Mixing class and function Constructors
  7. [clone] copy-constructor.js
  8. [copy] Kopírování objektů v JS
  9. [memory] 4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
  1. Č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][mdn_classes]!  2

  2. Šlo by opravit ručním vytvořením atributu na potomkovi: Object.assign($array_wtf, { _array: [ "OK" ], _index: 0 });

  3. Viz podpora JavaScript classes: Private class fields

  4. A closer look at super-references in JavaScript and ECMAScript 6