こんにちは。チームのみんな、今日もコード書いてるかい?
フロントエンドをやっていると、画面から入力されたフォームのバリデーションや、URLのパース、さらにはクリップボードの操作など、なんだかんだで「文字列操作」に直面する時間は多いよね。`slice`や`replace`、最近の定番であるテンプレートリテラルなんかは、日々のコーディングで息をするように使っていることだと思う。
だがな、少しマニアックな領域、例えば「入力文字のバイト数を厳密に制限したい」「特殊な絵文字やサロゲートペアが含まれていないかチェックしたい」といった、プロダクトの品質を左右するシビアな要件にぶ1つあたった時、お前たちはどうする?
今回は、そんな時にこそ思い出してほしい、JavaScriptの隠れた実力派メソッド――`String.prototype.charCodeAt()` についてガッツリ解説しようと思う。ただのマニュアルの丸写しじゃない。ブラウザの裏側の動きや、実務で絶対にハマる「サロゲートペアの罠」まで含めて、シニアの視点から叩き込んでやるから、心して聞いてくれ。
—
1. `charCodeAt()` とは何か?(基本仕様の確認)
まずは基本のおさらいだ。`charCodeAt()` は、指定したインデックス(何番目か)にある文字の、UTF-16コードユニット(0から65535までの整数)を返すメソッドだ。
const str = “JavaScript”;
console.log(str.charCodeAt(0)); // 出力: 72 (‘J’ のUTF-16コード)
console.log(str.charCodeAt(4)); // 出力: 83 (‘S’ のUTF-16コード)
「なんだ、文字のASCIIコードやUnicode番号を取るだけの地味なやつか」と思ったそこの君。まあ待て。話はそう単純じゃないんだ。JavaScriptの内部で文字列がどう扱われているかを知ると、このメソッドの本当のヤバさが分かってくる。
—
2. ブラウザの裏側:JavaScriptは文字列をどう保持しているのか?
JavaScript(ECMAScript)の仕様において、文字列は UTF-16エンコードされたコードユニットの列 としてメモリ上に保持されている。
ここで重要なのが、すべての文字が「1文字 = 1つのコードユニット(2バイト)」で表現されるわけではない、という点だ。
初期のJavaScriptが設計された時代、Unicodeは16ビット(2バイト)あれば世界のすべての文字を表現できる(BMP: Basic Multilingual Plane)と考えられていた。しかし、世の中そんなに甘くなかった。漢字が増え、絵文字が生まれ、2バイト(65,856文字)じゃ収まりきらなくなった。
そこで登場したのが、2つのコードユニットをペアにして1文字を表現する 「サロゲートペア(Surrogate Pairs)」 という仕組みだ。
例えば、みんな大好きな絵文字の 🚀 (Rocket) や、一部の拡張漢字は、UTF-16では2つのコードユニット(合わせて4バイト)を使って表現される。
ここで `charCodeAt()` を使うとどうなるか?
「1文字目」を指定したつもりが、サロゲートペアの前半(上位サロゲート)しか取得できず、文字全体の正しいコードポイントが取れなくなってしまうんだ。これが実務でよくあるバグの温床になる。
—
3. 実務で遭遇する「サロゲートペアの罠」と回避策
例えば、ユーザーが名前に絵文字を入力してきたとする。フロントエンドで「入力文字数は最大10文字まで!」というバリデーションを組んだとしよう。
安易に `str.length` を使ったり、ループで `charCodeAt()` を回したりすると、サロゲートペアの文字を「2文字」としてカウントしてしまう。Twitter(現X)の文字数カウンターなんかを想像してほしい。絵文字を入れると残り文字数が狂う現象、あれの裏側にはこういう文字エンコーディングの泥臭い歴史がある。
現代のモダンな開発では、文字コードを直接ゴリゴリ触る機会は減ったが、「入力値の厳密なバイト数チェック」や「レガシーなAPIとのデータ連携で特定の文字コード範囲を弾く必要がある場合」などでは、今でも `charCodeAt()` は現役の武器になる。
実務で安全に使うためのパターンをコードで見ていこう。
実用スニペット:サロゲートペアを考慮した安全な文字コード走査
もし君が「文字列に含まれるすべての文字のコードポイントを正確に取得したい」という要件に直面したら、`charCodeAt()` ではなく、より新しい `codePointAt()` を使うべきケースが多い。だが、あえて `charCodeAt()` を組み合わせて、裏側の仕組みを理解しながら安全に処理するコードを書くならこうなる。
/
- 文字列内のすべての文字(サロゲートペアを含む)のコードポイントを安全に取得する関数
- @param {string} str – 対象の文字列
/
function getActualCharacterCodes(str) {
const codes = [];
for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); // 上位サロゲート(High Surrogate: 0xD800 〜 0xDBFF)の判定 if (code >= 0xD800 && code <= 0xDBFF) { // 次の文字が存在し、かつ下位サロゲート(Low Surrogate: 0xDC00 〜 0xDFFF)であるか確認 if (i + 1 < str.length) { const nextCode = str.charCodeAt(i + 1); if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) { // サロゲートペアを結合して、正しいUnicodeコードポイントを計算 const fullCodePoint = (code - 0xD800) 0x400 + (nextCode - 0xDC00) + 0x10000; codes.push({ type: 'SurrogatePair', code: fullCodePoint }); i++; // ペアの後半分をスキップするためインデックスを進める continue; } } } // 通常の文字(BMP内)の場合 codes.push({ type: 'Standard', code: code }); } return codes; } // --- 動作確認 --- const targetString = "A🚀漢字"; console.log(getActualCharacterCodes(targetString)); / 出力結果のイメージ: [ { type: 'Standard', code: 65 }, // 'A' { type: 'SurrogatePair', code: 128640 }, // '🚀' (正確なコードポイント) { type: 'Standard', code: 27704 }, // '漢' { type: 'Standard', code: 23398 } // '字' ] / どうだ? このコードを読めば、ブラウザが裏側でどうやって2つのコードユニットを結合しているのか、その泥臭い実態が手に取るように分かるはずだ。 ---
4. シニアからの実践的なアドバイス(Best Practices)
実務で `charCodeAt()` を扱う上での心構えをいくつか伝授しておこう。
1. 基本は現代的なAPIへ移行せよ
もし単に「絵文字を含めた正確な文字数や文字を取得したい」だけなら、`charCodeAt()` のような低水準なAPIを直接叩くのではなく、スプレッド構文 (`[…str]`) や `Array.from(str)`、あるいは `codePointAt()` を使う方が圧倒的にバグが少ない。コードの保守性も高まる。
2. 「文字コードによるバリデーション」が必要な時だけ使え
例えば、特定の半角記号や制御文字(ASCIIコントロールコードなど)が入力されていないかを正規表現ではなく、数値範囲で高速に弾きたい場合などに `charCodeAt()` は真価を発揮する。パフォーマンスがシビアな入力監視の現場では、正規表現よりも数値比較の方が圧倒的に高速なケースがある。
3. エッジケース(絵文字や外字)を常に念頭に置け
「日本語と英数字しか入りません」という仕様書は、往々にして現実のユーザーによって破壊される。ユーザーは容赦なく絵文字を名前にぶち込んでくる。文字列をインデックスでループさせる時は、「1文字 = 1インデックスとは限らない」という前提を常に頭の片隅に置いておくことだ。
—
おわりに
文字列操作はフロントエンドの基本中の基本だが、その奥底には今回解説したようなUTF-16の歴史やブラウザのメモリ管理といった、エンジニアとしての「深み」が隠されている。
単に動くコードを書くだけなら誰でもできる。だが、「なぜこのメソッドを使うのか」「裏側で何が起きているのか」を理解しているエンジニアは、いざトラブルが起きた時にビクともしない。
日々のコーディングで使う小さなメソッド一つをとっても、こうした背景に思いを馳せられるようになってほしい。お前たちのコードが、より堅牢で美しいものになることを期待している。
さて、次のチケットに取りかかろうか!

コメント