Կոնստրուկտոր-ֆունկցիաներն ու օբյեկտների ստեղծումը new օպերատորի օգնությամբ: Ի՞նչ դեր է խաղում this բանալի բառը կոնստրուկտոր-ֆունկցիաների մեջ:
Ինչպես գիտենք JavaScript-ում և մնացած շատ այլ ծրագրավորման լեզուներում օգտագործվող օբյեկտ հասկացությունը նման է իրական, ֆիզիկական աշխարհի օբյեկտներին։ Օրինակ բաժակն ունի ձև, գույն, քաշ, պատրաստված է ինչ-որ նյութից։ Նույն կերպ JavaScript օբյեկտներն ունեն հատկություններ, որոնք բնութագրում են տվյալ օբյեկտը։ Հատկությունն ունի անուն, որին երբեմն նաև անվանում են բանալի, և արժեք։ Հատկության անունը կարող է լինել միայն String և Symbol տիպի, եթե մենք փորձենք որպես հատկության անուն օգտագործել ուրիշ տիպ, ապա այն անուղղակիորեն կվերածվի String տիպի։ Հատկության արժեքը կարող է լինել ցանկացած տիպի:
JavaScript-ում կան մի շարք ներդրված օբյեկտներ։ Մենք նույնպես կարող ենք ստեղծել մեր «սեփական» օբյեկտները։ Նոր օբյեկտների ստեղծման շատ տարածված ու հարմար եղանակ է լիտերալ սինթաքսի օգտագործումը, որի դեպքում մենք ստեղծվելիք օբյեկտի հատկություններն ու մեթոդներն ուղղակի ներառում ենք ձևավոր փակագծերի մեջ։ Օրինակ ենթադրենք պետք է ստեղծել user անունով օբյեկտ, որը հատկությունների միջոցով կնկարագրի user-ի անունն ու տարիքը, և կունենա մեթոդ՝ որի կանչը alert կանի ողջույնի խոսք՝ ուղղված այդ user-ին։
const user = {
name: "John",
age: 25,
sayHello: function () {
alert("Hello " + this.name);
},
};
console.log(user.age); // 25
user.sayHello(); // Hello John
Շատ հաճախ անհրաժեշտ է լինում ստեղծել ոչ թե մեկ, այլ մի քանի միանման օբյեկտներ։ Օրինակ կարող է լինել ինչ-որ օնլայն ֆորում, որտեղ մեկնաբանություններ թողելու համար օգտատերերը պետք է գրանցվեն։ Յուրաքանչյուր օգտատիրոջ համար ստեղծվում է առանձին օբյեկտ, որի մեջ ինչպես վերևի օրինակում էր, պետք է պահվի ինֆորմացիա տվյալ օգտատիրոջ անվան և տարիքի մասին, ինչպես նաև պետք է լինի մեթոդ, որը կողջունի նրան։ Մենք կարող ենք ստեղծել ևս մեկ օբյեկտ, օրինակ user2, որի մեջ կունենաք վերոհիշյալ հատկություններն ու մեթոդը։
const user2 = {
name: "Jane",
age: 22,
sayHello: function () {
alert("Hello " + this.name);
},
};
console.log(user2.age); // 22
user2.sayHello(); // Hello Jane
Իսկ եթե անհրաժեշտ լինի ստեղծել հարյուրավոր կամ հազարավոր օգտատերերին նկարագրող նմանատիպ օբյեկտնե՞ր։ Բնականաբար առանձին առանձին ստեղծել այդ օբյեկտները օպտիմալ լուծում չէ։ Մենք կարող ենք հեշտացնել մեր աշխատանքը, եթե ստեղծենք ֆունկցիա, որն ամեն անգամ որպես արգումենտ ստանալով օգտատիրոջ անունն ու տարիքը՝ մեզ կվերադարձնի նոր օբյեկտ։ Ինչպես գիտենք ֆունկցիաները միշտ ինչ-որ բան վերադարձնում են։ Եթե մենք ուղղակիորեն չենք տալիս, թե ֆունկցիայի կանչն ինչ վերադարձնի, ապա այն վերադարձնում է undefined: Մենք հեշտությամբ կարող ենք ստեղծել ֆունկցիա, որը կվերադարձնի օբյեկտ։
function createUser(name, age) {
return {
name: name,
age: age,
sayHello: function () {
alert("Hello " + this.name);
},
};
}
const user1 = createUser("John", 25);
const user2 = createUser("Jane", 22);
createUser ֆունկցիան ամեն կանչի ընթացքում վերադարձնում է մեր տված արգումենտներով ստեղծված նոր օբյեկտ, մի քանի անգամ կրճատելով գրվելիք կոդի ծավալը և պարզեցնելով կառուցվածքը։ Այս տիպի ֆունկցիաները կոչվում են factory function-ներ, factory նշանակում է գործարան, և այն ասես օբյեկտների ստեղծման «գործարան» լինի։ Ի դեպ եթե հատկության և պարամետրի անունները համընկնում են, մենք կարող ենք գրել ավելի համառոտ ձևով՝
function createUser(name, age) {
return {
name,
age,
sayHello: function () {
alert("Hello " + this.name);
},
};
}
Մենք կարող ենք օգտագործել նաև միանման օբյեկտների ստեղծման մեկ այլ եղանակ՝ կոնստրուկտոր-ֆունկցիաների և new օպերատորի օգնությամբ։
function User(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
alert("Hello " + this.name);
};
}
const user1 = new User("John", 25);
const user2 = new User("Jane", 22);
Կոնստրուկտոր ֆունկցիան սովորական ֆունկցիա է, այն, որ ընդունված է անվանումը գրել մեծատառով, ուղղակի պայմանավորվածություն է, լեզվական ստանդարտը նման պահանջներ չի ներկայացնում։ Եթե ֆունկցիայի անվանումը գրվեր փոքրատառ, նույն ձև այն աշխատելու էր, սակայն իհարկե ցանկալի է հետևել այդ պայմանավորվածություններին, որպեսզի կոդը արագ աչքի անցկացնելուց միանգամից պարզ լինի թե ինչ է արվում։
new օպերատորի հետ կոնստրուկտոր ֆունկցիան կանչելուց այն վերադարձնում է օբյեկտ, որի հատկությունները նկարագրված են կոնստրուկտոր ֆունկցիայի մեջ։ Եթե մենք կոնստրուկտոր ֆունկցիան կանչենք առանց new օպերատորի, ապա ժամանակակից խիստ ռեժիմում մենք կստանանք սխալ։ Օրինակ՝
const user1 = User("John", 25);
console.log(user1); // Uncaught TypeError
Պատճառն այն է, որ երբ մենք կոնստրուկտորը կանչում ենք new օպերատորի օգնությամբ, ապա ֆունկցիայի մեջ ստեղծվում է դատարկ օբյեկտ՝ {}, որը վերագրվում է որպես this-ին արժեք։ Ապա կատարվում են հատկությունների վերագրումները՝ օրինակ this.name = name, այսինքն այդ դատարկ օբյեկտի մեջ ստեղծվում է name անունով հատկություն, և դրան որպես արժեք վերագրվում է կոնստրուկտոր ֆունկցիայի name պարամետրով եկած արժեքը։ Երբ բոլոր հատկությունների վերագրումներն ավարտվում են՝ ֆունկցիան ավտոմատ վերադարձնում է this-ը, այսինքն նորաստեղծ օբյեկտը։ Այդ տակից կատարվող պրոցեսները կարելի է նկարագրել այսպես․
function User(name, age) {
// this = {}
this.name = name;
this.age = age;
this.sayHello = function () {
alert("Hello " + this.name);
};
// return this
}
Երբ մենք կոնստրուկտոր ֆունկցիան կանչում ենք առանց new օպերատորի, ապա տակից կատարվող այդ պրոցեսները տեղի չեն ունենում, հետևաբար՝ ֆունկցիայի մեջ գտնվող this-ի արժեքը լինում է undefined: Փորձելով կատարել undefined.name = name վերագրումը, բնականաբար ստանում ենք սխալ, undefined-ը պրիմիտիվ տիպ է, չենք կարող նրա հետ աշխատել ինչպես օբյեկտների հետ՝ ստեղծել հատկություններ և ինչ-որ արժեքներ վերագրել։
Քանի-որ ինչպես ասվեց կոնստրուկտոր ֆունկցիաները սովորական ֆունկցիաներ են, ոչինչ չի խանգարում մեզ return հրահանգի օգնությամբ ուղղակիորեն նշել թե ֆունկցիան ինչ պետք է վերադարձնի։ Սակայն այդպես անել չի կարելի, new օպերատորի հետ կոնստրուկտոր ֆունկցիան կանչելիս, այն մեզ միշտ կվերադարձնի this-ը՝ տվյալ պարագայում նորաստեղծ օբյեկտը։ Եթե մենք նշենք, որ կոնստրուկտոր ֆունկցիան պետք է վերադարձնի պրիմիտիվ տիպին պատկանող ինչ-որ արժեք, ապա այն կանտեսվի, և այնուամենայնիվ ֆունկցիան կվերադարձնի նորաստեղծ օբյեկտը։ Սակայն եթե return հրահանգի օգնությամբ պահանջենք վերադարձնել ինչ-որ ուրիշ օբյեկտ, ֆունկցիան կվերադարձնի այդ ուրիշ օբյեկտը, և փաստացի լրիվ անիմաստ գործողություն կանենք դրանով։
Նաև նշեմ, որ մենք ունենք հնարավորություն ստուգելու թե արդյոք կոնստրուկտոր ֆունկցիան կանչվել է new օպերատորի օգնությամբ թե ոչ։ Ընդամենը պետք է ֆունկցիայի մարմնում ամեն կանչի ժամանակ ստուգվի new.target հատկությունը։ Եթե նրա արժեքը հենց ֆունկցիան է, ապա այն կանչվել է new օպերատորի օգնությամբ, հակառակ դեպքում այդ հատկության արժեքը կլինի undefined: Մենք կարող ենք օգտագործել այս հատկությունը, ստեղծելով կոնստրուկտոր ֆունկցիա, որը կաշխատի թե new օպերատորով կանչի ժամանակ, թե առանց դրա։
function User(name, age) {
if (!new.target) {
return new User(name, age);
}
this.name = name;
this.age = age;
this.sayHello = function () {
alert("Hello " + this.name);
};
}
const user1 = User("John", 25);
console.log(user1.name); // John
Այժմ եթե խիստ ռեժիմում կանչենք այս կոնստրուկտոր ֆունկցիան առանց new օպերատորի՝ նույն արդյունքը կստանանք, ինչ new օպերատորով կանչելու ժամանակ։ Պատճառն այն է, որ մենք կոնստրուկտոր ֆունկցիայի մարմնում new.target հատկության շնորհիվ կարողանում ենք ֆիքսել, որ ֆունկցիան կանչվել է առանց new օպերատորի, հիշեցնեմ՝ դա այն դեպքն է, երբ այդ հատկության արժեքը լինում է undefined: Այս դեպքում մենք ուղղակի կոնստրուկտոր ֆունկցիայի կանչը վերահասցեավորում ենք new օպերատորի օգնությամբ կանչի։
Քանի-որ sayHello մեթոդը User կոնստրուկտոր ֆունկցիայի օգնությամբ ստեղծված բոլոր օբյեկտների մեջ նույնն է, և նրա կատարած գործառույթը ուղղակի կախված է this-ի արժեքից՝ այսինքն թե որ օբյեկտի մեջից է այն կանչվում, ապա կոդը էլ ավելի կոմպակտ դարձնելու համար մենք կարող ենք այդ մեթոդը տանել User կոնստրուկտոր ֆունկցիայի պրոտոտիպի մեջ։
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.sayHello = function () {
alert("hello " + this.name);
};
const user1 = new User("John", 25);
const user2 = new User("Jane", 22);
console.log(user1); // {name: "John", age: 25}
console.log(user2); // {name: "Jane", age: 22}
user1.sayHello(); // hello John
user2.sayHello(); // hello Jane
Այժմ մենք կարող ենք ստեղծել տասնյակ, հարյուրավոր և հազարավոր միանման օբյեկտներ՝ User կոնստրուկտոր ֆունկցիայի օգնությամբ, և այդ օբյեկտներն իրենց մեջ կունենան միանման հատկությունները՝ տարբեր արժեքներով։ Իսկ բոլորի համար ընդհանուր մեթոդը կլինի User կոնստրուկտոր ֆունկցիայի պրոտոտիպում՝ ազատելով մեզ յուրաքանչյուր օբյեկտի մեջ դրա ունենալու անհրաժեշտությունից: