Акт 1: „Ядрото“ — Част 6 (Наследяване на прототип)
Това е продължение на поредицата за JavaScript, която започна „тук“.
JavaScript има вграден механизъм, за да има обект, който наследява полета и методи от други обекти. Въпреки че е препоръчително никога да не позволявате йерархиите на наследяване да станат много сложни, в някои сценарии това е полезен модел, който позволява да се избегне репликацията на код. В тази история ще разгледаме всички основни аспекти на наследяването на прототипи.
Ако не сте запознати с наследяването на прототипи, тази история може да се окаже твърде трудна. Може би е най-добре да го прочетете няколко пъти и да опитате да експериментирате с всички дадени примери.
Прототипът:
Всеки обект в JavaScript има връзка към обект, наречен прототип. Този прототип разширява дадения обект със свойства, които не притежава. Например, ако обектът obj
има прототип {a: 1}
и самият обект не съдържа свойство с име a
, тогава извикването на obj.a
ще намери стойността от a
в прототипа.
Тъй като самият прототип е обект, той също има прототип. Ако сега прототипът на obj
, нито самият обект obj
съдържат a
, свойството се търси в прототипа на прототипа.
Виждате ли, имотите се търсят във верига, подобна на конструкция. Последният се нарича прототипна верига. Тази верига обикновено завършва с прототипа на Object
. Така че obj.a
връща undefined
само в случай, че нито един прототип във веригата до Object
не съдържа стойност за свойството a
.
Това е механизмът на JavaScript за наследяване на свойства. Работи за всички видове свойства, нека да са методи или свойства.
В следващия раздел ще видим как да четем и променяме прототипа на обект.
Създаване на обекти:
JavaScript предоставя няколко начина за създаване на обекти. Най-лесният начин е просто като напишете
const obj = {}; // or with possible properties
Ако направите това, прототипът автоматично се настройва на Object
. Но ако искате да промените прототипа на обекта, можете да направите следното:
const myProto = { a: 1 }; const obj = Object.create(myProto); console.log(obj.a); // 1
Този метод създава празен обект и задава дадения аргумент като негов прототип. И както научихме преди, неговият прототип, тоест myProto
, отново е Object
.
Третият метод е важен за познаване и отваря възможността за деклариране на класове:
function MyClass(){ this.a = 1; this.b = 2; } const m = new MyClass(); console.log(m); // MyClass { a: 1, b: 2 }
Функцията MyClass
, написах я с главна буква, за да ни напомня за класовете, може да се използва за създаване на обект чрез извикването му с оператора new
. Този оператор извиква функцията и задава this
-контекста на резултантния обект. По този начин можем да поставим произволни свойства и методи върху новия обект. Функции като MyClass
често се наричат конструкторски функции, тъй като се държат като тези.
Прототипът на новосъздадения обект е обектът, който е посочен от MyClass.prototype
:
Не забравяйте, че в JavaScript функциите също са обекти и затова имат прототип. Прототипът на всички функции е Function
. Този обект Function
има свойство с име prototype
. Следователно MyClass
има наследеното поле prototype
, чиято стойност можем да зададем на всеки обект. Когато сега извикаме MyClass
от new MyClass()
, тогава прототипът на получения обект е зададен на обекта MyClass.prototype
.
Преди да прочетете горните предишни изречения още десет пъти, нека първо да разгледаме един пример:
function MyClass(){ this.a = 1; this.b = 2; } MyClass.prototype.c = 3; MyClass.prototype.d = 4; const m = new MyClass(); console.log(m); // MyClass { a: 1, b: 2 } console.log(m.c); // 3 console.log(m.d); // 4
Виждате ли, използвахме свойството с име prototype
на функционалния обект MyClass
, за да променим prototype
на m
. И така, какво общо има прототипът на MyClass
със стойността на неговото поле с име prototype
? Нищо!
Обикновено се поставят само методи в прототипа на конструктора ( MyClass.prototpye
), за да се избегнат трудни за откриване конфликти. Следващият пример показва такъв „приятен“ капан:
function MyClass(){ this.a = 1; this.b = 2; } MyClass.prototype.c = {d: 3}; const m1 = new MyClass(); const m2 = new MyClass(); console.log(m1.c.d); // 3 m1.c.d = 4; console.log(m2.c.d); // 4 console.log(MyClass.prototype.c.d); // 4
Тук промяна на свойство, постижимо от m1
, тоест m1.c.d = 4
, променя стойността не само във всички други екземпляри на класа, но дори и в самия прототип на конструктора. Ако имате код като този в голям проект... успех!
Има и друг специален случай:когато функция конструктор върне стойност, тази стойност се използва като нов обект. Този нов обект нямада получи прототип от конструктора:
function MyClass() { this.a = 1; this.b = 2; return {}; } MyClass.prototype.c = 3; const m = new MyClass(); console.log(m); // {} console.log(m.c); // undefined
Добро упражнение е да внедрите своя собствена версия на Object.create
:
function create(p) { const f = function () { }; f.prototype = p; return new f(); } const obj = create({ a: 1 }); console.log(obj.a);
Най-накрая има още един метод за промяна на прототипа на обект:
const a = { p: 3 }; Object.setPrototypeOf(a, { q: 4 }); console.log(a.q); // 4
Тук прототипът на a
е зададен на {q: 4}
. Този метод се различава от всички останали дотолкова, че прототипът се променя на вече съществуващ обект.
Понякога е необходимо изрично да се получи прототипът на обект. Това може да стане чрез метод, предоставен от Object
:
const a = { p: 3 }; Object.setPrototypeOf(a, { q: 4 }); console.log(Object.getPrototypeOf(a)); // { q: 4 }
Наследяване на прототип:
В предишния раздел видяхме как може да се използва функция като конструктор за обекти. По-специално, свойството „прототип“ на функцията се оказа полезно за дефиниране на прототипния обект за обекти, създадени от new
-оператора. Всичко това води до модел, наречен „наследяване на прототип“, който позволява наследяване на методи и полета от един обект на друг. Основният модел показва следния пример:
function A(p, q) { this.p = p; this.q = q; } A.prototype.someMethod = function () { console.log('hello from someMethod'); }; A.prototype.anotherMethod = function () { console.log('anotherMethod from A'); } function B(p, q) { A.call(this, p, q); <-- place properties from A onto 'this' this.anotherMethod = function () { <-- override method from A console.log('anotherMethod from B'); } } B.prototype = Object.create(A.prototype); <-- set prototype B.prototype.constructor = B; <-- correct the constructor prop.
Класът B
наследява полета и методи от A
. Типичен модел е всички методи да се поставят върху прототипа, но това не е строго необходимо. В B
методът A
се извиква чрез използване на контекста на B
:
A.call(this, p, q)
Това поставя всички полета и методи на A
в новосъздадения обект, посочен от this
. Имайте предвид обаче, че се копират само примитивни стойности! Въведените в обект стойности се предоставят на екземпляра на B
чрез препратка! Така промените в свойствата на такива обекти в инстанция на A
са видими в инстанции на B
.
За да имаме всички методи, дефинирани в прототипа на A
, налични в B
, задаваме прототипа на B
на обект, който има прототипа на A
като прототип:
B.prototype = Object.create(A.prototype)
Може би се чудите защо това допълнително Object.create
. Причината за това е да се защити прототипът на A
от промени, направени от B
. Ще разгледаме това в пример скоро. Преди да обясним последния ред:
Всяка функция има свойство constructor
в своето свойство с име prototype
Последното сочи към самата функция. Тъй като сме заменили прототипа на B
изцяло с прототипа на A
, трябва отново да коригираме това свойство на конструктора (в противен случай то ще сочи към B
, а не към A
).
Нека разгледаме някои случаи на горните класове, за да направим модела по-ясен:
const b = new B(1, 2); console.log(b.p); // 1 b.someMethod(); // 'hello from someMethod' (inherited!) b.anotherMethod(); // 'anotherMethod from B' (overridden!) const a = new A(3, 4); console.log(a.p); // 3 a.anotherMethod(); // 'anotherMethod from A' b.anotherMethod = function () { console.log('the new anotherMethod'); } a.anotherMethod(); // 'anotherMethod from A' (a's method remains!)
Първо, someMethod
е наследено от B
и може да бъде извикано от там. anotherMethod
е по-вълнуващо, тъй като е заменено от B
. Виждаме, че когато го извиквате чрез B
, той изпълнява правилната версия на този метод.
Във втора стъпка ние предефинираме метода anotherMethod
в B
:
b.anotherMethod = function () { console.log('the new anotherMethod'); }
Решаващо за разбиране е, че въпреки че b.anotherMethod
първоначално е наследен от A.prototype
, промяната му на b
не го променя на A.prototype
. Но още повече, ако би променил прототипа на b
, както например тук
Object.getPrototypeOf(b).anotherMethod = () => console.log('from B');
тогава това няма да повлияе изцяло на a
. Гарантирахме, че като поставихме свойството на прототипа на B
с create
:
B.prototype = Object.create(A.prototype)
Въпреки че наследяването изглежда на пръв поглед като много използваем модел, който помага да се избегне репликацията на код, трябва да се избягва използването му, когато е възможно, или да се ограничава възможно най-много. Причината за това е, че кодът, който използва този модел, бързо става труден за разбиране и поддръжка. Алтернативен модел е „Композиция“, който възлага функционалност в услуги, които могат да бъдат инжектирани, където е необходимо.
Операторът „instanceof“:
Операторът instanceof
проверява конструктора да се съдържа във веригата на прототипа. Не забравяйте, че в предишния раздел коригирахме полето prototype.constructor
на дъщерния клас A
. Една от причините за това е да се гарантира, че instanceof
-операторът все още работи. Нека разгледаме един пример:
function A(p, q) { this.p = p; this.q = q; } function B(p, q) { A.call(this, p, q); } B.prototype = Object.create(A.prototype); B.prototype.constructor = B; const b = new B(1, 2); const a = new A(3, 4); console.log(`b instanceof B: ${b instanceof B}`); // true console.log(`b instanceof A: ${b instanceof A}`); // true console.log(`b instanceof Object: ${b instanceof Object}`); // true console.log(`a instanceof Object: ${a instanceof B}`) // false
В този пример B
наследява от A
. Така че A
е в прототипната верига на B
. Това обяснява защо b instanceof B
и b instanceof A
. Тъй като веригите на прототипи обикновено завършват с Object
, b
също има конструктор Object
във веригата на прототипите. И накрая, очевидно a
няма B
в своята прототипна верига.
Това е всичко за сега и не забравяйте да оставите коментари за въпроси или грешки, които откриете. Нека видим отново в следващата история, ако желаете.
Благодаря за четенето!