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.