やあ。今日もコードと格闘しているかい?
フロントエンドの現場にいると、「なんかこのコード、意図せず文字列結合されちゃうな……」「`NaN`にならないはずなのに、なんでだ?」なんてバグに遭遇することがあるよね。
大抵の場合、犯人はJavaScriptの暗黙の型変換(Coercion)だ。
そして、その裏側でこっそり主導権を握っているのが、今回深掘りする `valueOf` と `toString` という2つのメソッドさ。
公式ドキュメントを読んでも「オブジェクトをプリミティブ値に変換するときに云々……」としか書いてなくて、ぶっちゃけ「で、どっちが先に呼ばれるの?」ってモヤッとした経験、ない?
今回は、中級からもう一歩先に進みたい君に向けて、ブラウザのエンジンが裏側でどう動いているのか、その残酷で美しいアルゴリズムを叩き込んでやろう。
—
なぜ `valueOf` と `toString` が必要なのか?
JavaScriptには、`number`や`string`といった「プリミティブ型」と、`Object`や`Array`といった「参照型(オブジェクト)」があるよね。
問題は、オブジェクトを「数値として扱いたい場面(例:`obj + 1`)」や「文字列として扱いたい場面(例:`console.log(‘result: ‘ + obj)`)」に直面したときだ。
オブジェクトはそのままでは計算も表示もできない。だから、JavaScriptエンジンは「こいつを何とかしてプリミティブな値(数か文字列か)に変換せねば!」と焦る。
そのときに召喚されるのが、オブジェクトのプロテイン……じゃなくてプロトタイプに潜む、`valueOf` と `toString` という2人の使者なわけだ。
変換アルゴリズムの核心:ToPrimitive
仕様(ECMAScript)の言葉を借りると、オブジェクトをプリミティブに変換する内部処理を `ToPrimitive` と呼ぶ。
この `ToPrimitive` が実行される際、変換先の「ヒント(PreferredType)」が渡される。ヒントは主に以下の3パターンだ。
1. `string`:文字列にしたい(例:テンプレートリテラル、`String()`関数)
2. `number`:数値にしたい(例:算術演算子 `+`(単項以外), `-`, 比較演算子 `<` など)
3. `default`:どっちでもいい(例:二項演算子の `+` や `==`)
この「ヒント」によって、`valueOf` と `toString` のどちらを先に呼び出すかの優先順位が劇的に変わる。ここが今日の最重要ポイントだ。
ルール1:ヒントが `string` の場合
1. `toString()` を呼ぶ。もしそれがプリミティブを返せば、それを採用!
2. ダメなら(オブジェクトを返したら)、ชม`valueOf()` を呼ぶ。
3. どっちもダメなら `TypeError` をスローする。
ルール2:ヒントが `number` または `default` の場合
1. `valueOf()` を呼ぶ。もしそれがプリミティブを返せば、それを採用!
2. ダメなら、`toString()` を呼ぶ。
3. どっちもダメなら `TypeError` をスローする。
……おや? `default` のときは `number` と同じ扱いになるんだ。
つまり、特別な指定がない限り、JavaScriptはまず `valueOf` で数値としての可能性を探り、ダメなら `toString` で文字列に逃げるというデフォルト戦略をとっている。
—
現場で検証:コードで挙動を暴く
百聞は一見にしかず。実際にカスタムオブジェクトを作って、どっちがどの順番で呼ばれているかコンソールに出力してみよう。
// 挙動を追跡するためのカスタムオブジェクト
const cleverObj = {
valueOf() {
console.log(‘-> valueOf() が呼ばれました’);
// 検証のために今回は数値を返す
return 42;
},
toString() {
console.log(‘-> toString() が呼ばれました’);
// 検証のために今回は文字列を返す
return ‘Hello from toString’;
}
};
// 1. 数値コンテキスト(ヒント: number)
console.log(‘— 1. 数値との足し算 —‘);
console.log(cleverObj + 10);
// 出力順: valueOf() -> 52
// 解説: 算術演算子「+」は数値コンテキストを好むため、まずvalueOfが呼ばれる。
// 2. 文字列コンテキスト(ヒント: string)
console.log(‘— 2. 文字列との結合 (String関数) —‘);
console.log(String(cleverObj));
// 出力順: toString() -> “Hello from toString”
// 解説: String()は明示的に文字列を要求するため、まずtoStringが呼ばれる。
// 3. デフォルトコンテキスト(ヒント: default)
console.log(‘— 3. テンプレートリテラル —‘);
console.log(`Value is: ${cleverObj}`);
// 出力順: toString() -> “Value is: Hello from toString”
// 解説: 意外かもしれないが、テンプレートリテラルや一部の演算は、
// 内部的にString化を優先するため toString() が先に走ることが多い。
この挙動、知っていると知らないとでは大違いだよね。
特にライブラリの内部実装や、独自のラッパーオブジェクト(例えば、金額を扱うMoneyオブジェクトなど)を設計する際には、この仕組みを理解していないと、意図しない型変換バグを踏み抜くことになる。
—
実務で役立つTips:valueOf/toStringのオーバーライド
実務において、自分で `valueOf` や `toString` をカスタム実装する機会はどんなときだろう?
例えば、「独自のラッパーオブジェクトをプリミティブな値のように直感的に比較・計算させたい」というケースだ。
以下のコードを見てほしい。金銭(Money)を扱うクラスの例だ。
class Money {
constructor(amount, currency = ‘JPY’) {
this.amount = amount;
this.currency = currency;
}
// 算術演算(<, >, +, – など)で数値として扱えるようにする
valueOf() {
return this.amount;
}
// 画面表示やログ出力の際に自然な文字列になるようにする
toString() {
return `${this.amount} ${this.currency}`;
}
}
const priceA = new Money(1500);
const priceB = new Money(800);
// 1. 比較演算子がシームレスに動く!
if (priceA > priceB) {
console.log(‘priceAの方が高いです’); // ちゃんと動く!
}
// 2. 四則演算もそのままいける!
const total = priceA + priceB;
console.log(total); // 2300 (数値として計算される)
// 3. テンプレートリテラルではtoStringが光る!
console.log(`合計金額: ${priceA}`); // 合計金額: 1500 JPY
どうだい? このように `valueOf` と `toString` を適切に設計してやると、オブジェクトでありながらプリミティブなプリミティブのように振る舞う、非常にエレガントなクラスを作ることができるんだ。
—
まとめとシニアからのアドバイス
JavaScriptの暗黙の型変換は、一見すると「バグの温床」として嫌われがちだ。`[] + {}` が `”[object Object]”` になるようなカオスな挙動を見ると、静的型付けに逃げたくなる気持ちもよくわかる。
しかし、言語の仕様を深く理解し、`ToPrimitive`、`valueOf`、そして `toString` の優先順位を頭に叩き込んでおけば、それは恐るべきものではなく、「コードを簡潔にするための強力な武器」に変わる。
チームメンバーから「なんでこの比較、うまくいくの?」と聞かれたときに、ドヤ顔でこのアルゴリズムを解説できるようになっておこうぜ。
それじゃあ、また次のコードレビュー会で!

コメント