ToPrimitive()
ToString()
および関連演算ToPropertyKey()
ToNumeric()
および関連演算+
)==
)この章では、JavaScriptにおける型強制の役割を調べます。このトピックについて比較的深く掘り下げ、例えばECMAScript仕様がどのように型強制を扱うかを見ていきます。
各演算(関数、演算子など)は、そのパラメータが特定の型を持つことを期待します。値がパラメータに適切な型を持っていない場合、関数などに対しては3つの一般的な選択肢があります。
関数が例外をスローする
関数がエラー値を返す
関数がその引数を有用な値に変換する
(3)では、演算は暗黙的な型変換を実行します。これは型強制と呼ばれます。
JavaScriptは当初例外を持たなかったため、ほとんどの演算で型強制とエラー値を使用しています。
// Coercion
assert.equal(3 * true, 3);
// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);
しかし、(特に新しい機能に関しては)引数が適切な型を持っていない場合に例外をスローするケースもあります。
null
またはundefined
のプロパティへのアクセス
シンボルの使用
BigIntと数値の混合
その演算をサポートしていない新しい呼び出しまたは関数呼び出しの値
読み取り専用プロパティの変更(厳格モードでのみ例外をスロー)
型強制に対処する2つの一般的な方法は次のとおりです。
呼び出し元は、値を明示的に変換して適切な型にすることができます。例えば、次のやり取りでは、文字列としてエンコードされた2つの数値を掛け合わせたいと考えています。
呼び出し元は、演算にその変換を任せることができます。
私は通常、前者の方が好みです。なぜなら、私の意図を明確にするからです。私はx
とy
が数値ではないことを期待していますが、2つの数値を掛け合わせたいのです。
以降のセクションでは、ECMAScript仕様で実際の引数を期待される型に変換するために使用される最も重要な内部関数を説明します。
例えば、TypeScriptでは次のように記述します。
仕様では、これは次のようになります(JavaScriptに翻訳されているため、理解しやすくなっています)。
プリミティブ型またはオブジェクトが期待される場合は、次の変換関数が使用されます。
ToBoolean()
ToNumber()
ToBigInt()
ToString()
ToObject()
これらの内部関数には、JavaScriptで非常に類似したアナログがあります。
数値と並んで存在するBigIntの導入後、仕様では以前ToNumber()を使用していた場所にToNumeric()
を頻繁に使用しています。詳細については、読み進めてください。
現在、JavaScriptには2つの組み込み数値型があります:numberとbigint。
ToNumeric()
は数値num
を返します。その呼び出し元は通常、num
の仕様型であるメソッドmthd
を呼び出します。
Type(num)::mthd(···)
その他にも、次の演算でToNumeric
を使用します。
++
演算子*
演算子小数点以下のない数値が期待される場合は、ToInteger(x)
が使用されます。結果の範囲は、その後さらに制限されることがよくあります。
ToNumber(x)
を呼び出し、小数点以下を除去します(Math.trunc()
に似ています)。ToInteger()
を使用する演算Number.prototype.toString(radix?)
String.prototype.codePointAt(pos)
Array.prototype.slice(start, end)
ToInt32()
、ToUint32()
は数値を32ビット整数に変換し、ビット演算子で使用されます(表1を参照)。
ToInt32()
:符号付き、範囲[−231, 231−1](制限値を含む)ToUint32()
:符号なし(したがってU
)、範囲[0, 232−1](制限値を含む)演算子 | 左オペランド | 右オペランド | 結果の型 |
---|---|---|---|
<< |
ToInt32() |
ToUint32() |
Int32 |
符号付き >> |
ToInt32() |
ToUint32() |
Int32 |
符号なし >>> |
ToInt32() |
ToUint32() |
Uint32 |
& , ^ , | |
ToInt32() |
ToUint32() |
Int32 |
~ |
— | ToInt32() |
Int32 |
ToPropertyKey()
は文字列またはシンボルを返し、次で使用されます。
[]
in
演算子の左辺Object.defineProperty(_, P, _)
Object.fromEntries()
Object.getOwnPropertyDescriptor()
Object.prototype.hasOwnProperty()
Object.prototype.propertyIsEnumerable()
Reflect
メソッドToLength()
は(直接)主に文字列インデックスに使用されます。ToIndex()
のヘルパー関数l
の範囲:0 ≤ l
≤ 253−1ToIndex()
は型付き配列インデックスに使用されます。ToLength()
との主な違い:引数が範囲外の場合、例外をスローします。i
の範囲:0 ≤ i
≤ 253−1ToUint32()
は配列インデックスに使用されます。i
の範囲:0 ≤ i
< 232−1(上限は除外され、.length
のためのスペースを残します)型付き配列要素の値を設定する場合、次の変換関数のいずれかが使用されます。
ToInt8()
ToUint8()
ToUint8Clamp()
ToInt16()
ToUint16()
ToInt32()
ToUint32()
ToBigInt64()
ToBigUint64()
この章の残りの部分では、いくつかの仕様アルゴリズムに遭遇しますが、「実装」はJavaScriptで行われます。次のリストは、頻繁に使用されるパターンを仕様からJavaScriptにどのように変換するかを示しています。
仕様:Type(value)がStringの場合
JavaScript:if (TypeOf(value) === 'string')
(非常に大まかな翻訳です。TypeOf()
は以下で定義されています)
仕様:IsCallable(method)がtrueの場合
JavaScript:if (IsCallable(method))
(IsCallable()
は以下で定義されています)
仕様:numValueをToNumber(value)とする
JavaScript:let numValue = Number(value)
仕様:isArrayをIsArray(O)とする
JavaScript:let isArray = Array.isArray(O)
仕様:Oに[[NumberData]]内部スロットがある場合
JavaScript:if ('__NumberData__' in O)
仕様:tagをGet(O, @@toStringTag)とする
JavaScript:let tag = O[Symbol.toStringTag]
仕様:「[object 」、tag、「]」の文字列連結を返す。
JavaScript:return '[object ' + tag + ']';
仕様の言語に合わせるため、let
(const
ではなく)を使用しています。
いくつかのものは省略されています。たとえば、ReturnIfAbruptショートハンドの?
と!
です。
/**
* 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()
演算ToPrimitive()
は、多くの型強制アルゴリズムの中間ステップです(この章の後半でいくつか見ていきます)。これは任意の値をプリミティブ値に変換します。
ToPrimitive()
は仕様で頻繁に使用されます。なぜなら、ほとんどの演算子はプリミティブ値でのみ動作できるためです。たとえば、加算演算子(+
)を使用して数値を加算し、文字列を連結できますが、配列を連結するには使用できません。
これが、ToPrimitive()
のJavaScriptバージョンです。
/**
* @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()
は、オブジェクトがSymbol.toPrimitive
(A行)を介してプリミティブへの変換をオーバーライドできるようにします。オブジェクトがそうしない場合、それはOrdinaryToPrimitive()
に渡されます。
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()
の呼び出し元はどのヒントを使用しますか?パラメータhint
には、3つの値のいずれかを取ることができます。
'number'
は、可能であればinput
を数値に変換する必要があることを意味します。'string'
は、可能であればinput
を文字列に変換する必要があることを意味します。'default'
は、数値と文字列のどちらにも優先順位がないことを意味します。さまざまな演算がToPrimitive()
を使用する方法の例をいくつか示します。
hint === 'number'
。次の演算は数値を優先します。ToNumeric()
ToNumber()
ToBigInt()
、BigInt()
<
)hint === 'string'
。次の演算は文字列を優先します。ToString()
ToPropertyKey()
hint === 'default'
。次の演算は、返されるプリミティブ値の型に関してニュートラルです。==
)+
)new Date(value)
(value
は数値または文字列のいずれかです)見てきたように、デフォルトの動作では'default'
は'number'
であるかのように扱われます。Symbol
とDate
のインスタンスのみが、この動作をオーバーライドします(後で示します)。
プリミティブへの変換がSymbol.toPrimitive
を介してオーバーライドされていない場合、OrdinaryToPrimitive()
は次の2つのメソッドのいずれか、または両方とも呼び出します。
'toString'
は、プリミティブ値を文字列にしたいというhint
が示されている場合に最初に呼び出されます。'valueOf'
は、プリミティブ値を数値にしたいというhint
が示されている場合に最初に呼び出されます。次のコードは、それがどのように機能するかを示しています。
const obj = {
toString() { return 'a' },
valueOf() { return 1 },
};
// String() prefers strings:
assert.equal(String(obj), 'a');
// Number() prefers numbers:
assert.equal(Number(obj), 1);
プロパティキーSymbol.toPrimitive
を持つメソッドは、通常のプリミティブへの変換をオーバーライドします。これは標準ライブラリでは2回だけ行われます。
Symbol.prototype[Symbol.toPrimitive](hint)
Symbol
のインスタンスの場合、このメソッドは常にラップされたシンボルを返します。Symbol
のインスタンスには文字列を返す.toString()
メソッドがあるためです。しかし、hint
が'string'
であっても、.toString()
は呼び出されるべきではありません。そうすることで、Symbol
のインスタンスを文字列(まったく異なる種類のプロパティキーです)に誤って変換してしまうことがないからです。Date.prototype[Symbol.toPrimitive](hint)
Date.prototype[Symbol.toPrimitive]()
これは、日付がプリミティブ値に変換される方法です。
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);
};
デフォルトのアルゴリズムとの唯一の違いは、'default'
が'string'
になり('number'
ではなく)、hint
を'default'
に設定する演算を使用した場合に観察できます。
==
演算子は、他のオペランドがundefined
、null
、boolean
以外のプリミティブ値の場合、オブジェクトをプリミティブ(デフォルトのヒント付き)に変換します。次のやり取りでは、日付の変換の結果が文字列であることがわかります。
+
演算子は、両方のオペランドをプリミティブ型に強制変換します(デフォルトのヒント付き)。結果のいずれかが文字列の場合、文字列連結を実行します(そうでない場合は数値加算を実行します)。次のインタラクションでは、日付の強制変換の結果が文字列であることがわかります。なぜなら、演算子は文字列を返しているからです。
ToString()
および関連する操作これは、ToString()
のJavaScriptバージョンです。
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);
}
}
この関数が、オブジェクトに対してプリミティブ結果を文字列に変換する前に、中間ステップとしてToPrimitive()
を使用している点に注目してください(A行)。
ToString()
は、String()
の動作と興味深い点で異なります。argument
がシンボルである場合、前者はTypeError
をスローしますが、後者はスローしません。なぜでしょうか?シンボルのデフォルトは、文字列への変換で例外をスローすることです。
> const sym = Symbol('sym');
> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string
そのデフォルトは、String()
とSymbol.prototype.toString()
でオーバーライドされています(両方とも次のセクションで説明します)。
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()
は、関数呼び出しを介して呼び出されるか、new
を介して呼び出されるかによって動作が異なります。new.target
を使用して、この2つを区別します。
これらは、ヘルパー関数StringCreate()
と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()
String()
に加えて、.toString()
メソッドを使用してシンボルを文字列に変換することもできます。その仕様は以下のとおりです。
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.toString
.toString()
のデフォルトの仕様は以下のとおりです。
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 + ']';
};
この操作は、プレーンオブジェクトを文字列に変換する場合に使用されます。
デフォルトでは、クラスのインスタンスを文字列に変換する場合にも使用されます。
通常、MyClass
の文字列表現を設定するために.toString()
をオーバーライドしますが、「object
」の後の文字列内の角括弧で囲まれた部分も変更できます。
class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
String(new MyClass()), '[object Custom!]');
.toString()
のオーバーライドバージョンとObject.prototype
の元のバージョンを比較すると興味深いです。
> ['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()
は、とりわけ、角括弧演算子で使用されます。その動作は以下のとおりです。
function ToPropertyKey(argument) {
let key = ToPrimitive(argument, 'string'); // (A)
if (TypeOf(key) === 'symbol') {
return key;
}
return ToString(key);
}
繰り返しますが、プリミティブ型で操作する前に、オブジェクトはプリミティブ型に変換されます。
ToNumeric()
および関連する操作ToNumeric()
は、とりわけ、乗算演算子(*
)で使用されます。その動作は以下のとおりです。
function ToNumeric(value) {
let primValue = ToPrimitive(value, 'number');
if (TypeOf(primValue) === 'bigint') {
return primValue;
}
return ToNumber(primValue);
}
ToNumber()
ToNumber()
は、以下のとおり動作します。
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);
}
}
ToNumber()
の構造は、ToString()
の構造と似ています。
+
)JavaScriptの加算演算子の仕様は以下のとおりです。
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)
}
このアルゴリズムの手順
Type()
は、lnum
のECMAScript仕様の型を返します。.add()
は数値型のメソッドです。==
)/** 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;
}
次の操作はここでは示されていません。
JavaScriptの型強制変換の仕組みを詳しく見てきたので、型変換に関する用語の簡単な用語集で締めくくりましょう。
型変換では、出力値が特定の型になるようにします。入力値が既にその型である場合は、変更せずにそのまま返されます。そうでない場合は、目的の型を持つ値に変換されます。
明示的な型変換とは、プログラマが操作(関数、演算子など)を使用して型変換をトリガーすることを意味します。明示的な変換は次のようになります。
型キャストとは、プログラミング言語によって異なります。たとえば、Javaでは、チェック付きの明示的な型変換です。
型強制変換は暗黙的な型変換です。操作は、必要な型に引数を自動的に変換します。チェック付き、チェックなし、またはその中間の場合があります。
[出典:Wikipedia]