Předmět primitivní konverze

Co se stane, když jsou přidány objekty obj1 + obj2 , odečteno obj1 - obj2 nebo vytištěné pomocí alert(obj) ?

JavaScript vám neumožňuje přizpůsobit, jak operátoři pracují na objektech. Na rozdíl od některých jiných programovacích jazyků, jako je Ruby nebo C++, nemůžeme implementovat speciální objektovou metodu pro zpracování sčítání (nebo jiných operátorů).

V případě takových operací se objekty automaticky převedou na primitiva a operace se pak provede nad těmito primitivy a výsledkem je primitivní hodnota.

To je důležité omezení:výsledek obj1 + obj2 (nebo jiná matematická operace) nemůže být jiný objekt!

Např. nemůžeme vytvořit objekty představující vektory nebo matice (nebo úspěchy nebo cokoli), přidat je a očekávat jako výsledek „souhrnný“ objekt. Takové architektonické počiny jsou automaticky „mimo plán“.

Takže, protože zde technicky moc nezmůžeme, ve skutečných projektech se s objekty nepočítá. Když se to stane, až na vzácné výjimky, je to kvůli chybě v kódování.

V této kapitole se budeme zabývat tím, jak se objekt převádí na primitivní a jak jej přizpůsobit.

Máme dva účely:

  1. Umožní nám to porozumět tomu, co se děje v případě chyb v kódování, když k takové operaci došlo náhodou.
  2. Existují výjimky, kdy jsou takové operace možné a vypadají dobře. Např. odečítání nebo porovnávání dat (Date předměty). Najdeme je později.

Pravidla převodu

V kapitole Převody typů jsme viděli pravidla pro numerické, řetězcové a booleovské převody primitiv. Ale nechali jsme mezeru pro předměty. Nyní, jak víme o metodách a symbolech, je možné je vyplnit.

  1. Neprobíhá převod na logickou hodnotu. Všechny objekty jsou true v booleovském kontextu, tak jednoduchém. Existují pouze převody čísel a řetězců.
  2. K numerickému převodu dochází, když odečítáme objekty nebo aplikujeme matematické funkce. Například Date objekty (které budou popsány v kapitole Datum a čas) lze odečíst a výsledek date1 - date2 je časový rozdíl mezi dvěma daty.
  3. Pokud jde o převod řetězce, obvykle k němu dochází, když na výstup vydáme objekt s alert(obj) a v podobných kontextech.

Převod řetězců a čísel můžeme implementovat sami pomocí speciálních objektových metod.

Nyní pojďme k technickým detailům, protože je to jediný způsob, jak pokrýt téma do hloubky.

Rady

Jak JavaScript rozhodne, kterou konverzi použít?

Existují tři varianty převodu typu, ke kterým dochází v různých situacích. Říká se jim „nápovědy“, jak je popsáno ve specifikaci:

"string"

Pro převod z objektu na řetězec, když provádíme operaci s objektem, který očekává řetězec, například alert :

// output
alert(obj);

// using object as a property key
anotherObj[obj] = 123;
"number"

Pro převod z objektu na číslo, jako když počítáme:

// explicit conversion
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// less/greater comparison
let greater = user1 > user2;

Většina vestavěných matematických funkcí také zahrnuje takový převod.

"default"

Vyskytuje se ve vzácných případech, kdy si operátor „není jistý“, jaký typ očekávat.

Například binární plus + umí pracovat jak s řetězci (zřetězuje je), tak s čísly (sčítá je). Pokud tedy binární plus získá objekt jako argument, použije "default" nápověda k převodu.

Také, pokud je objekt porovnáván pomocí == s řetězcem, číslem nebo symbolem, není také jasné, jaký převod by měl být proveden, takže "default" používá se nápověda.

// binary plus uses the "default" hint
let total = obj1 + obj2;

// obj == number uses the "default" hint
if (user == 1) { ... };

Větší a menší operátory porovnání, například < > , může pracovat jak s řetězci, tak s čísly. Přesto používají "number" nápověda, nikoli "default" . Je to z historických důvodů.

V praxi jsou však věci o něco jednodušší.

Všechny vestavěné objekty kromě jednoho případu (Date objekt, naučíme se to později) implementujte "default" konverzi stejným způsobem jako "number" . A pravděpodobně bychom měli udělat totéž.

Přesto je důležité vědět o všech 3 nápovědách, brzy uvidíme proč.

K provedení převodu se JavaScript pokusí najít a zavolat tři objektové metody:

  1. Zavolejte na číslo obj[Symbol.toPrimitive](hint) – metoda se symbolickým klíčem Symbol.toPrimitive (systémový symbol), pokud taková metoda existuje,
  2. Jinak, pokud je nápověda "string"
    • zkuste zavolat na číslo obj.toString() nebo obj.valueOf() , cokoli, co existuje.
  3. Jinak, pokud je nápověda "number" nebo "default"
    • zkuste zavolat obj.valueOf() nebo obj.toString() , cokoli, co existuje.

Symbol.toPrimitive

Začněme od prvního způsobu. Je tam vestavěný symbol s názvem Symbol.toPrimitive který by měl být použit pro pojmenování metody převodu takto:

obj[Symbol.toPrimitive] = function(hint) {
 // here goes the code to convert this object to a primitive
 // it must return a primitive value
 // hint = one of "string", "number", "default"
};

Pokud je metoda Symbol.toPrimitive existuje, používá se pro všechny rady a nejsou potřeba žádné další metody.

Například zde user objekt jej implementuje:

let user = {
 name: "John",
 money: 1000,

 [Symbol.toPrimitive](hint) {
 alert(`hint: ${hint}`);
 return hint == "string" ? `{name: "${this.name}"}` : this.money;
 }
};

// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

Jak můžeme vidět z kódu, user se v závislosti na konverzi stane samopopisným řetězcem nebo peněžní částkou. Jediná metoda user[Symbol.toPrimitive] zpracovává všechny případy konverze.

toString/valueOf

Pokud neexistuje Symbol.toPrimitive pak se JavaScript pokusí najít metody toString a valueOf :

  • Pro "string" nápověda:volejte toString a pokud neexistuje nebo pokud vrací objekt místo primitivní hodnoty, zavolejte valueOf (takže toString má prioritu pro převody řetězců).
  • Další tipy:volejte valueOf a pokud neexistuje nebo pokud vrací objekt místo primitivní hodnoty, zavolejte toString (takže valueOf má přednost pro matematiku).

Metody toString a valueOf pocházejí z dávných dob. Nejsou to symboly (symboly tak dávno neexistovaly), ale spíše „běžné“ metody pojmenované řetězci. Poskytují alternativní „starý“ způsob implementace konverze.

Tyto metody musí vrátit primitivní hodnotu. Pokud toString nebo valueOf vrátí objekt, pak je ignorován (stejně, jako kdyby neexistovala žádná metoda).

Ve výchozím nastavení má prostý objekt následující toString a valueOf metody:

  • toString metoda vrací řetězec "[object Object]" .
  • valueOf metoda vrací samotný objekt.

Zde je ukázka:

let user = {name: "John"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

Pokud se tedy pokusíme použít objekt jako řetězec, jako v alert nebo tak, pak ve výchozím nastavení vidíme [object Object] .

Výchozí valueOf je zde zmíněn pouze pro úplnost, aby nedošlo k záměně. Jak vidíte, vrací samotný objekt, a tak je ignorován. Neptejte se mě proč, je to z historických důvodů. Můžeme tedy předpokládat, že neexistuje.

Pojďme implementovat tyto metody k přizpůsobení převodu.

Například zde user dělá totéž jako výše s použitím kombinace toString a valueOf místo Symbol.toPrimitive :

let user = {
 name: "John",
 money: 1000,

 // for hint="string"
 toString() {
 return `{name: "${this.name}"}`;
 },

 // for hint="number" or "default"
 valueOf() {
 return this.money;
 }

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

Jak vidíme, chování je stejné jako v předchozím příkladu s Symbol.toPrimitive .

Často chceme jediné „chytré“ místo pro zpracování všech primitivních konverzí. V tomto případě můžeme implementovat toString pouze takto:

let user = {
 name: "John",

 toString() {
 return this.name;
 }
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

Při absenci Symbol.toPrimitive a valueOf , toString zvládne všechny primitivní převody.

Konverze může vrátit jakýkoli primitivní typ

Důležité je vědět o všech primitivních konverzních metodách, že nemusí nutně vracet „naznačené“ primitivum.

Neexistuje žádná kontrola, zda toString vrátí přesně řetězec nebo zda Symbol.toPrimitive metoda vrací číslo pro nápovědu "number" .

Jediná povinná věc:tyto metody musí vrátit primitiv, nikoli objekt.

Historické poznámky

Z historických důvodů, pokud toString nebo valueOf vrátí objekt, není tam žádná chyba, ale taková hodnota je ignorována (jako kdyby metoda neexistovala). Je to proto, že ve starověku neexistoval v JavaScriptu dobrý koncept „chyby“.

Naproti tomu Symbol.toPrimitive je přísnější, musí vrátí primitiv, jinak dojde k chybě.

Další konverze

Jak již víme, mnoho operátorů a funkcí provádí převody typů, např. násobení * převádí operandy na čísla.

Pokud předáme objekt jako argument, pak existují dvě fáze výpočtů:

  1. Objekt je převeden na primitivní (pomocí výše popsaných pravidel).
  2. Pokud je to nutné pro další výpočty, převede se také výsledné primitivum.

Například:

let obj = {
 // toString handles all conversions in the absence of other methods
 toString() {
 return "2";
 }
};

alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
  1. Násobení obj * 2 nejprve převede objekt na primitivní (to je řetězec "2" ).
  2. Poté "2" * 2 se změní na 2 * 2 (řetězec se převede na číslo).

Binary plus zřetězí řetězce ve stejné situaci, protože rád přijímá řetězec:

let obj = {
 toString() {
 return "2";
 }
};

alert(obj + 2); // 22 ("2" + 2), conversion to primitive returned a string => concatenation

Shrnutí

Konverze z objektu na primitiva je volána automaticky mnoha vestavěnými funkcemi a operátory, které očekávají primitiva jako hodnotu.

Existují 3 typy (nápovědy):

  • "string" (pro alert a další operace, které vyžadují řetězec)
  • "number" (pro matematiku)
  • "default" (málo operátorů, obvykle to objekty implementují stejným způsobem jako "number" )

Specifikace explicitně popisuje, který operátor používá kterou nápovědu.

Algoritmus převodu je:

  1. Zavolejte na číslo obj[Symbol.toPrimitive](hint) pokud metoda existuje,
  2. Jinak, pokud je nápověda "string"
    • zkuste zavolat na číslo obj.toString() nebo obj.valueOf() , cokoli, co existuje.
  3. Jinak, pokud je nápověda "number" nebo "default"
    • zkuste zavolat na číslo obj.valueOf() nebo obj.toString() , cokoli, co existuje.

Všechny tyto metody musí vrátit primitivum do práce (pokud je definováno).

V praxi často stačí implementovat pouze obj.toString() jako „catch-all“ metoda pro převody řetězců, která by měla vracet „lidsky čitelnou“ reprezentaci objektu pro účely protokolování nebo ladění.