Edited at

なぜ絵文字を含む文字を1文字ずつに分けるのにArray.fromだけで十分なのか?

More than 3 years have passed since last update.

「Node.js で絵文字を使う時の注意点 - Qiita」にコメントしたのですが、大雑把すぎて説明になっていない気がするので、詳細に解説したいと思います。


TL;DR


  • 絵文字のようなサロゲートペアを含む文字列をJavaScriptで扱おうとすると面倒。

  • s.lengths[0] とか s.split('') とかがおかしくなる。

  • 1文字ずつに分割したいときは Array.from が使える。

  • なぜなら、Array.from は内部的に for of 文のようなことをしていて、文字列に対する for of はサロゲートペアを考慮するから。


問題点と解決まで

絵文字を含む文字、つまりUTF-16のサロゲートペアを含むような文字を一文字ずつに分割したい場合、JavaScriptでは問題が生じます。つまり、どういうことかというと、

const kira = 'キラッ🌠' // 見ため上は4文字

console.log(kira.length) //=> 5 (けれど `length` は5文字ってことになる)

というように、サロゲートペアが2文字文としてカウントされてしまい、

kira.split('').forEach(s => console.log(s))

//=> キ
//=> ラ
//=> ッ
//=> ?(変な文字)
//=> ?(変な文字)

のように、上手く1文字ずつ分割することもできなくなってしまいます。昔の人々はこれに対処するためになんかすごい正規表現を生み出したらしいのですが、ES2015環境においてはたった一つのメソッドによって解決されてしまいます。

それが Array.from です。このメソッドは arguments を配列にしたり、DOM要素のリストを配列にしたりするのによく使われるような気がしますが、サロゲートペアを含む文字列を1文字ずるに分割することにも使うことができるのです。

Array.from(kira).forEach(s => console.log(s))

//=> キ
//=> ラ
//=> ッ
//=> 🌠

それでは、なぜ Array.from で1文字ずつに分割できるのか、というのがこの記事の主題になります。


Array.from とは?

ES2015で Array に追加されたメソッドの1つです。(もう一つは Array.of

MDNによると、引数として与えた配列っぽいオブジェクトや反復可能オブジェクトを配列に変換するメソッドのようです。

まず、配列っぽいオブジェクト(array-like)というのは、



  • length プロパティがある


  • obj[0], obj[1] のようにして各要素にアクセスできる

ようなオブジェクトのことになります。このようなオブジェクトを配列に変換する、というのはこんなコードになります。

function arrayLikeToArray(obj) {

const length = obj.length
const array = []
for (let i = 0; i < length; i++) {
array.push(obj[i])
}
return array
}
// 本当は `Array.of.apply(null, obj)` で十分

しかし、Stringlength プロパティや obj[0] のようなアクセスはサロゲートペアを考慮しないので、Array.from の内部で文字列は反復可能オブジェクトとして扱われていると予想できます。

実際に、Array.fromは反復可能オブジェクトかどうかを試してから、そうでないときに配列のようなオブジェクトとして配列に変換することを試みます。

しかし、反復可能オブジェクトとは一体どのようなオブジェクトなのでしょうか?

これには一言で答えることができます。

for of 文でループすることができるようなオブジェクトのことです。


for of 文とは?

こちらはES2015にて追加された構文の一つです。for in と似ているのですが、 for in はプロパティ名を反復するのに対し for of はプロパティの値を反復するとMDNには書かれています

なので、for of を使って配列を作るにはこのようにすればいいです。

function iterableToArray(obj) {

const array = []
for (let v of obj) {
array.push(v)
}
return array
}
// 本当は `[...obj]` で十分

for of、というかES2015のイテレーターについては他に優秀な説明記事がいくつもあるので説明を譲るとして、ここでは Stringfor of 文でループさせたときの挙動について説明します。

ここまでの流れをくめば想像しやすいことですが、文字列を for of に渡した場合、サロゲートペアも考慮して1文字ずつ反復していきます。

for (let c of kira) {

console.log(c)
}
//=> キ
//=> ラ
//=> ッ
//=> 🌠

なので、Array.from でサロゲートペアを考慮して文字列を1文字ずつに分割することができるわけです。


あとがき

書いている途中で気付いたのですが、Array.fromじゃなくてspread operatorを使って、[...kira] のように書いた方が短かいです。

また、サロゲートペアを含めた1文字だけを取得したいときは String.prototype.codePointAt が、 絵文字などをUCS-2のコードポイントから構築したいときは String.fromCodePoint が使えます。→参考

それと、サロゲートペアを考慮する、というのは要するにUTF-16文字列をUCS-2のコードポイント毎で考えているということです。Unicodeの結合文字やIVSを考慮してくれるわけではありません。

この文章で自分が言いたかったことは「Array.from を単に、配列っぽオブジェクトを配列にすると考えても反復可能オブジェクトを配列にする、と考えても問題だ」っていうことで、実際のところタイトルにあるような内容は別にどうでもよかったりします。

ES2015っぽいコードで Array.prototype.slice.call(arguments) とかを見るとげんなりするので、世界平和のためにも Array.from とかspread operatorを使ってやってください。

こんな長い記事に最後までお付き合いいただきありがとうございました。