Przyjrzymy się bliżej cukrowi składniowemu, jakim są klasy w JavaScript i postaramy się zrozumieć jak się one mają do starszych elementów języka.

Ok, napiszmy taką klasę:

class Person {
    constructor(name, age){
        this.age = age;
        this.name = name;
    }
}

let p1 = new Person("John", 23);
console.log(p1);

Jak ona się ma do funkcji konstruującej? Cóż, funkcję konstruującą można wywołać bez new:

function Person(name, age){

  if(!new.target)
      return new Person(name, age);
    
  this.name = name;
  this.age = age;
}

let jim =  Person("Jim", 20);

console.log(jim);
//Object { name: "Jim", age: 20 }

W klasach już się tak nie pobawimy. Nie ma new – jest error.

Ok, zobaczmy jak się mają metody klasy do tego wszystkiego, czyli metod obiektów:

class Person {
    constructor(name, age){
        this.age = age;
        this.name = name;
    }

    sayHi(){
        console.log("HI");
    }
}

let p1 = new Person("John", 23);
console.log(p1);

console.log(Person.sayHi); //undefined
console.log(Person.prototype.sayHi); //function sayHi
p1.sayHi(); //HI

Jak widać metody Person dopisywane są do prototypu Person, nie zaś do obiektu.

Ok, a metody statyczne?

class Person {
    constructor(name, age){
        this.age = age;
        this.name = name;
    }

    sayHi(){
        console.log("HI");
    }

    static staticHi(){
        console.log("HI");
    }
}

let p1 = new Person("John", 23);

console.log(Person.staticHi); //function staticHi
console.log(Person.prototype.staticHi); //undefined

Person.staticHi(); //HI

console.log(typeof p1); //object

Ok, odrobina magii jak możemy zrobić mixin obiektowi:

let obj1 = function() {};

let mixin1 = {
    helloWorld() {
        console.log("hw");
    },
    hello2(){
        console.log("hello 2");
    }
}

Object.assign(obj1, mixin1);

console.log(obj1.helloWorld); //function...
console.log(obj1.hello2); //function...

obj1.helloWorld(); //hw
obj1.hello2(); //hello2

Idąc tym tokiem myślenia możemy napisać coś takiego:

let myMixin = {
    mixinMethod(){
        console.log("mixin!");
    }
}

function mixin(...mixins) {

    const fn = function() {};

    Object.assign(fn.prototype, ...mixins);

    return fn;

}

class ClsWithMixins extends mixin(myMixin) {
    constructor(){
        super();
        this.name = "John";
    }
}

let john1 = new ClsWithMixins();
john1.mixinMethod();
//mixin!

Czyli tworzymy funkcję, na jej prototypie dodajemy mixiny (obiekty z metodami/metodą), zwracamy tę funkcję. Z tej funkcji dziedziczy nasza klasa, która tworzy nowe obiekty uzbrojone już w metody z mixinów.

Jak chcemy możemy dalej to drążyć:

let john1 = new ClsWithMixins();
john1.mixinMethod();
//mixin!

console.log(john1.__proto__);
//Object
// Constructor - ClsWithMixins

console.log(john1.__proto__.constructor); //class ClsWithMixins

console.log(typeof john1.__proto__.constructor); //function

console.log(john1.__proto__.constructor.__proto__); //function fn (ma mixinMethod w prototypie)

Jak widać:

  • Prototyp to Object, konstruktor to ClsWithMixins
  • Próbując wypisywać __proto__.constructor dostajemy „class”, ale wiemy, że klasy w JS nie istnieją
  • Próbując wypisać typ __proto__.constructor dostajemy function. To jest funkcja
  • Ta funkcja ma swój prototyp, function fn, który ma mixinMethod w prototypie

Nie są to najłatwiejsze zagadnienia, ale można się przez to spokojnie przegryźć i powoli wskakiwać na wyższy poziom rozumienia JavaScript…