オブジェクトが加算obj1 + obj2
、減算obj1 - obj2
される場合、またはalert(obj)
を使用して出力される場合はどうなりますか?
JavaScriptでは、オブジェクトに対する演算子の動作をカスタマイズすることはできません。 RubyやC++など、他のプログラミング言語とは異なり、加算(または他の演算子)を処理するための特別なオブジェクトメソッドを実装することはできません。
このような操作の場合、オブジェクトは自動的にプリミティブ型に変換され、その後、これらのプリミティブ型に対して操作が実行され、プリミティブ値が生成されます。
これは重要な制限です。obj1 + obj2
(または他の数学演算)の結果は、別のオブジェクトになることはできません!
たとえば、ベクトルや行列(または実績など)を表すオブジェクトを作成し、それらを追加して、結果として「合計」オブジェクトを期待することはできません。このようなアーキテクチャ上の偉業は自動的に「不可能」になります。
そのため、技術的にはここではあまりできないため、実際のプロジェクトではオブジェクトを使った数学演算はありません。それが起こる場合、まれな例外を除いて、それはコーディングミスによるものです。
この章では、オブジェクトがプリミティブ型に変換される方法と、それをカスタマイズする方法について説明します。
私たちには2つの目的があります
- このような操作が誤って発生した場合、コーディングミスの際に何が起こっているのかを理解することができます。
- このような操作が可能で、見栄えの良い例外があります。たとえば、日付(
Date
オブジェクト)の減算や比較です。それらについては後で説明します。
変換ルール
型変換の章では、プリミティブ型の数値、文字列、およびブール値への変換のルールについて説明しました。しかし、オブジェクトについては説明を省略しました。メソッドとシンボルについて知ったので、それを埋めることができます。
- ブール値への変換はありません。すべてのオブジェクトは、ブール値コンテキストでは、単純に
true
です。数値変換と文字列変換のみが存在します。 - 数値変換は、オブジェクトを減算したり、数学関数を実行したりするときに発生します。たとえば、日付と時刻の章で説明する
Date
オブジェクトは減算でき、date1 - date2
の結果は2つの日付の時刻差です。 - 文字列変換に関しては、通常、
alert(obj)
などでオブジェクトを出力するときに発生します。
特別なオブジェクトメソッドを使用して、文字列と数値の変換を自分で実装できます。
それでは、技術的な詳細に入りましょう。それがこのトピックを深く掘り下げる唯一の方法だからです。
ヒント
JavaScriptは、どの変換を適用するかをどのように決定しますか?
さまざまな状況で発生する3種類の型変換があります。 仕様書で説明されているように、これらは「ヒント」と呼ばれます。
"string"
-
オブジェクトから文字列への変換の場合、
alert
のように文字列を期待する操作をオブジェクトに対して実行しているとき// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
-
数学演算を実行しているときなど、オブジェクトから数値への変換の場合
// 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;
ほとんどの組み込み数学関数にも、このような変換が含まれています。
"default"
-
演算子が期待する型が「わからない」まれなケースで発生します。
たとえば、二項プラス
+
は、文字列(連結)と数値(加算)の両方で機能します。したがって、二項プラスが引数としてオブジェクトを取得する場合、"default"
ヒントを使用してそれを変換します。また、オブジェクトが
==
を使用して文字列、数値、またはシンボルと比較される場合、どの変換を実行する必要があるかも明確ではないため、"default"
ヒントが使用されます。// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
<
>
などの比較演算子は、文字列と数値の両方で使用できます。それでも、それらは"default"
ではなく、"number"
ヒントを使用します。これは歴史的な理由によるものです。
しかし実際には、物事はもう少し単純です。
1つのケース(Date
オブジェクト、後で学習します)を除くすべての組み込みオブジェクトは、"default"
変換を"number"
と同じ方法で実装します。そして、私たちもおそらく同じことをする必要があります。
それでも、3つのヒントすべてについて知ることが重要です。すぐに理由がわかります。
変換を実行するために、JavaScriptは3つのオブジェクトメソッドを見つけて呼び出そうとします
obj[Symbol.toPrimitive](hint)
を呼び出します。シンボリックキーSymbol.toPrimitive
(システムシンボル)を持つメソッドが存在する場合、- それ以外の場合は、ヒントが
"string"
の場合obj.toString()
またはobj.valueOf()
を呼び出してみてください。どちらか存在する方を呼び出します。
- それ以外の場合は、ヒントが
"number"
または"default"
の場合obj.valueOf()
またはobj.toString()
を呼び出してみてください。どちらか存在する方を呼び出します。
Symbol.toPrimitive
最初のメソッドから始めましょう。 Symbol.toPrimitive
という名前の組み込みシンボルがあり、次のように変換メソッドに名前を付けるために使用する必要があります
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"
};
メソッドSymbol.toPrimitive
が存在する場合、すべてのヒントに使用され、それ以上のメソッドは必要ありません。
たとえば、ここではuser
オブジェクトが実装しています
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
コードからわかるように、user
は変換に応じて自己記述的な文字列または金額になります。単一のメソッドuser[Symbol.toPrimitive]
がすべての変換ケースを処理します。
toString/valueOf
Symbol.toPrimitive
がない場合、JavaScriptはメソッドtoString
とvalueOf
を見つけようとします
"string"
ヒントの場合:toString
メソッドを呼び出し、それが存在しない場合、またはプリミティブ値ではなくオブジェクトを返す場合は、valueOf
を呼び出します(そのため、toString
は文字列変換の優先順位が高くなります)。- 他のヒントの場合:
valueOf
を呼び出し、それが存在しない場合、またはプリミティブ値ではなくオブジェクトを返す場合は、toString
を呼び出します(そのため、valueOf
は数学演算の優先順位が高くなります)。
メソッドtoString
とvalueOf
は古代から来ています。それらはシンボルではなく(シンボルはそれほど昔には存在していませんでした)、むしろ「通常の」文字列で名前が付けられたメソッドです。それらは、変換を実装するための代替の「古いスタイルの」方法を提供します。
これらのメソッドはプリミティブ値を返さなければなりません。 toString
またはvalueOf
がオブジェクトを返す場合、それは無視されます(メソッドがない場合と同じです)。
デフォルトでは、プレーンオブジェクトには次のtoString
およびvalueOf
メソッドがあります
toString
メソッドは文字列"[object Object]"
を返します。valueOf
メソッドはオブジェクト自体を返します。
デモは次のとおりです
let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
そのため、alert
などでオブジェクトを文字列として使用しようとすると、デフォルトで[object Object]
が表示されます。
デフォルトのvalueOf
は、混乱を避けるために、完全を期すためにのみここで言及されています。ご覧のとおり、オブジェクト自体を返すため、無視されます。なぜかと聞かないでください。それは歴史的な理由によるものです。そのため、存在しないと想定できます。
これらのメソッドを実装して変換をカスタマイズしましょう。
たとえば、ここではuser
は、Symbol.toPrimitive
の代わりにtoString
とvalueOf
の組み合わせを使用して、上記と同じことを行います
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
ご覧のとおり、動作はSymbol.toPrimitive
を使用した前の例と同じです。
多くの場合、すべてのプリミティブ変換を処理する単一の「キャッチオール」の場所が必要です。この場合、次のようにtoString
のみを実装できます
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
Symbol.toPrimitive
とvalueOf
がない場合、toString
はすべてのプリミティブ変換を処理します。
変換は任意のプリミティブ型を返すことができます
すべてのプリミティブ変換メソッドについて知っておくべき重要なことは、それらが必ずしも「ヒント」されたプリミティブを返すとは限らないということです。
toString
が正確に文字列を返すかどうか、またはSymbol.toPrimitive
メソッドがヒント"number"
に対して数値を返すかどうかは制御できません。
唯一の必須事項:これらのメソッドは、オブジェクトではなく、プリミティブを返さなければなりません。
歴史的な理由から、toString
またはvalueOf
がオブジェクトを返す場合、エラーはありませんが、そのような値は無視されます(メソッドが存在しないかのように)。これは、古代のJavaScriptには適切な「エラー」の概念がなかったためです。
対照的に、Symbol.toPrimitive
はより厳格であり、プリミティブを返さなければなりません。そうでない場合はエラーが発生します。
さらなる変換
すでにわかっているように、多くの演算子と関数は型変換を実行します。たとえば、乗算*
はオペランドを数値に変換します。
オブジェクトを引数として渡すと、計算には2つの段階があります
- オブジェクトはプリミティブに変換されます(上記のルールを使用して)。
- それ以上の計算に必要な場合、結果のプリミティブも変換されます。
例えば
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
- 乗算
obj * 2
は、最初にオブジェクトをプリミティブ(文字列"2"
)に変換します。 - 次に、
"2" * 2
は2 * 2
になります(文字列は数値に変換されます)。
二項プラスは、同じ状況で文字列を連結します。文字列を喜んで受け入れるためです
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
まとめ
オブジェクトからプリミティブへの変換は、値としてプリミティブを期待する多くの組み込み関数と演算子によって自動的に呼び出されます。
これには3つのタイプ(ヒント)があります
"string"
(alert
および文字列を必要とするその他の操作の場合)"number"
(数学演算の場合)"default"
(少数の演算子、通常オブジェクトは"number"
と同じ方法で実装します)
仕様では、どの演算子がどのヒントを使用するかを明示的に説明しています。
変換アルゴリズムは次のとおりです
- メソッドが存在する場合は、
obj[Symbol.toPrimitive](hint)
を呼び出します。 - それ以外の場合は、ヒントが
"string"
の場合obj.toString()
またはobj.valueOf()
を呼び出してみてください。どちらか存在する方を呼び出します。
- それ以外の場合は、ヒントが
"number"
または"default"
の場合obj.valueOf()
またはobj.toString()
を呼び出してみてください。どちらか存在する方を呼び出します。
これらのメソッドはすべて、機能するにはプリミティブを返さなければなりません(定義されている場合)。
実際には、オブジェクトの「人間が読める」表現をログ記録またはデバッグの目的で返す必要がある文字列変換の「キャッチオール」メソッドとして、obj.toString()
のみを実装すれば十分な場合がよくあります。
コメント
<code>
タグを使用し、複数行のコードの場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen など)を使用してください。