Հետաքրքիր մանրամասներ JavaScript օբյեկտների մասին

հետաքրքիր մանրամասներ js օբյեկտների մասին

Պատկերավոր ասած՝ օբյեկտները 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

Թեգեր
364