[JavaScript] "10" + 1 は "101" だが "10" - 1 は 9 を理解する


そういうものなんです!仕様です!(完)

だけではあれなので、仕様をちゃんとみてみる

加算演算子(+)

12.8.3 The Addition Operator ( + )をみていきます。

NOTE The addition operator either performs string concatenation or numeric addition.

12.8.3.1 Runtime Semantics: Evaluation
AdditiveExpression:AdditiveExpression+MultiplicativeExpression

加算演算子は文字の連結と数値の加算を担うことがわかりました。
次に処理の流れをみていきましょう。

  1. Let lref be the result of evaluating AdditiveExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating MultiplicativeExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lprim be ? ToPrimitive(lval).
  6. Let rprim be ? ToPrimitive(rval).
  7. If Type(lprim) is String or Type(rprim) is String, then
    a. Let lstr be ? ToString(lprim).
    b. Let rstr be ? ToString(rprim).
    c. Return the string-concatenation of lstr and rstr.
  8. Let lnum be ? ToNumeric(lprim).
  9. Let rnum be ? ToNumeric(rprim).
  10. If Type(lnum) is different from Type(rnum), throw a TypeError exception.
  11. Let T be Type(lnum).
  12. Return T::add(lnum, rnum).

流れを確認すると7.で型チェックをしていますね。
どうやら評価する値がStringだった場合はToStringする仕様となっています。
そうでない場合はToNumericですね。

確認してわかったこと

加算演算子(+)は文字の連結と数値の加算の機能を担っている。
評価する値を型チェックして文字列か数値に変換している。

減算演算子(ー)

12.8.4 The Subtraction Operator ( - )をみていきます。

12.8.4.1 Runtime Semantics: Evaluation
AdditiveExpression:AdditiveExpression-MultiplicativeExpression

  1. Let lref be the result of evaluating AdditiveExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating MultiplicativeExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lnum be ? ToNumeric(lval).
  6. Let rnum be ? ToNumeric(rval).
  7. If Type(lnum) is different from Type(rnum), throw a TypeError exception.
  8. Let T be Type(lnum).
  9. Return T::subtract(lnum, rnum).

お気づきになったと思いますが、型チェックがないですね。
減算演算子には文字列連結の機能はないので型変換する必要がないということでしょう。
評価する値を数値に変換してそのまま処理していることがわかりました。

確認してわかったこと

減算演算子(ー)は数値の減算を担う機能。
評価する値を(型チェックせずに)数値に変換している。

まとめ

加算演算子(+)は、型チェックの結果、文字の連結として処理されるから"10" + 1 は "101"となる。
減算演算子(ー)は、数値の減算のみを行うから"10" - 1 は 9となる。

これで何故 "10" + 1 は "101" だが "10" - 1 は 9になるのかが確信を持って言えるようになりました!

これからはこんな挙動を質問されてもキリッと答えられますね!

"10" + 1       // => '101'
"10" - 1       // => 9
"10" + 1 - 1   // => 100
"10" + (1 - 1) // => "100"
"1" - 1 + "1"  // => "01"
1 - "1" + 1    // => 1

余談

「"10" + 1 は "101" だけど "10" - 1 は 9 なのなんでだろ
みたいな質問が流れてきたのがきっかけでした。

ちゃんと根拠を持って教えてあげようと思い、初めて仕様書リーディングに挑戦してみました。
こちらが、仕様書を読む勇気をもらった素晴らしい記事です。(唐突な宣伝。)
JavaScriptの「継承」はどう定義されるのか仕様書を読んで理解する - Qiita

いつもだったら、「キモい動きするなー、雑学として覚えておこう。」くらいで終わっていたと思います。やっぱり仕様は大事ですね。

追記

@suin さんが非常に有用な日本語訳をコメントしてくださいました。
本当にありがとうございます。
併せてお読みいただけるとさらに理解が深まります!
+/ー演算子 日本語訳へのリンク

参考