はじめに
JavaScriptにおける文字列の切り出しをまとめました。
substr
この機能は非推奨とされているので新たにこの機能を採用するのは避けてください。
Stringオブジェクトに組み込まれたメソッドの一つで、指定した位置から指定した文字数だけ文字列切り出してくれる関数です。
typescriptで定義された型は以下のようになっています。
String.substr(from: number, length?: number): string
文字列オブジェクトから呼び出されるメソッドで、文字列をfromで指定した場所からlengthで指定した分を切り出して返却する関数です。
lengthは任意で渡すことができ、何も渡さなかった場合はfromで指定した以降の文字列を全て取得することができます。負の値、NaNは0として扱われます。
fromが正の値の場合は文字列の先頭から数えた位置から切り出します。String.length + 1(文字列の長さ+1)を超える値を指定した場合は空の文字列を返します。負の値の場合は文字列の末尾から数えた位置から切り出します。String.length(文字列の長さ)以下の場合は最初の文字列から切り出します。NaNであった場合も同様に最初の文字から切り出されます。
例
fromとlengthが捻りなく与えられた場合
'qiita'.substr(2, 3) // 'ita'
fromだけが与えられた場合
'qiita'.substr(2) // 'ita'
fromに負の値を与えた場合
'qiita'.slice(-2) // 'ta'
lengthにNaNを与えた場合
'qiita'.substr(2, NaN) // ''
lengthに文字列の長さを超える値を与えた場合
'qiita'.substr(2, 1000) // 'ita'
lengthに負の値を与えた場合
'qiita'.substr(2, -100) // ''
fromに文字列の長さを超える値を与えた場合
'qiita'.substr(100) // ''
fromに絶対値が文字列の長さを超える負の値を与えた場合
'qiita'.substr(-100) // 'qiita'
fromにNaNを与えた場合
'qiita'.substr(NaN) // 'qiita'
参考
substring
この機能もStringオブジェクトに組み込まれたメソッドの一つです。指定した位置から指定した位置まで文字列切り出してくれる関数です。
typescriptで定義された型は以下のようになっています。
String.substring(start: number, end?: number): string
substrと比べると、命名を除けば同じ型となっています。比べてみるとfromがstartにlengthがendになっています。引数名からも察しがつきますが、startは切り出し始めの位置、endは終了位置の直後を表す値となっています(endの位置の文字列は切り出されません)。
endを指定しなかった場合はstart以降の文字列が切り出されます。また、負の値やNaNは0として扱われます。
startがendよりも小さい場合はこれらの値が交換された結果が返ってきます。つまり、startがendとして、endがstartとして扱われることもあるということです。おそらくこのことからstartでも負の値やNaNは0として扱われます。
例
startとendが捻りなく与えられた場合
'qiita'.substr(2, 3) // 'i'
startだけが与えられた場合
'qiita'.substr(2) // 'ita'
startに負の値を与えた場合
'qiita'.slice(-2) // 'qiita'
endにNaNを与えた場合
'qiita'.substr(2, NaN) // 'qi'
endに文字列の長さを超える値を与えた場合
'qiita'.substr(2, 1000) // 'ita'
endに負の値を与えた場合
'qiita'.substr(2, -100)// 'qi'
startに文字列の長さを超える値を与えた場合
'qiita'.substr(100) // ''
startに絶対値が文字列の長さを超える負の値を与えた場合
'qiita'.substr(-100) // 'qiita'
startにNaNを与えた場合
'qiita'.substr(NaN) // 'qiita'
参考
slice
この機能もStringオブジェクトに組み込まれたメソッドの一つです。指定した位置から指定した位置まで文字列切り出してくれる関数です。
String.slice(start?: number, end?: number): string;
これまでの説明はstartが任意である点を除けばsubstringと全く同じです。しかし、この二つのメソッドの違いはstartが任意になっただけではありません。
startからendの手前までの文字列が切り出されること、NaNであった場合は0として扱われることは同じですが、負の数であった場合の挙動は異なります。負の値であったときは末尾から絶対値分遡った位置から文字列が切り出されるようになります。数式で書くとmax(String.length + start, 0)になります。0から文字列の長さの幅になります。
また、startがendよりも小さい場合でも交換されることはなく空文字列が返ってきます(負の値の場合はsubstringでは0とするのに対して、sliceでは末尾から遡った値とするので非自明な挙動になることが交換されない理由だと思います)。
startもendも指定しなかった場合は文字列をそのまま返します。Stringはイミュータブルですので、startとendを指定せずに使うことはないと考えられます。余談ですがArrayにも同様にsliceメソッドがあります。Arrayはミュータブルであり通常の代入(const a = b)では参照元が同じになってしまいます。それを防ぐためにシャローコピーをしたい場合は引数なしでsliceを使って行うことができます。ただ、意味論的にsliceを用いるよりはスプレット構文を用いるべきだと考えています。
例
startとendが捻りなく与えられた場合
'qiita'.slice(2, 3) // 'i'
startだけが与えられた場合
'qiita'.slice(2) // 'ita'
startに負の値を与えた場合
'qiita'.slice(-2) // 'ta'
endにNaNを与えた場合
'qiita'.slice(2, NaN) // 'qi'
endに文字列の長さを超える値を与えた場合
'qiita'.slice(2, 1000) // 'ita'
endに負の値を与えた場合
'qiita'.slice(2, -100) // ''
startに文字列の長さを超える値を与えた場合
'qiita'.slice(100) // ''
startに絶対値が文字列の長さを超える負の値を与えた場合
'qiita'.slice(-100) // 'qiita'
startにNaNを与えた場合
'qiita'.slice(NaN) // 'qiita'
参考
charAt
Stringオブジェクトに組み込まれたメソッドの一つです。指定した位置の文字を取得する関数です。
String.charAt(pos: number): string;
posが0からString.length-1(文字列の長さ-1)でない数値の場合はから文字列を取得します。一方posがNaNの場合は最初の文字列を取得します(pos=0として扱われます)。
例
posが捻りなく与えられた場合
'qiita'.charAt(3) // 't'
posに文字列の長さを与えられた場合
'qiita'.charAt('qiita'.length) // ''
posに負の値を与えられた場合
'qiita'.charAt(-1) // ''
posにNaNを与えられた場合
'qiita'.charAt(NaN) // 'q'
参考
全体を通して注意
これらのメソッドにおいて指定した数値は文字列の位置の指定や取得範囲の指定を行なっています。位置や範囲はString.lengthに基づいて指定されることに注意をする必要があります。例えば𠮷野家の𠮷は'𠮷'.lengthのようにすると2を得らます。そのため、'𠮷野家'の野屋を取得するために直感に従って'𠮷野家'.slice(1, 3)としても'\uDFB7野'が返されます。𠮷の長さが2であることが原因なので'𠮷野家'.slice(2, 4)とする必要があるのです。これが起きる原因はlengthメソッドが文字列内のコード単位の数を返しているからです。𠮷は\uD842と\uDFB7の2つのコード単位を持つのでこのようになっています。他にも竈門禰󠄀豆子の禰󠄀や👍など身の回りの文字でも起こる現象となっているので注意したいです。今のところこれを解決するには泥臭い方法しかわからず、スマートな切り出し方法がわからないのでご存じであればコメントいただけると幸いです。