Przyglądamy się bliżej OOP w JS – między innymi tworzymy działające dziedziczenie w funkcjach-konstruktorach (przed ES6), aby lepiej zrozumieć, jak OOP w JS działa.
Ok, zobaczmy na coś takiego:
function Person123() {};
Teraz zastanówmy się jak to działa:
function Person123() {};
console.log(Person123.prototype);
// Object { … }
// constructor: function Person123()
// <prototype>: Object { … }
console.log(typeof Person123.prototype);
// Object
console.log(Person123.prototype.constructor);
//function Person123()
console.log(typeof Person123.prototype.constructor);
//function
Jak widać każda taka funkcja ma swój prototyp. Mamy tam konstruktor i prototyp. Typ prorotypu to object, typ konstruktora to function.
Ok, napiszmy sobie prostą funkcję-konstruktor:
function Person(name, age){
this.name = name;
this.age = age;
}
let john = new Person("john", 32);
console.log(john);
//Object { name: "john", age: 32 }
Person to funkcja. Jak każda funkcja posiada metodę call, której możemy przypisać inny this, pożyczyć za zasadzie takiego jednorazowego partial application:
function Worker(name, age, dept){
Person.call(this, name, age);
this.dept = dept;
}
let jane = new Worker("Jane", 22, "HR");
console.log(jane);
//Object { name: "Jane", age: 22, dept: "HR" }
Dlaczego nie ma new? Sami sobie odpowiedzmy. Co robi Person? Do danego obiektu przypisuje properties (name, age…). Person to funkcja, new Person to obiekt.
Person tutaj potrzebuje wskazania co jest thisem oraz jakie argumenty mają być i robi swoje. Możemy jeszcze to wziąć po lupę:
function Person(name, age){
this.name = name;
this.age = age;
}
let john = new Person("john", 32);
console.log(john);
//Object { name: "john", age: 32 }
let jim = {};
Person.call(jim, "jim", 22);
console.log(jim);
//Object { name: "jim", age: 22 }
Jak widać new nie jest potrzebne, gdy sami wcześniej ręcznie utworzymy obiekt i zawołamy konstruktor w jego kontekście. Teraz powinno stać się bardziej jasne co właściwie robi ta funkcja-konstruktor, a czego nie robi i czym jest new.
Podobne działanie możemy uzyskać z Object.assign:
let unknown = Object.assign({}, {name: "Unknown", age: 22});
console.log(unknown);
//Object { name: "Unknown", age: 22 }
Object.assign może też dopisywać do już istniejących obiektów i zwracać takiego merga:
let jim = {};
Person.call(jim, "jim", 22);
console.log(jim);
//Object { name: "jim", age: 22 }
let jim2 = Object.assign(jim, {job: "coder"});
console.log(jim2);
//Object { name: "jim", age: 22, job: "coder" }
console.log(jim);
//Object { name: "jim", age: 22, job: "coder" }
Tylko jak widać assign modyfikuje oryginał jeżeli to nie pusty obiekt.
W pewnym uproszczeniu to co robi funkcja Person razem z operatorem new i nowym obiektem możemy sobie w ten sposób odwzorować:
function Person(name, age){
if(!new.target){
function PersonConstructor(name, age){
this.name = name;
this.age = age;
}
let newObj = {};
PersonConstructor.call(newObj, name, age);
return newObj;
} else {
this.name = name;
this.age = age;
}
}
let jim = Person("Jim", 20);
console.log(jim);
//Object { name: "Jim", age: 20 }
let john = new Person("John", 20);
console.log(john);
//Object { name: "John", age: 20 }
Czyli:
- mamy funkcję, która jest konstruktorem i zajmuje się przypisywaniem rzeczy do this
- tworzymy nowy obiekt
- wołamy funkcję-konstruktor w kontekście nowo utworzonego obiektu
- zwracamy ten nowo utworzony obiekt z przypisanymi do niego właściwościami
To wszystko dzieje się pod spodem, gdy robimy:
function Person(name, age){
this.name = name;
this.age = age;
}
let john = new Person("John", 20);
console.log(john);
//Object { name: "John", age: 20 }