ToPrimitive()ToString() und verwandte OperationenToPropertyKey()ToNumeric() und verwandte Operationen+)==)In diesem Kapitel untersuchen wir die Rolle der Typumwandlung in JavaScript. Wir werden dieses Thema relativ tiefgehend behandeln und uns z. B. ansehen, wie die ECMAScript-Spezifikation Typumwandlung handhabt.
Jede Operation (Funktion, Operator usw.) erwartet, dass ihre Parameter bestimmte Typen haben. Wenn ein Wert nicht den richtigen Typ für einen Parameter hat, gibt es drei gängige Optionen, z. B. für eine Funktion:
Die Funktion kann eine Ausnahme auslösen
Die Funktion kann einen Fehlerwert zurückgeben
Die Funktion kann ihre Argumente in nützliche Werte umwandeln
Bei (3) führt die Operation eine implizite Typumwandlung durch. Dies wird als Typumwandlung bezeichnet.
JavaScript hatte anfangs keine Ausnahmen, weshalb es für die meisten seiner Operationen Typumwandlung und Fehlerwerte verwendet.
// Coercion
assert.equal(3 * true, 3);
// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);Es gibt jedoch auch Fälle (insbesondere bei neueren Funktionen), in denen Ausnahmen ausgelöst werden, wenn ein Argument nicht den richtigen Typ hat.
Zugriff auf Eigenschaften von null oder undefined
Verwendung von Symbolen
Mischen von BigInts und Zahlen
Aufrufe von Werten mit new oder Funktion, die diese Operation nicht unterstützen
Ändern schreibgeschützter Eigenschaften (löst nur im Strict Mode einen Fehler aus)
Zwei gängige Methoden, mit Typumwandlung umzugehen, sind:
Ein Aufrufer kann Werte explizit umwandeln, damit sie die richtigen Typen haben. Zum Beispiel möchten wir im folgenden Interaktionsbeispiel zwei als Strings kodierte Zahlen multiplizieren:
Ein Aufrufer kann die Operation die Umwandlung für ihn durchführen lassen.
Ich bevorzuge normalerweise die erstere Methode, da sie meine Absicht verdeutlicht: Ich erwarte, dass x und y keine Zahlen sind, möchte aber zwei Zahlen multiplizieren.
Die folgenden Abschnitte beschreiben die wichtigsten internen Funktionen, die von der ECMAScript-Spezifikation verwendet werden, um tatsächliche Parameter in erwartete Typen umzuwandeln.
Zum Beispiel würden wir in TypeScript schreiben:
In der Spezifikation sieht dies wie folgt aus (übersetzt in JavaScript, um das Verständnis zu erleichtern):
Immer wenn primitive Typen oder Objekte erwartet werden, werden die folgenden Umwandlungsfunktionen verwendet:
ToBoolean()ToNumber()ToBigInt()ToString()ToObject()Diese internen Funktionen haben Entsprechungen in JavaScript, die sehr ähnlich sind.
Nach der Einführung von BigInts, die neben Zahlen existieren, verwendet die Spezifikation häufig ToNumeric(), wo sie zuvor ToNumber() verwendete. Lesen Sie weiter für mehr Informationen.
Derzeit hat JavaScript zwei eingebaute numerische Typen: number und bigint.
ToNumeric() gibt einen numerischen Wert num zurück. Seine Aufrufer rufen normalerweise eine Methode mthd des Spezifikationstyps von num auf.
Type(num)::mthd(···)
Unter anderem verwenden die folgenden Operationen ToNumeric:
++-Operator*-OperatorToInteger(x) wird verwendet, wenn eine Zahl ohne Nachkommastellen erwartet wird. Der Bereich des Ergebnisses wird oft danach weiter eingeschränkt.
ToNumber(x) auf und entfernt die Nachkommastellen (ähnlich wie Math.trunc()).ToInteger() verwenden:Number.prototype.toString(radix?)String.prototype.codePointAt(pos)Array.prototype.slice(start, end)ToInt32(), ToUint32() wandeln Zahlen in 32-Bit-Integer um und werden von bitweisen Operatoren verwendet (siehe Tab. 1).
ToInt32(): vorzeichenbehaftet, Bereich [−231, 231−1] (Grenzen eingeschlossen)ToUint32(): vorzeichenlos (daher das U), Bereich [0, 232−1] (Grenzen eingeschlossen)| Operator | Linker Operand | Rechter Operand | Ergebnistyp |
|---|---|---|---|
<< |
ToInt32() |
ToUint32() |
Int32 |
vorzeichenbehaftetes >> |
ToInt32() |
ToUint32() |
Int32 |
vorzeichenloses >>> |
ToInt32() |
ToUint32() |
Uint32 |
&, ^, | |
ToInt32() |
ToUint32() |
Int32 |
~ |
— | ToInt32() |
Int32 |
ToPropertyKey() gibt einen String oder ein Symbol zurück und wird verwendet von:
[]in-OperatorsObject.defineProperty(_, P, _)Object.fromEntries()Object.getOwnPropertyDescriptor()Object.prototype.hasOwnProperty()Object.prototype.propertyIsEnumerable()ReflectToLength() wird (direkt) hauptsächlich für String-Indizes verwendet.ToIndex()l: 0 ≤ l ≤ 253−1ToIndex() wird für Typed Array-Indizes verwendet.ToLength(): löst eine Ausnahme aus, wenn das Argument außerhalb des Bereichs liegt.i: 0 ≤ i ≤ 253−1ToUint32() wird für Array-Indizes verwendet.i: 0 ≤ i < 232−1 (die Obergrenze ist ausgeschlossen, um Platz für die .length zu lassen)Wenn wir den Wert eines Typed Array-Elements setzen, wird eine der folgenden Umwandlungsfunktionen verwendet:
ToInt8()ToUint8()ToUint8Clamp()ToInt16()ToUint16()ToInt32()ToUint32()ToBigInt64()ToBigUint64()Im Rest dieses Kapitels werden wir mehrere Spezifikationsalgorithmen antreffen, die aber als JavaScript „implementiert“ sind. Die folgende Liste zeigt, wie einige häufig verwendete Muster von der Spezifikation in JavaScript übersetzt werden:
Spec: If Type(value) is String
JavaScript: if (TypeOf(value) === 'string')
(sehr lose Übersetzung; TypeOf() ist unten definiert)
Spec: If IsCallable(method) is true
JavaScript: if (IsCallable(method))
(IsCallable() ist unten definiert)
Spec: Let numValue be ToNumber(value)
JavaScript: let numValue = Number(value)
Spec: Let isArray be IsArray(O)
JavaScript: let isArray = Array.isArray(O)
Spec: If O has a [[NumberData]] internal slot
JavaScript: if ('__NumberData__' in O)
Spec: Let tag be Get(O, @@toStringTag)
JavaScript: let tag = O[Symbol.toStringTag]
Spec: Return the string-concatenation of “[object ", tag, and "]”.
JavaScript: return '[object ' + tag + ']';
let (und nicht const) wird verwendet, um der Sprache der Spezifikation zu entsprechen.
Einige Dinge werden weggelassen – zum Beispiel die ReturnIfAbrupt-Kurzformen ? und !.
/**
* An improved version of typeof
*/
function TypeOf(value) {
const result = typeof value;
switch (result) {
case 'function':
return 'object';
case 'object':
if (value === null) {
return 'null';
} else {
return 'object';
}
default:
return result;
}
}
function IsCallable(x) {
return typeof x === 'function';
}ToPrimitive()Die Operation ToPrimitive() ist ein Zwischenschritt für viele Typumwandlungsalgorithmen (von denen einige wir später in diesem Kapitel sehen werden). Sie wandelt beliebige Werte in primitive Werte um.
ToPrimitive() wird in der Spezifikation häufig verwendet, da die meisten Operatoren nur mit primitiven Werten arbeiten können. Zum Beispiel können wir den Additionsoperator (+) verwenden, um Zahlen zu addieren und Strings zu verketten, aber wir können ihn nicht verwenden, um Arrays zu verketten.
Dies ist, wie die JavaScript-Version von ToPrimitive() aussieht:
/**
* @param hint Which type is preferred for the result:
* string, number, or don’t care?
*/
function ToPrimitive(input: any,
hint: 'string'|'number'|'default' = 'default') {
if (TypeOf(input) === 'object') {
let exoticToPrim = input[Symbol.toPrimitive]; // (A)
if (exoticToPrim !== undefined) {
let result = exoticToPrim.call(input, hint);
if (TypeOf(result) !== 'object') {
return result;
}
throw new TypeError();
}
if (hint === 'default') {
hint = 'number';
}
return OrdinaryToPrimitive(input, hint);
} else {
// input is already primitive
return input;
}
}ToPrimitive() erlaubt Objekten, die Umwandlung in primitive Werte über Symbol.toPrimitive (Zeile A) zu überschreiben. Wenn ein Objekt dies nicht tut, wird es an OrdinaryToPrimitive() weitergeleitet.
function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
let methodNames;
if (hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else {
methodNames = ['valueOf', 'toString'];
}
for (let name of methodNames) {
let method = O[name];
if (IsCallable(method)) {
let result = method.call(O);
if (TypeOf(result) !== 'object') {
return result;
}
}
}
throw new TypeError();
}ToPrimitive()?Der Parameter hint kann einen von drei Werten annehmen:
'number' bedeutet: wenn möglich, sollte input in eine Zahl umgewandelt werden.'string' bedeutet: wenn möglich, sollte input in einen String umgewandelt werden.'default' bedeutet: es gibt keine Präferenz für Zahlen oder Strings.Hier sind einige Beispiele dafür, wie verschiedene Operationen ToPrimitive() verwenden:
hint === 'number'. Die folgenden Operationen bevorzugen Zahlen:ToNumeric()ToNumber()ToBigInt(), BigInt()<)hint === 'string'. Die folgenden Operationen bevorzugen Strings:ToString()ToPropertyKey()hint === 'default'. Die folgenden Operationen sind neutral bezüglich des Typs des zurückgegebenen primitiven Werts:==)+)new Date(value) (value kann entweder eine Zahl oder ein String sein)Wie wir gesehen haben, ist das Standardverhalten für 'default', als ob es 'number' wäre. Nur Instanzen von Symbol und Date überschreiben dieses Verhalten (später gezeigt).
Wenn die Umwandlung in primitive Werte nicht über Symbol.toPrimitive überschrieben wird, ruft OrdinaryToPrimitive() entweder eine oder beide der folgenden beiden Methoden auf:
'toString' wird zuerst aufgerufen, wenn hint angibt, dass wir möchten, dass der primitive Wert ein String ist.'valueOf' wird zuerst aufgerufen, wenn hint angibt, dass wir möchten, dass der primitive Wert eine Zahl ist.Der folgende Code demonstriert, wie das funktioniert:
const obj = {
toString() { return 'a' },
valueOf() { return 1 },
};
// String() prefers strings:
assert.equal(String(obj), 'a');
// Number() prefers numbers:
assert.equal(Number(obj), 1);Eine Methode mit dem Eigenschaftsschlüssel Symbol.toPrimitive überschreibt die normale Umwandlung in primitive Werte. Dies wird nur zweimal in der Standardbibliothek gemacht:
Symbol.prototype[Symbol.toPrimitive](hint)
Symbol ist, gibt diese Methode immer das gewrappte Symbol zurück.Symbol eine .toString()-Methode haben, die Strings zurückgibt. Aber auch wenn hint 'string' ist, sollte .toString() nicht aufgerufen werden, damit wir Instanzen von Symbol nicht versehentlich in Strings umwandeln (die eine völlig andere Art von Eigenschaftsschlüssel sind).Date.prototype[Symbol.toPrimitive](hint)
Date.prototype[Symbol.toPrimitive]()So behandeln Dates die Umwandlung in primitive Werte:
Date.prototype[Symbol.toPrimitive] = function (
hint: 'default' | 'string' | 'number') {
let O = this;
if (TypeOf(O) !== 'object') {
throw new TypeError();
}
let tryFirst;
if (hint === 'string' || hint === 'default') {
tryFirst = 'string';
} else if (hint === 'number') {
tryFirst = 'number';
} else {
throw new TypeError();
}
return OrdinaryToPrimitive(O, tryFirst);
};Der einzige Unterschied zum Standardalgorithmus besteht darin, dass 'default' zu 'string' (und nicht zu 'number') wird. Dies kann beobachtet werden, wenn wir Operationen verwenden, die hint auf 'default' setzen:
Der ==-Operator wandelt Objekte in primitive Werte um (mit einem Standardhinweis), wenn der andere Operand ein primitiver Wert außer undefined, null und boolean ist. In der folgenden Interaktion können wir sehen, dass das Ergebnis der Umwandlung des Datums ein String ist:
Der +-Operator wandelt beide Operanden in primitive Werte um (mit einem Standardhinweis). Wenn eines der Ergebnisse ein String ist, führt er String-Verkettung durch (andernfalls numerische Addition). In der folgenden Interaktion können wir sehen, dass das Ergebnis der Umwandlung des Datums ein String ist, da der Operator einen String zurückgibt.
ToString() und verwandte OperationenDies ist die JavaScript-Version von ToString():
function ToString(argument) {
if (argument === undefined) {
return 'undefined';
} else if (argument === null) {
return 'null';
} else if (argument === true) {
return 'true';
} else if (argument === false) {
return 'false';
} else if (TypeOf(argument) === 'number') {
return Number.toString(argument);
} else if (TypeOf(argument) === 'string') {
return argument;
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
return BigInt.toString(argument);
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'string'); // (A)
return ToString(primValue);
}
}Beachten Sie, wie diese Funktion ToPrimitive() als Zwischenschritt für Objekte verwendet, bevor das primitive Ergebnis in einen String umgewandelt wird (Zeile A).
ToString() weicht auf interessante Weise von der Funktionsweise von String() ab: Wenn argument ein Symbol ist, löst die erstere einen TypeError aus, während die letztere dies nicht tut. Warum? Der Standard für Symbole ist, dass die Umwandlung in Strings Ausnahmen auslöst.
> const sym = Symbol('sym');
> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a stringDieser Standard wird in String() und Symbol.prototype.toString() (beide werden in den nächsten Unterabschnitten beschrieben) überschrieben.
String()function String(value) {
let s;
if (value === undefined) {
s = '';
} else {
if (new.target === undefined && TypeOf(value) === 'symbol') {
// This function was function-called and value is a symbol
return SymbolDescriptiveString(value);
}
s = ToString(value);
}
if (new.target === undefined) {
// This function was function-called
return s;
}
// This function was new-called
return StringCreate(s, new.target.prototype); // simplified!
}String() funktioniert unterschiedlich, je nachdem, ob es über einen Funktionsaufruf oder über new aufgerufen wird. Es verwendet new.target, um die beiden zu unterscheiden.
Dies sind die Hilfsfunktionen StringCreate() und SymbolDescriptiveString():
/**
* Creates a String instance that wraps `value`
* and has the given protoype.
*/
function StringCreate(value, prototype) {
// ···
}
function SymbolDescriptiveString(sym) {
assert.equal(TypeOf(sym), 'symbol');
let desc = sym.description;
if (desc === undefined) {
desc = '';
}
assert.equal(TypeOf(desc), 'string');
return 'Symbol('+desc+')';
}Symbol.prototype.toString()Neben String() können wir auch die Methode .toString() verwenden, um ein Symbol in einen String umzuwandeln. Seine Spezifikation sieht wie folgt aus.
Symbol.prototype.toString = function () {
let sym = thisSymbolValue(this);
return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
if (TypeOf(value) === 'symbol') {
return value;
}
if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
let s = value.__SymbolData__;
assert.equal(TypeOf(s), 'symbol');
return s;
}
}Object.prototype.toStringDie Standard-Spezifikation für .toString() sieht wie folgt aus:
Object.prototype.toString = function () {
if (this === undefined) {
return '[object Undefined]';
}
if (this === null) {
return '[object Null]';
}
let O = ToObject(this);
let isArray = Array.isArray(O);
let builtinTag;
if (isArray) {
builtinTag = 'Array';
} else if ('__ParameterMap__' in O) {
builtinTag = 'Arguments';
} else if ('__Call__' in O) {
builtinTag = 'Function';
} else if ('__ErrorData__' in O) {
builtinTag = 'Error';
} else if ('__BooleanData__' in O) {
builtinTag = 'Boolean';
} else if ('__NumberData__' in O) {
builtinTag = 'Number';
} else if ('__StringData__' in O) {
builtinTag = 'String';
} else if ('__DateValue__' in O) {
builtinTag = 'Date';
} else if ('__RegExpMatcher__' in O) {
builtinTag = 'RegExp';
} else {
builtinTag = 'Object';
}
let tag = O[Symbol.toStringTag];
if (TypeOf(tag) !== 'string') {
tag = builtinTag;
}
return '[object ' + tag + ']';
};Diese Operation wird verwendet, wenn wir einfache Objekte in Strings umwandeln.
Standardmäßig wird sie auch verwendet, wenn wir Instanzen von Klassen in Strings umwandeln.
Normalerweise würden wir .toString() überschreiben, um die String-Darstellung von MyClass zu konfigurieren, aber wir können auch ändern, was nach „object“ innerhalb des Strings steht, mit den eckigen Klammern.
class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
String(new MyClass()), '[object Custom!]');Es ist interessant, die überschriebenen Versionen von .toString() mit der ursprünglichen Version in Object.prototype zu vergleichen.
> ['a', 'b'].toString()
'a,b'
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'
> /^abc$/.toString()
'/^abc$/'
> Object.prototype.toString.call(/^abc$/)
'[object RegExp]'ToPropertyKey()ToPropertyKey() wird unter anderem vom Klammeroperator verwendet. So funktioniert es:
function ToPropertyKey(argument) {
let key = ToPrimitive(argument, 'string'); // (A)
if (TypeOf(key) === 'symbol') {
return key;
}
return ToString(key);
}Auch hier werden Objekte in primitive Werte umgewandelt, bevor mit primitiven Werten gearbeitet wird.
ToNumeric() und verwandte OperationenToNumeric() wird unter anderem vom Multiplikationsoperator (*) verwendet. So funktioniert es:
function ToNumeric(value) {
let primValue = ToPrimitive(value, 'number');
if (TypeOf(primValue) === 'bigint') {
return primValue;
}
return ToNumber(primValue);
}ToNumber()ToNumber() funktioniert wie folgt:
function ToNumber(argument) {
if (argument === undefined) {
return NaN;
} else if (argument === null) {
return +0;
} else if (argument === true) {
return 1;
} else if (argument === false) {
return +0;
} else if (TypeOf(argument) === 'number') {
return argument;
} else if (TypeOf(argument) === 'string') {
return parseTheString(argument); // not shown here
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
throw new TypeError();
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'number');
return ToNumber(primValue);
}
}Die Struktur von ToNumber() ist ähnlich der Struktur von ToString().
+)So ist der Additionsoperator von JavaScript spezifiziert:
function Addition(leftHandSide, rightHandSide) {
let lprim = ToPrimitive(leftHandSide);
let rprim = ToPrimitive(rightHandSide);
if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
return ToString(lprim) + ToString(rprim);
}
let lnum = ToNumeric(lprim);
let rnum = ToNumeric(rprim);
if (TypeOf(lnum) !== TypeOf(rnum)) {
throw new TypeError();
}
let T = Type(lnum);
return T.add(lnum, rnum); // (B)
}Schritte dieses Algorithmus:
Type() gibt den ECMAScript-Spezifikationstyp von lnum zurück. .add() ist eine Methode von numerischen Typen.==)/** Loose equality (==) */
function abstractEqualityComparison(x, y) {
if (TypeOf(x) === TypeOf(y)) {
// Use strict equality (===)
return strictEqualityComparison(x, y);
}
// Comparing null with undefined
if (x === null && y === undefined) {
return true;
}
if (x === undefined && y === null) {
return true;
}
// Comparing a number and a string
if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
return abstractEqualityComparison(x, Number(y));
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
return abstractEqualityComparison(Number(x), y);
}
// Comparing a bigint and a string
if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
let n = StringToBigInt(y);
if (Number.isNaN(n)) {
return false;
}
return abstractEqualityComparison(x, n);
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
return abstractEqualityComparison(y, x);
}
// Comparing a boolean with a non-boolean
if (TypeOf(x) === 'boolean') {
return abstractEqualityComparison(Number(x), y);
}
if (TypeOf(y) === 'boolean') {
return abstractEqualityComparison(x, Number(y));
}
// Comparing an object with a primitive
// (other than undefined, null, a boolean)
if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
&& TypeOf(y) === 'object') {
return abstractEqualityComparison(x, ToPrimitive(y));
}
if (TypeOf(x) === 'object'
&& ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y))) {
return abstractEqualityComparison(ToPrimitive(x), y);
}
// Comparing a bigint with a number
if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
|| (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
if ([NaN, +Infinity, -Infinity].includes(x)
|| [NaN, +Infinity, -Infinity].includes(y)) {
return false;
}
if (isSameMathematicalValue(x, y)) {
return true;
} else {
return false;
}
}
return false;
}Die folgenden Operationen werden hier nicht gezeigt:
Nachdem wir uns nun genauer angesehen haben, wie die Typumwandlung in JavaScript funktioniert, schließen wir mit einem kurzen Glossar der Begriffe im Zusammenhang mit Typumwandlung ab:
Bei der Typumwandlung möchten wir, dass der Ausgabewert einen bestimmten Typ hat. Wenn der Eingabewert bereits diesen Typ hat, wird er einfach unverändert zurückgegeben. Andernfalls wird er in einen Wert umgewandelt, der den gewünschten Typ hat.
Explizite Typumwandlung bedeutet, dass der Programmierer eine Operation (eine Funktion, einen Operator usw.) verwendet, um eine Typumwandlung auszulösen. Explizite Umwandlungen können sein:
Was Typ-Casting ist, hängt von der Programmiersprache ab. Zum Beispiel ist es in Java explizite, geprüfte Typumwandlung.
Typumwandlung ist implizite Typumwandlung: Eine Operation wandelt ihre Argumente automatisch in die Typen um, die sie benötigt. Kann geprüft oder ungeprüft oder etwas dazwischen sein.
[Quelle: Wikipedia]