Պատկերավոր ասած՝ օբյեկտները JavaScript-ի հիմքն են հանդիսանում, ուստի դրանց առավել խորը ուսումնասիրությունը թույլ կտա ձեզ գրել ավելի մաքուր և արդյունավետ կոդ: Հոդվածը առավելապես նախատեսված է ընթերցողների համար, ովքեր ունեն տարրական գիտելիքներ օբյեկտների վերաբերյալ:
Հոդվածի բովանդակություն - Օբյեկտներ միայն կարդալու համար
- Խորը և մակերեսային կլոնավորում
- Օբյեկտները որպես պրիմիտիվներ
Օբյեկտներ միայն կարդալու համար
Պատկերացնենք, ցանկանում ենք ստեղծել հորիզոնի ուղղությունների թվարկում.
const DIRECTIONS = {
NORTH: "north",
SOUTH: "south",
EAST: "east",
WEST: "west"
}
Այնուամենայնիվ, DIRECTIONS
-ում առկա հատկությունները/բանալիները կարող են փոփոխվել: Բացի այդ, այնտեղ կարող են ավելացվել նաև նորերը: Մեծածավալ կոդերի դեպքում սա կարող է վտանգավոր լինել, քանի որ մեծացնում է ի սկզբանե չփոխելու և միայն կարդալու համար նախատեսված արժեքների փոփոխության շանսը:
DIRECTIONS.NORTH = "south"; //ոչինչ չի խանգարում հետագայում նման գործողություն կատարելուն
Այս պարագայում կիրառելի է Object.freeze-ը.
Object.freeze(DIRECTIONS);
Object.freeze
կանխում է օբյեկտի մեջ նոր հատկությունների ավելացումը, գոյություն ունեցող հատկությունների փոփոխումը կամ ջնջումը:
Object.freeze(DIRECTIONS);
DIRECTIONS.NORTH = "south";
DIRECTIONS.UP = "up";
del DIRECTIONS.SOUTH;
Վերը բերված օրինակում DIRECTIONS.NORTH
-ը չի փոխվի, DIRECTIONS.UP
չի ավելացվի և DIRECTIONS.SOUTH
-ը չի ջնջվի: Ընդ որում, strict mode-ում (խիստ ռեժիմում) նման գործողությունները կառաջացնեն TypeError
, ոչ խիստ ռեժիմում դրանք բարզապես բաց կթողնվեն, առանց որևէ սխալի հաղորդագրության:
Object.freeze
-ը նաև սահմանափակում է օբյեկտի առանձին հատկությունների դեսկրիպտորներին (նկարագրիչներին) փոփոխելու հնարավորությունը: Հատկությունների դեսկրիպտորը կարելի է պատկերացնել որպես հատկության «կարգավորում» և բաղկացած է չորս դաշտից.
value
: հատկության փաստացի արժեքըenumerable
: որոշում է, թէ արդյոք տվյալ հատկությունը պետք է վերադարձվի օբյեկտի հավաքագրման(iterating, перебор) և թվարկման(enumerating, перечисление) ժամանակ:enumerable
կարգավորիչը հավասար էtrue
, ապա համապատասխան հատկությունը հասանելի կլինիfor _ in
ցիկլի ժամանակ և կներառվիObject.keys()
-ի մեջconfigurable
: որոշում է հատկության ջնջելու և իր դեսկրպտորի փոփոխման հնարավորությունըwritable
: որոշում է վերագրման միջոցով հատկության արժեքի փոփոխության հնարավորությունը
const obj = {'a': 1, 'b':2};
console.log(Object.getOwnPropertyDescriptors(obj));
/*
obj օբյեկտի հատկությունների դեսկրիպտորների արտաբերում
{
a: {value: 1, writable: true, enumerable: true, configurable: true},
b: {value: 2, writable: true, enumerable: true, configurable: true}
}
*/
Object.freeze
-ը կթողնի enumerable
անփոփոխ, փոխարենը կսահմանի օբյեկտի հատկությունների configurable
և writable
արժեքները false
. Արդյունքում այլևս անհնար կլինի խմբագրել օբյեկտի հատկությունների դեսկրիպտորը (չենք կարողանա փոփոխել writable
, enumerable
կամ configurable
արժեքները) և վերավերագրել հատկությունների արժեքները.
Հատուկ նյուանս: Object.freeze
-ը սահմանում է սահմանափակումներ միայն օբյեկտի վերին մակարդակի հատկությունների համար, այսինքն, սառեցնելով օբյեկտը, մենք այնուամենայնիվ հնարավորություն կունենանք փոփոխել նրա դուստր մակարդակի հատկությունները:
const o = {a: 0, b: {c: 5}};
Object.freeze(o);
o.b.c = 10; // կաշխատի առանց խնդիրների
Վերոնշյալ սահմանափակումները կտարածվեն միայն a
և b
հատկությունների վրա, բայց ոչ c
-ի վրա: Մենք կարող ենք կատարել ցանկացած գործողություն վերջինիս արժեքի հետ, նույնիսկ օբյեկտի սառեցումից (freeze
) հետո:
Ամբողջական սառեցման համար անհրաժեշտ է ռեկուրսիվ կանչել Object.freeze
-ը հատկությունների ժառանգների համար:
function deepFreeze(object) {
// վերցնում ենք օբյեկտի հատկությունների ցանկը
const propNames = Object.getOwnPropertyNames(object);
// Սառեցնում ենք դուստր հատկությունները նախքան ծնողին սառեցնելը
for (const name of propNames) {
const value = object[name];
if (value && typeof value === "object") {
// Ռեկուրսիվ կանչ
deepFreeze(value);
}
}
return Object.freeze(object);
}
const obj2 = {
internal: {
a: null
}
};
deepFreeze(obj2);
obj2.internal.a = 'anotherValue'; // սառեցված է
obj2.internal.a; // null
Մակերեսային և խորը կլոնավորում
Ի տարբերություն պրիմիտիվների, JavaScript-ում օբյեկտները փոխանցվում են հղումով(reference), որն իրենից նեկայացնում է ցուցիչ դեպի հիշողության մեջ օբյեկտի պահման հասցե:
const myPet = {
name: "Doggie",
type: "Dog"
}
myPet
-ը պահում է հղումը դեպի օբյեկտ, որին կապված է, այլ ոչ հենց բուն օբյեկտը:
const yourPet = myPet;
yourPet.name = "Cattie";
console.log(myPet);
/*
myPet-ը կունենա հետևյալ տեսքը.
{ name: 'Cattie', type: 'Dog' }
*/
Նման վերագրումը հանգեցնում է myPet
օբյեկտի հղման պատճենումը որպես yourPet
-ի հղում: Արդյունքում yourPet
և myPet
օբյեկտները հղում են անում դեպի միևնույն հասցե, հետևաբար նաև՝ դեպի նույն օբյեկտ: Այսինքն, yourPet
օբյեկտի հատկությունների փոփոխություն կարտացոլվի myPet
-ում, և հակառակը:
Երբ երկու և ավելի փոփոխականներ հղում են անում դեպի նույն օբյեկտ, նրանց անվանում են մակերեսային պատճեներ կամ կլոններ: Ինչպես երևում է վերը նշված օրինակում, նրանց ստեղծման եղանակներից մեկը վերագրումն է:
const obj = {"a": 0};
const anotherObj = obj; // մակերեսային կլոն
Մակերեսային կլոնի ստուգում կարելի է իրականացնել Object.is
-ի միջոցով: Object.is
-ը ցույց է տալիս, թէ արդյոք փոփոխականները միևնույն հղումն ունեն և միևնույն օբյեկտին են հղվում:
const obj = {"a": 0};
const anotherObj = obj;
Object.is(obj, anotherObj); // վերադարձնում է true
Կարծես թե պարզ է! Հապա ինչո՞ւ է ստորև բերված կոդի օրինակը վերադարձնում false
const histo1 = {"a": 0};
const histo2 = {"a": 0};
Object.is(histo1, histo2); // վերադարձնում է false
Եթե գուշակեցիք, ապա շնորհավորում եմ Ձեզ: Իրականում, չնայած նրան, որ histo1
և histo2
օբյեկտների պարունակությունը նույնն է, դա չի նշանակում, որ նրանք հիշողության մեջ միևնույն հասցեն ունեն: Յուրաքանչյուր անգամ, երբ մենք փոփոխականին վերագրում ենք օբյեկտ՝ նկարագրելով կառուցվածքը (չենք վերագրում գոյություն ունեցող օբյեկտ պարունակող փոփոխական), հիշողության մեջ հիշվում է օբյեկտը, որպես լիովին նոր էություն, և յուրաքանչյուր անգամ այդ նոր տեղն ուղղող կրկին նոր հղումը վերագրվում է փոփոխականին: Հենց այս իսկ պատճառով վերոնշյալ օրինակում histo1
և histo2
օբյեկտները չեն հանդիսանում մակերեսային կլոններ, որովհետև նրանց հղումները ցույց են տալիս տարբեր օբյեկտներ, որոնք ընդամենը նույն պարունակությունն ունեն, և հանդիսանում են խորը կլոններ.
Կա նման պատճեններ ստանալու երկու եղանակ.
Խորը կլոնավորում JSON-ի միջոցով
const me = {"name": "Martin"};
const you = JSON.parse(JSON.stringify(me));
Այստեղ գաղափարը կայանում է նրանում, որ նախ անհրաժեշտ կլինի փոխակերպել օբյեկտը տողի JSON.stringify
-ի միջոցով, ապա վերլուծել/փարսել(parse) տողը JSON.parse
-ի միջոցով, և ստանալ նոր օբյեկտ: Հիմնական սահմանափակումն այս պարագայում այն է, որ օբյեկտները, որոնց հատկությունները ֆունկցիաներ են, անհրաժեշտ կերպով չեն պատճենվի, քանզի JSON.Stringify
չի կարող կոդավորել ֆունկցիաները:
Խորը կլոնավորում Lodash-ի միջոցով
// in ECMAScript: import * as cloneDeep from "lodash.clonedeep"
const cloneDeep = require("lodash.clonedeep");
const obj1 = {a: 1, b: {c: 4}};
// obj2-ը հանդիսանում է obj1-ի խորը կլոն
const obj2 = cloneDeep(obj1);
console.log(obj2)
//obj2: { a: 1, b: { c: 4 } }
Object.is(obj1, obj2) // returns false
Կարելի է ներմուծվել cloneDeep
-ը Lodash-ից: Սա մեթոդ է, որը ռեկուրսիվորեն կլոնավորում է իրեն փոխանցված օբյեկտի հատկությունները: Հետ վերադարձված օբյեկտը կլինի խորը պատճեն: Հիմնական թերությունը արտաքին գրադարան տեղադրելու անհրաժեշտությունն է, ինչը մեծացնում է հավելվածի ընդհանուր չափը:
Այսպիսով, այն իրավիճակներում, երբ օբյեկտի արժեքները համատեղելի են JSON-ի հետ, ավելի հեշտ կլինի օգտագործել JSON.stringify
-ն: Հակառակ դեպքում lodash.cloneDeep
ավելի արդյունավետ կլինի:
Օբյեկտները որպես պրիմիտիվներ
Չնայած նրան, որ օբյեկտներին կցված փոփոխականները պահում են օբյեկտի հղումը, այնուամենայնիվ Object.prototype.valueOf-ի վերասահմանման միջոցով կարելի է ստանալ օբյեկտի պրիմիտիվ արժեքը:
Object.prototype.valueOf
ֆունկցիան վերադարնում է օբյեկտի պրիմիտիվ արժեքը: Լռելյայն այն վերադարձնում է հենց օբյեկը, բայց կարելի է նրան վերասահմանել վերադարձնելու բոլորովին այլ բան:
const result = 1 + new Number(14);
console.log(result);
// result: 15
Number
հանդիսանում է թվային օբյեկտ-փաթեթավորում: Եվ այստեղ հետաքրքիրն այն է, որ օբյեկտին(new Number (14)
) գումարելով 1
պրիմիտիվը, մենք ստանում ենք ճիշտ արդյունք՝ 15
:
new Number(14)
-ին 1
գումարելուց JavaScript-ը ավտոմատ կերպով փոխակերպում էnew Number(14)
-ը իրեն համապատասխանող պրիմիտիվ արժեքին՝ 14
. Այս արժեքը ստացվում է Number.prototype.valueOf()
-ի շնորհիվ, որը օգտագործման համար փոխակերպում է իրականացնում և վերադարձնում այն թվային արժեքը, որն իր մեջ պահում է Number
-ը:
Եվս մեկ օրինակ. ենթադրենք ունենք StringBuilder
օբյեկտը, որը նախատեսել ենք տողերի զուգակցման (concating) համար և ցանկանում ենք օգտագործել հետևյալ կերպ.
function StringBuilder(){
this.strings = [];
this.add = (s) => {
this.strings.push(s)
}
this.concat = () => {
return this.strings.join("");
}
}
const builder = new StringBuilder();
builder.add("Hi");
builder.add(" Bye");
console.log(builder.concat()) // "Hi Bye"
const builder = new StringBuilder();
builder.add("B");
builder.add("C");
const result = "A" + builder;
//result: մենք ակնկալում ենք "ABC", բայց ստանում ենք "A[object Object]"
Որպեսզի result
-ը ստանանք որպես "ABC"
, կարող ենք վերասահմանենք StringBuilder.prototype.valueOf
-ը, կանչելով builder.strings
զուգակցման մեթոդը:
StringBuilder.prototype.valueOf = function() {
return this.concat();
}
StringBuilder
օբյեկտի յուրաքանչյուր պրիմիտիվի փոխարկման ժամանակ ստացված պրիմիտիվը իրար կմիացնի օբյեկտին ավելացված բոլոր տողերը:
Կարելի է վերասահմանել Object.prototype.valueOf
-ը, որպեսզի օբյեկտներին տրամադրել որոշակի պրիմիտիվ արժեքներ, երբ դրանք վերածվում են պրիմիտիվների:
Սկզբնաղբյուր՝ A Deep Dive Into JavaScript Objects