Postaramy się rozwiązać jedną z większych zagadek – także dla mnie – w JS, czyli czym są prototypy. Na przykładzie funkcji mixin, którą już omawialiśmy. Do dzieła.

Ok, tak wygląda funkcja mixin:

function mixin(...mixins) {

    const fn = function() {};

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

    return fn;

}

Pamiętamy jak jej używać, ale może najpierw przypomnę:

function mixin(...mixins) {

    const fn = function() {};

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

    return fn;

}
let iterator = {

    each(fn) {
        console.log("fake iterator");
        return this;
    }

};

class Person extends mixin(iterator) {
    constructor(name, age){
        super();
        this.name = name;
        this.age = age;
    }
}

let john = new Person("John", 30);
console.log(john);

Oczywiście takich obiektów może być tam ile chcemy, na tym ten mixin polega. Niestety tak de facto to nie jest to trait jak w PHP, my dziedziczymy z jednej klasy (dlatego super jest konieczne) po prostu ta klasa to jest merge różnych funkcji nazwanych mixinem i my z niego dziedziczymy.

Bardziej ciekawe jest jak to się dzieje. Zauważmy, że różne obiekty mają coś takiego jak prototype:

Array.prototype
Object.prototype
Function.prototype

Ale już utworzone przez nich obiekty tego nie mają:

{}.prototype //nie ma
[].prototype //nie ma
"string".prototype //nie ma

Zauważmy też, że każda klasa jest funkcją:

let _cls = class Cls {}
console.log(typeof _cls);
//function

Zobaczmy też na taką drobną różnicę między funkcją konstuującą a zwracającą literał obiektu:

function Worker(name, salary){
    this.name = name;
    this.salary = salary;
}

function worker2(name, salary){
    return {
        name: name,
        salary: salary
    }
}

let worker1 = new Worker("Jim", 2000);
console.log(typeof worker1);
//object
console.log(Object.getPrototypeOf(worker1).constructor.name);
//Worker

let worker_2 = worker2("Jim2", 2000);
console.log(typeof worker_2);
//object
console.log(Object.getPrototypeOf(worker_2).constructor.name);
//object

Jedna i druga daje typ obiekt, ale w przypadku pierwszej ten obiekt ma jakiś swój prototyp inny niż Object. Jest coś pomiędzy nim a object i to coś może mieć własne metody, pola i tak dalej.

Oczywiście wiemy jak wstrzykiwać rzeczy do tego prototypu:

function Worker(name, salary){
    this.name = name;
    this.salary = salary;
}

Worker.prototype.sayhi = function(){
    return `Hi my name is ${this.name}`;
}

I to nam robi ten mixin i zwraca funkcję (niby klasę) z której nasza funkcja (niby klasa) dziedziczy (niby korzysta z mixina). Dla przypomnienia:

  • Object.create(proto) – tworzy obiekt z prototypem jako podanym argumentem
  • Object.assign(target, …sources) – kopiuje properties ze źródeł do targetu

Teraz ja jak na to patrzę to mam jedno zasadnicze pytanie:

function mixin(...mixins) {

    const fn = function() {};

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

    return fn;

}

Czyli czy przypadkiem to coś nie dodaje do Function.prototype tych metod? Ale okazuje się, że nie. Funkcje w JS są inne.

Zobaczmy na to:

const arr = [];
arr.prototype //nie ma
const obj = {};
obj.prototype //nie ma
const fn = function() {};
fn.prototype //jest
fn.prototype.constructor.name //fn

A zatem one mają własny scope i Object.assign tam przypisuje do fn, nie do Function.prototype. Zresztą Function.prototype to zostało użyte do utworzenia fn, zaś fn.prototype zostanie użyte do utworzenia klasy, z której dziedziczymy mixiny…

Swoją drogą warto wspomnieć, że jak tworzymy literał obiektu to korzystamy z funkcji, która jest w Object.prototype.constructor. Do tworzenia obiektów używamy funkcji.

Nie każdy obiekt jednak jest tworzony przez Obj.prototype.constructor. A właściwość prototype to właściwość funkcji. Tylko funkcje to mają. I pokazuje on na obiekt wyższy w hierarchii prototypowej, z którego funkcji constructor ten obiekt dziedziczy.

Swoją drogą tam będzie pętla nieskończona, bo w końcu dojdziemy do Object. Prototype to obiekt z konstruktorem, tak? To sobie zobaczmy, że w końcu dojdziemy do constructor.name object. Ale sam prototype to też obiekt.

Mamy też właściwość __proto__ (wspierane przez przeglądarki ale nie jest to żaden standard) jak również funkcję Object.getPrototypeOf(obj), która dla konkretnego obiektu pokaże jego prototyp. Tam w końcu dojdziemy do null, bo prototypem Object jest null, object nie ma prototypu.