StringオブジェクトとString型

公開:2020-03-15 18:21
更新:2020-03-16 06:05
カテゴリ:javascript,string

JSの文字列

これはJSを嗜むものにとって当たり前のことかもしれないが。

let literal = 'aaa';
let obj = new String('aaa');

という2つの変数を定義する。このとき

literal instanceof String;

の結果はどうなるだろうか。結果はfalseである。また、

obj instanceof String;

の結果はどうだろうか。結果はtrueである。

次に、

typeof literal;

の結果はどうだろうか。結果は"string"である。そして

typeof obj

の結果はobjectである。

普段はあまりStringオブジェクトとStringリテラルの違いを意識することはない。が、「変数が文字列であるかどうか」を確認するようなコードを書かなければならなくなったとき、単純にinstanceof Stringでは駄目だということがわかる。正しく文字列かどうかを判別するには以下のようなコードになるだろう。

function isString(v){
  return ((typeof v) == 'string') || (v instanceof String);
}

しかしリテラルであってもStringオブジェクトで定義されているメソッドは使える。

'aaa'.substr(1)
// 'aa'

これはリテラルからオブジェクトへの暗黙変換(自動変換)が起こっているのではないかと推測できる。さらに以下を考える。

let literal_substring = 'aaa'.substr(1);
let obj_substring = new String('aaa').substr(1);

この場合のそれぞれのtypeofの結果を見てみる。リテラルにメソッドを適用したとき、JS内部で暗黙的にStringへの変換が起こっていると考えると、私は両方がobjectになるのではないかと思ったが違った。結果はstringである。またinstanceofの結果はどうだろうか。結果はfalseである。つまりStringのメソッドは結果を文字列で返す場合、Stringオブジェクトで返すのではなく文字列リテラルで返すようだ。リテラル→オブジェクト→リテラルの変換が内部的に起こっていると考えられる。

仕様書を読む

ちょっとここまで考えたところで、文字列周りの挙動に興味を持ったので仕様書を読んでみることにした。

ECMAScript® 2019 Language Specification

JSの内部では文字列はすべてString型の値で保持される。

そしてこの文字列を操作するためのビルトイン・コンストラクタ(オブジェクト)としてStringがある。これはC言語におけるstring.hで定義される文字列操作関数をオブジェクトにパッケージしたようなものである。

そしてもう一つ、JSにはビルトインでString関数があり、これは値をString型の値に変換するユーティリティである。

// String Primitive Value
let a = 'aaaa';
// Stringコンストラクタ
let b = new String('aaa');
// [String: 'aaa']
// String関数
let b = String(b);
// 'aaa'
let a = String(10);
// '10'

この時点で私はStringコンストラクタとString関数を混同していることがわかった。。

つぎにStringコンストラクタで生成されるオブジェクトに用意されているメソッドの動きを眺めてみた。例えばsubstringは以下であった。

1. Let O be ? RequireObjectCoercible(this value).
2. Let S be ? ToString(O).
3. Let len be the length of S.
4. Let intStart be ? ToInteger(start).
5. If end is undefined, let intEnd be len; else let intEnd be ? ToInteger(end).
6. Let finalStart be min(max(intStart, 0), len).
7. Let finalEnd be min(max(intEnd, 0), len).
8. Let from be min(finalStart, finalEnd).
9. Let to be max(finalStart, finalEnd).
10. Return the String value whose length is to - from, containing code units from S, namely the code units with indices from through to - 1, in ascending order.

1.のRequireObjectCoercibleNullUndefinedであれば例外をスローし、それ以外であればObjectのthis(O)をそのまま返すというものである。そして2.でToStringを呼び出してOを文字列化し、substringの処理を行い、最後の10でString型の値を返している。

とすると、以下のようなコードは動くはずだと思って試すと動いた。おお、すごい。。

String.prototype.substring.call(10,0,1)
// '1'

がしかし

(10).substring(0,1)
// Uncaught TypeError: 10.substring is not a function

というのは動作しない。まあこれは当たり前かなと思う。しかしString型の値の場合は動作する。

'10'.substring(0,1)
// '1'

この暗黙な型変換はいつどこで行わるのか。おそらくプロパティ・アクセサ(.)で行われているのではないか。ということで、"."の評価を追ってみた。

Propery AccessorのRuntime Semantics : Evaluationには以下の流れで処理されるとある。

1. Let baseReference be the result of evaluating MemberExpression.
2. Let baseValue be ? GetValue(baseReference).
3. Let bv be ? RequireObjectCoercible(baseValue).
4. Let propertyNameString be StringValue of IdentifierName.
5. If the code matched by this MemberExpression is strict mode code, let strict be true, else let strict be false.
6. Return a value of type Reference whose base value component is bv, whose referenced name component is propertyNameString, and whose strict reference flag is strict.

まず1.でプロパティのベース部分、先ほどの例でいうと'aaa'.substring(1,1).から左側の部分を評価して、2.で'aaa'の値を取得するとある。次にGetValueを見てみる。

1. ReturnIfAbrupt(V).
2. If Type(V) is not Reference, return V.
3. Let base be GetBase(V).
4. If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
5. If IsPropertyReference(V) is true, then
  a. If HasPrimitiveBase(V) is true, then  
      i. Assert: In this case, base will never be undefined or null.  
      ii. Set base to ! ToObject(base).  
  b. Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).  
6. Else base must be an Environment Record,
  a. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).

今回のケースは5.に遷移する。IsPropertyReferenceVがObjectもしくはPrimitiveであればtrueを返す。さらにHasPrimitiveBasetrueになる。そして'ii. Set base to ! ToObject(base). 'に遷移し、'aaa'ToObjectによってStringオブジェクトに変換され、返される。この処理によって'aaa'Stringオブジェクトのメソッドを使えるようになるわけである。 "(10)"の場合、Numberオブジェクトに変換され、Numberオブジェクトはsubstringメソッドを持たないのでエラーが発生するのである。

まとめ

JavaScriptが持つ文字列型はString型の値であるが、これはプリミティブかつ値のみを持ち、文字列操作のメソッドを持たない。文字列操作を行う場合はStringオブジェクトに暗黙的に変換され処理され、再びString型の値に戻される。

しかし、Stringオブジェクト自体はString型の値で初期化してオブジェクトとして使用できる。 が、メソッドで返されるのはすべてString型の値となってしまう点には注意が必要かもしれない。

let s = new String('aaaa');
let b = s.substring(0);

s == b;
// true
s === b;
// false

上の例では、==演算子は暗黙の型変換を行うからtrueになる。厳密な等価演算子だとfalseになってしまうのは、暗黙な型変換を行わないからである。sはオブジェクト型で、bString型だからね。厳密性を考えるとfalseのほうが正しいようにも思える。どちらを使うかはケース・バイ・ケースかなあと思うけど、通常は文字列に関しては==演算子を使っておくほうが自然に思えるね。