Odkazy na objekty a kopírování

Jedním ze základních rozdílů mezi objekty a primitivy je to, že objekty se ukládají a kopírují „podle odkazu“, zatímco primitivní hodnoty:řetězce, čísla, booleovské hodnoty atd. – se vždy kopírují „jako celá hodnota“.

To je snadné pochopit, když se podíváme trochu pod pokličku toho, co se stane, když zkopírujeme hodnotu.

Začněme primitivem, jako je řetězec.

Zde vložíme kopii message do phrase :

let message = "Hello!";
let phrase = message;

Výsledkem je, že máme dvě nezávislé proměnné, z nichž každá obsahuje řetězec "Hello!" .

Docela zřejmý výsledek, že?

Objekty takové nejsou.

Proměnná přiřazená k objektu neukládá objekt samotný, ale jeho „adresu v paměti“ – jinými slovy „odkaz“ na něj.

Podívejme se na příklad takové proměnné:

let user = {
 name: "John"
};

A tady je návod, jak se to ve skutečnosti ukládá do paměti:

Objekt je uložen někde v paměti (vpravo na obrázku), zatímco user proměnná (vlevo) má na ni „odkaz“.

Můžeme si představit proměnnou objektu, například user , jako list papíru s adresou objektu.

Když s objektem provádíme akce, např. vzít vlastnost user.name , JavaScript engine se podívá na to, co je na této adrese, a provede operaci na skutečném objektu.

Tady je důvod, proč je to důležité.

Když se zkopíruje proměnná objektu, zkopíruje se odkaz, ale samotný objekt se nezduplikuje.

Například:

let user = { name: "John" };

let admin = user; // copy the reference

Nyní máme dvě proměnné, z nichž každá uchovává odkaz na stejný objekt:

Jak vidíte, stále existuje jeden objekt, ale nyní se dvěma proměnnými, které na něj odkazují.

K přístupu k objektu a úpravě jeho obsahu můžeme použít kteroukoli z proměnných:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // changed by the "admin" reference

alert(user.name); // 'Pete', changes are seen from the "user" reference

Je to, jako bychom měli skříň se dvěma klíči a použili jeden z nich (admin ), abyste se do toho dostali a provedli změny. Pak, pokud později použijeme jiný klíč (user ), stále otevíráme stejnou skříň a máme přístup ke změněnému obsahu.

Porovnání podle reference

Dva objekty jsou si rovny, pouze pokud jsou stejným objektem.

Například zde a a b odkazují na stejný objekt, takže jsou si rovny:

let a = {};
let b = a; // copy the reference

alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true

A zde dva nezávislé objekty nejsou stejné, i když vypadají stejně (oba jsou prázdné):

let a = {};
let b = {}; // two independent objects

alert( a == b ); // false

Pro srovnání jako obj1 > obj2 nebo pro srovnání s primitivním obj == 5 , objekty jsou převedeny na primitiva. Brzy budeme studovat, jak konverze objektů fungují, ale abych řekl pravdu, taková srovnání jsou potřeba velmi zřídka – obvykle se objeví v důsledku programátorské chyby.

Klonování a slučování, Object.assign

Kopírování proměnné objektu tedy vytvoří další odkaz na stejný objekt.

Ale co když potřebujeme duplikovat objekt?

Můžeme vytvořit nový objekt a replikovat strukturu existujícího objektu tím, že projdeme jeho vlastnosti a zkopírujeme je na primitivní úrovni.

Takhle:

let user = {
 name: "John",
 age: 30
};

let clone = {}; // the new empty object

// let's copy all user properties into it
for (let key in user) {
 clone[key] = user[key];
}

// now clone is a fully independent object with the same content
clone.name = "Pete"; // changed the data in it

alert( user.name ); // still John in the original object

Můžeme také použít metodu Object.assign.

Syntaxe je:

Object.assign(dest, [src1, src2, src3...])
  • První argument dest je cílový objekt.
  • Další argumenty src1, ..., srcN (může jich být tolik, kolik je potřeba) jsou zdrojové objekty.
  • Kopíruje vlastnosti všech zdrojových objektů src1, ..., srcN do cíle dest . Jinými slovy, vlastnosti všech argumentů počínaje druhým se zkopírují do prvního objektu.
  • Volání vrátí dest .

Můžeme jej například použít ke sloučení několika objektů do jednoho:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

Pokud zkopírovaný název vlastnosti již existuje, bude přepsán:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // now user = { name: "Pete" }

Můžeme také použít Object.assign nahradit for..in smyčka pro jednoduché klonování:

let user = {
 name: "John",
 age: 30
};

let clone = Object.assign({}, user);

Zkopíruje všechny vlastnosti user do prázdného objektu a vrátí jej.

Existují i ​​jiné metody klonování objektu, nap. pomocí syntaxe šíření clone = {...user} , probráno později ve výukovém programu.

Vnořené klonování

Doposud jsme předpokládali, že všechny vlastnosti user jsou primitivní. Vlastnosti však mohou být odkazy na jiné objekty.

Takhle:

let user = {
 name: "John",
 sizes: {
 height: 182,
 width: 50
 }
};

alert( user.sizes.height ); // 182

Nyní nestačí zkopírovat clone.sizes = user.sizes , protože user.sizes je objekt a bude zkopírován odkazem, takže clone a user bude sdílet stejné velikosti:

let user = {
 name: "John",
 sizes: {
 height: 182,
 width: 50
 }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, same object

// user and clone share sizes
user.sizes.width++; // change a property from one place
alert(clone.sizes.width); // 51, get the result from the other one

Chcete-li to opravit a vytvořit user a clone skutečně oddělené objekty, měli bychom použít klonovací smyčku, která zkoumá každou hodnotu user[key] a pokud je to objekt, pak také replikujte jeho strukturu. Tomu se říká „hluboké klonování“.

K jeho implementaci můžeme použít rekurzi. Nebo, pokud nechcete znovu objevovat kolo, vezměte existující implementaci, například _.cloneDeep(obj) z JavaScriptové knihovny lodash.

Objekty Const lze upravovat

Důležitým vedlejším efektem ukládání objektů jako odkazů je, že objekt je deklarován jako const může být upraven.

Například:

const user = {
 name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

Mohlo by se zdát, že řádek (*) způsobí chybu, ale nedělá. Hodnota user je konstantní, musí vždy odkazovat na stejný objekt, ale vlastnosti tohoto objektu se mohou volně měnit.

Jinými slovy, const user dává chybu, pouze pokud se pokusíme nastavit user=... jako celek.

To znamená, že pokud opravdu potřebujeme vytvořit konstantní vlastnosti objektu, je to také možné, ale pomocí zcela jiných metod. Zmíníme se o tom v kapitole Příznaky a deskriptory vlastností.

Shrnutí

Objekty jsou přiřazeny a zkopírovány podle odkazu. Jinými slovy, proměnná neukládá „hodnotu objektu“, ale „odkaz“ (adresu v paměti) pro hodnotu. Takže zkopírování takové proměnné nebo její předání jako argument funkce zkopíruje tento odkaz, nikoli samotný objekt.

Všechny operace prostřednictvím zkopírovaných referencí (jako je přidávání/odebírání vlastností) se provádějí na stejném jediném objektu.

K vytvoření „skutečné kopie“ (klonu) můžeme použít Object.assign pro takzvanou „mělkou kopii“ (vnořené objekty se kopírují odkazem) nebo funkci „hlubokého klonování“, jako je _.cloneDeep(obj).