2024年3月31日

プリミティブ型へのオブジェクト変換

オブジェクトが加算obj1 + obj2、減算obj1 - obj2される場合、またはalert(obj)を使用して出力される場合はどうなりますか?

JavaScriptでは、オブジェクトに対する演算子の動作をカスタマイズすることはできません。 RubyやC++など、他のプログラミング言語とは異なり、加算(または他の演算子)を処理するための特別なオブジェクトメソッドを実装することはできません。

このような操作の場合、オブジェクトは自動的にプリミティブ型に変換され、その後、これらのプリミティブ型に対して操作が実行され、プリミティブ値が生成されます。

これは重要な制限です。obj1 + obj2(または他の数学演算)の結果は、別のオブジェクトになることはできません!

たとえば、ベクトルや行列(または実績など)を表すオブジェクトを作成し、それらを追加して、結果として「合計」オブジェクトを期待することはできません。このようなアーキテクチャ上の偉業は自動的に「不可能」になります。

そのため、技術的にはここではあまりできないため、実際のプロジェクトではオブジェクトを使った数学演算はありません。それが起こる場合、まれな例外を除いて、それはコーディングミスによるものです。

この章では、オブジェクトがプリミティブ型に変換される方法と、それをカスタマイズする方法について説明します。

私たちには2つの目的があります

  1. このような操作が誤って発生した場合、コーディングミスの際に何が起こっているのかを理解することができます。
  2. このような操作が可能で、見栄えの良い例外があります。たとえば、日付(Dateオブジェクト)の減算や比較です。それらについては後で説明します。

変換ルール

型変換の章では、プリミティブ型の数値、文字列、およびブール値への変換のルールについて説明しました。しかし、オブジェクトについては説明を省略しました。メソッドとシンボルについて知ったので、それを埋めることができます。

  1. ブール値への変換はありません。すべてのオブジェクトは、ブール値コンテキストでは、単純にtrueです。数値変換と文字列変換のみが存在します。
  2. 数値変換は、オブジェクトを減算したり、数学関数を実行したりするときに発生します。たとえば、日付と時刻の章で説明するDateオブジェクトは減算でき、date1 - date2の結果は2つの日付の時刻差です。
  3. 文字列変換に関しては、通常、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つのオブジェクトメソッドを見つけて呼び出そうとします

  1. obj[Symbol.toPrimitive](hint)を呼び出します。シンボリックキーSymbol.toPrimitive(システムシンボル)を持つメソッドが存在する場合、
  2. それ以外の場合は、ヒントが"string"の場合
    • obj.toString()またはobj.valueOf()を呼び出してみてください。どちらか存在する方を呼び出します。
  3. それ以外の場合は、ヒントが"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はメソッドtoStringvalueOfを見つけようとします

  • "string"ヒントの場合:toStringメソッドを呼び出し、それが存在しない場合、またはプリミティブ値ではなくオブジェクトを返す場合は、valueOfを呼び出します(そのため、toStringは文字列変換の優先順位が高くなります)。
  • 他のヒントの場合:valueOfを呼び出し、それが存在しない場合、またはプリミティブ値ではなくオブジェクトを返す場合は、toStringを呼び出します(そのため、valueOfは数学演算の優先順位が高くなります)。

メソッドtoStringvalueOfは古代から来ています。それらはシンボルではなく(シンボルはそれほど昔には存在していませんでした)、むしろ「通常の」文字列で名前が付けられたメソッドです。それらは、変換を実装するための代替の「古いスタイルの」方法を提供します。

これらのメソッドはプリミティブ値を返さなければなりません。 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の代わりにtoStringvalueOfの組み合わせを使用して、上記と同じことを行います

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.toPrimitivevalueOfがない場合、toStringはすべてのプリミティブ変換を処理します。

変換は任意のプリミティブ型を返すことができます

すべてのプリミティブ変換メソッドについて知っておくべき重要なことは、それらが必ずしも「ヒント」されたプリミティブを返すとは限らないということです。

toStringが正確に文字列を返すかどうか、またはSymbol.toPrimitiveメソッドがヒント"number"に対して数値を返すかどうかは制御できません。

唯一の必須事項:これらのメソッドは、オブジェクトではなく、プリミティブを返さなければなりません。

歴史的背景

歴史的な理由から、toStringまたはvalueOfがオブジェクトを返す場合、エラーはありませんが、そのような値は無視されます(メソッドが存在しないかのように)。これは、古代のJavaScriptには適切な「エラー」の概念がなかったためです。

対照的に、Symbol.toPrimitiveはより厳格であり、プリミティブを返さなければなりません。そうでない場合はエラーが発生します。

さらなる変換

すでにわかっているように、多くの演算子と関数は型変換を実行します。たとえば、乗算*はオペランドを数値に変換します。

オブジェクトを引数として渡すと、計算には2つの段階があります

  1. オブジェクトはプリミティブに変換されます(上記のルールを使用して)。
  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
  1. 乗算obj * 2は、最初にオブジェクトをプリミティブ(文字列"2")に変換します。
  2. 次に、"2" * 22 * 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"と同じ方法で実装します)

仕様では、どの演算子がどのヒントを使用するかを明示的に説明しています。

変換アルゴリズムは次のとおりです

  1. メソッドが存在する場合は、obj[Symbol.toPrimitive](hint)を呼び出します。
  2. それ以外の場合は、ヒントが"string"の場合
    • obj.toString()またはobj.valueOf()を呼び出してみてください。どちらか存在する方を呼び出します。
  3. それ以外の場合は、ヒントが"number"または"default"の場合
    • obj.valueOf()またはobj.toString()を呼び出してみてください。どちらか存在する方を呼び出します。

これらのメソッドはすべて、機能するにはプリミティブを返さなければなりません(定義されている場合)。

実際には、オブジェクトの「人間が読める」表現をログ記録またはデバッグの目的で返す必要がある文字列変換の「キャッチオール」メソッドとして、obj.toString() のみを実装すれば十分な場合がよくあります。

チュートリアルマップ

コメント

コメントする前にこれをお読みください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを使用し、複数行のコードの場合は <pre> タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen など)を使用してください。