「Node.js で絵文字を使う時の注意点 - Qiita」にコメントしたのですが、大雑把すぎて説明になっていない気がするので、詳細に解説したいと思います。
TL;DR
- 絵文字のようなサロゲートペアを含む文字列をJavaScriptで扱おうとすると面倒。
- ↑
s.length
、s[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)` で十分
しかし、String
の length
プロパティや 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のイテレーターについては他に優秀な説明記事がいくつもあるので説明を譲るとして、ここでは String
を for 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を使ってやってください。
こんな長い記事に最後までお付き合いいただきありがとうございました。