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.のRequireObjectCoercible
はNull
かUndefined
であれば例外をスローし、それ以外であれば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.に遷移する。IsPropertyReference
はV
がObjectもしくはPrimitiveであればtrue
を返す。さらにHasPrimitiveBase
もtrue
になる。そして'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
はオブジェクト型で、b
はString
型だからね。厳密性を考えるとfalse
のほうが正しいようにも思える。どちらを使うかはケース・バイ・ケースかなあと思うけど、通常は文字列に関しては==
演算子を使っておくほうが自然に思えるね。