【JavaScript】文字列から任意の1文字を取得する方法|charAt・ブラケット記法・at()の違いと実務パターン

【JavaScript】文字列から任意の1文字を取得する方法|charAt・ブラケット記法・at()の違いと実務パターン JavaScript

JavaScriptで文字列から特定の位置にある1文字を取得するには、charAt()ブラケット記法 str[i]at()(ES2022)の3つの方法があります。

この記事では、3つの方法の違い・使い分けを比較表付きで解説し、charCodeAt / codePointAt との関係、サロゲートペアの注意点、実務でよく使うパターンまで体系的にまとめます。

スポンサーリンク

文字列から1文字を取得する3つの方法

JavaScriptでは、文字列の指定位置にある1文字を取得する方法が3つあります。それぞれの基本構文を確認しましょう。

1. charAt() メソッド

charAt() は文字列の指定位置の文字を返す、最も伝統的なメソッドです。

// 構文
string.charAt(index)

// 使用例
const str = 'JavaScript';

console.log(str.charAt(0));   // "J"
console.log(str.charAt(4));   // "S"
console.log(str.charAt(99));  // ""(範囲外は空文字)
console.log(str.charAt());    // "J"(引数省略時は0番目)

2. ブラケット記法 str[i]

配列のようにインデックスでアクセスする記法です。ES5以降のモダンな書き方として広く使われています。

// 構文
string[index]

// 使用例
const str = 'JavaScript';

console.log(str[0]);   // "J"
console.log(str[4]);   // "S"
console.log(str[99]);  // undefined(範囲外はundefined)

3. at() メソッド(ES2022)

at() はES2022で追加された最新のメソッドです。最大の特徴は負のインデックスに対応していることです。

// 構文
string.at(index)

// 使用例
const str = 'JavaScript';

console.log(str.at(0));    // "J"
console.log(str.at(-1));   // "t"(末尾の文字)
console.log(str.at(-3));   // "i"(末尾から3番目)
console.log(str.at(99));   // undefined(範囲外はundefined)

at() を使えば、従来の str.charAt(str.length - 1)str.at(-1) と簡潔に書けます。

at() のポリフィル(レガシー環境向け)

IE11など at() が使えない環境では、以下のポリフィルを追加することで対応できます。

// String.prototype.at ポリフィル
if (!String.prototype.at) {
  String.prototype.at = function(index) {
    const n = Math.trunc(index) || 0;
    const i = n >= 0 ? n : n + this.length;
    if (i < 0 || i >= this.length) return undefined;
    return this.charAt(i);
  };
}

3つの方法の違いを比較

3つの方法には、範囲外アクセス時の戻り値・負のインデックス対応・引数省略時の挙動などの違いがあります。

比較項目 charAt() str[i] at()
範囲外の戻り値 ""(空文字) undefined undefined
負のインデックス (空文字を返す) (undefinedを返す) (末尾から計算)
引数省略時 0番目の文字 —(構文エラー) 0番目の文字
導入時期 ES1(1997年〜) ES5(2009年〜) ES2022(2022年〜)
ブラウザ対応 全ブラウザ 全ブラウザ モダンブラウザ
おすすめ度 △ レガシー ◎ 最も一般的 ◎ 末尾アクセスに便利

範囲外アクセスの挙動の違い

範囲外アクセス時の戻り値の違いは、条件分岐に影響します。実際のコードで確認してみましょう。

const str = 'ABC';

// charAt() は空文字を返す
console.log(str.charAt(99));        // ""
console.log(str.charAt(99) === ''); // true

// ブラケット記法は undefined を返す
console.log(str[99]);               // undefined
console.log(str[99] === undefined); // true

// at() も undefined を返す
console.log(str.at(99));           // undefined

// 条件分岐での違い
if (str.charAt(99)) {
  // 空文字は falsy → 実行されない
}

if (str[99] !== undefined) {
  // undefined チェックで安全に判定できる
}

? 使い分けの指針

  • 通常のアクセス → ブラケット記法 str[i] が最もシンプル
  • 末尾からのアクセスat(-1) が簡潔で可読性が高い
  • 範囲外で空文字がほしい場合charAt() が便利

TypeScriptでの型の違い

TypeScriptを使っている場合、3つの方法で戻り値の型が異なる点に注意が必要です。

const str = 'Hello';

// charAt() → 常に string 型
const a: string = str.charAt(0);        // OK

// ブラケット記法 → string | undefined 型
const b: string = str[0];               // noUncheckedIndexedAccess が有効だとエラー
const b2: string | undefined = str[0]; // 安全な書き方

// at() → string | undefined 型
const c: string | undefined = str.at(0); // 正しい型

? TypeScript Tips

TypeScriptで noUncheckedIndexedAccess を有効にしている場合、ブラケット記法と at()string | undefined を返します。charAt() は常に string を返すため、型ガード不要で使えるメリットがあります。

charCodeAt / codePointAt との関係

「文字そのものを取得する」メソッドと「文字コードを取得する」メソッドは目的が異なります。それぞれの違いを理解しておくと、文字列操作の幅が広がります。

メソッド 戻り値 サロゲートペア対応 用途
charAt() 文字(string) 指定位置の文字を取得
charCodeAt() UTF-16コード(number) 文字のUTF-16コードを取得(0〜65535)
codePointAt() Unicodeコードポイント(number) Unicodeコードポイントを取得(0〜1114111)
at() 文字(string) 負のインデックス対応で文字を取得
const str = 'ABC';

// 文字そのものを取得
console.log(str.charAt(0));      // "A"(文字)

// UTF-16コードを取得
console.log(str.charCodeAt(0));  // 65("A"のUTF-16コード)

// Unicodeコードポイントを取得
console.log(str.codePointAt(0)); // 65(基本ラテン文字は同じ値)

// 文字コード → 文字に変換
console.log(String.fromCharCode(65));   // "A"
console.log(String.fromCodePoint(65));  // "A"

charCodeAt vs codePointAt の具体的な値の違い

基本的な文字(BMP内)では両者は同じ値を返しますが、サロゲートペア文字では大きく異なります。

文字 charCodeAt(0) codePointAt(0) 備考
A 65 65 同じ値
12354 12354 同じ値(BMP内)
? 55362(上位サロゲート) 134071(正しい値) サロゲートペア
? 55356(上位サロゲート) 127881(正しい値) サロゲートペア

サロゲートペア文字に注意

JavaScriptの文字列は内部的にUTF-16でエンコードされています。「?」(つちよし)や絵文字のような文字はサロゲートペア(2つのコードユニット)で表現されるため、charAt() やブラケット記法では正しく取得できない場合があります。

サロゲートペアで起きる問題

const str = '?野家';  // 「?」はサロゲートペア文字

// length も正しくカウントできない
console.log(str.length);          // 4(見た目は3文字なのに4)

// charAt() は孤立サロゲートを返してしまう
console.log(str.charAt(0));      // "ud842"(文字化け)
console.log(str.charAt(1));      // "udfb7"(文字化け)

// ブラケット記法も同様の問題
console.log(str[0]);            // "ud842"(文字化け)

// at() も同様
console.log(str.at(0));         // "ud842"(文字化け)

⚠️ 重要

charAt()str[i]at() はいずれもUTF-16のコードユニット単位で動作します。サロゲートペア文字(絵文字、一部の漢字など)を正しく扱うには、以下の方法を使いましょう。

解決策1: スプレッド構文で配列に変換

const str = '?野家';

// スプレッド構文はコードポイント単位で分割される
const chars = [... str];
console.log(chars);         // ["?", "野", "家"]
console.log(chars.length);  // 3(正しい文字数)
console.log(chars[0]);     // "?"(正しく取得できる)

解決策2: for…of ループ

const str = '?野家';

// for...of はコードポイント単位でイテレートする
for (const char of str) {
  console.log(char);
}
// "?"
// "野"
// "家"

解決策3: codePointAt() で正しいコードポイントを取得

const str = '?野家';

// charCodeAt は上位サロゲートのみを返す
console.log(str.charCodeAt(0));   // 55362(上位サロゲート 0xD842)

// codePointAt はサロゲートペアを正しく解釈する
console.log(str.codePointAt(0));  // 134071(正しいコードポイント U+20BB7)

// コードポイントから文字を復元
console.log(String.fromCodePoint(134071)); // "?"

解決策4: Array.from() で分割

const str = 'Hello?World';

// Array.from() もコードポイント単位で分割できる
const chars = Array.from(str);
console.log(chars.length);  // 11(正しい文字数)
console.log(chars[5]);     // "?"(正しく取得)

// 比較: split('') ではサロゲートペアが壊れる
const broken = str.split('');
console.log(broken.length); // 12(絵文字が2つに分割される)

// 正規表現の u フラグ付き split なら正しく分割できる
const correct = str.split(/(?:)/u);
console.log(correct.length); // 11(絵文字も1要素として分割)

サロゲートペア対応のまとめ

方法 サロゲートペア対応 コード例
charAt() / str[i] ✕ 壊れる str.charAt(0)
スプレッド構文 ○ 対応 [...str][0]
Array.from() ○ 対応 Array.from(str)[0]
for...of ○ 対応 for (const c of str)
codePointAt() ○ 対応 str.codePointAt(0)

Intl.Segmenter で書記素クラスタ単位の処理

スプレッド構文や for...of はサロゲートペアには対応しますが、結合文字(アクセント記号)や複合絵文字(?‍?‍?‍? など)は正しく扱えません。これらを「見た目通りの1文字」として扱うには Intl.Segmenter を使います。

const emoji = '?‍?‍?‍?'; // 家族絵文字(ZWJシーケンス)

// スプレッド構文では分解されてしまう
console.log([...emoji]);
// ["?", "‍", "?", "‍", "?", "‍", "?"](7要素に分解)

// Intl.Segmenter なら1文字として認識
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
const segments = [...segmenter.segment(emoji)];
console.log(segments.length);      // 1(正しく1文字)
console.log(segments[0].segment); // "?‍?‍?‍?"
// 結合文字の例
const cafe = 'café';  // "e" + 結合アクセント(U+0301)

console.log([...cafe]);       // ["c","a","f","e","́"](5要素に分解)

const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
const graphemes = [...segmenter.segment(cafe)].map(s => s.segment);
console.log(graphemes);       // ["c","a","f","é"](4文字として正しく認識)

? 文字の粒度と使い分け

  • ASCII文字のみcharAt() / str[i] で十分
  • 絵文字・漢字を含む → スプレッド構文 [...str] または for...of
  • 結合文字・複合絵文字を含むIntl.Segmenter を使用

文字列を1文字ずつ走査する方法の比較

文字列を1文字ずつ処理する方法は複数あります。目的に応じて最適な方法を選びましょう。

方法1: for文 + charAt() / ブラケット記法

const str = 'Hello';

// for文 + charAt()
for (let i = 0; i < str.length; i++) {
  console.log(str.charAt(i));
}

// for文 + ブラケット記法(こちらのほうが簡潔)
for (let i = 0; i < str.length; i++) {
  console.log(str[i]);
}

方法2: for…of(サロゲートペア対応)

const str = 'Hello?';

// for...of はコードポイント単位でイテレート
for (const char of str) {
  console.log(char);
}
// "H" "e" "l" "l" "o" "?"(絵文字も正しく1文字として処理)

方法3: スプレッド構文 + forEach

const str = 'Hello?';

// インデックスも必要な場合
[...str].forEach((char, index) => {
  console.log(`${index}: ${char}`);
});
// "0: H" "1: e" "2: l" "3: l" "4: o" "5: ?"

文字列走査方法の比較表

方法 サロゲートペア インデックス取得 break / continue
for + charAt()
for...of (手動カウント要)
[...str].forEach() (中断不可)
[...str].entries()

実務で使えるユースケース集

先頭文字・末尾文字の取得

const str = 'JavaScript';

// 先頭文字
const first = str[0];                      // "J"
const first2 = str.charAt(0);                // "J"

// 末尾文字
const last = str.at(-1);                    // "t"(最も簡潔)
const last2 = str[str.length - 1];         // "t"(従来の方法)
const last3 = str.charAt(str.length - 1);  // "t"(charAt版)

先頭文字を大文字にする(capitalize)

function capitalize(str) {
  if (!str) return '';
  return str[0].toUpperCase() + str.slice(1);
}

console.log(capitalize('hello'));      // "Hello"
console.log(capitalize('javaScript')); // "JavaScript"

入力バリデーション(先頭文字の判定)

// 先頭が英字かどうかを判定
function startsWithLetter(str) {
  const first = str[0];
  return first && /[a-zA-Z]/.test(first);
}

console.log(startsWithLetter('Hello'));  // true
console.log(startsWithLetter('123abc')); // false
console.log(startsWithLetter(''));       // false

// 末尾の文字で処理を分岐
function getFileType(path) {
  const lastChar = path.at(-1);
  if (lastChar === '/') return 'directory';
  return 'file';
}

console.log(getFileType('/home/user/'));       // "directory"
console.log(getFileType('/home/user/file.txt')); // "file"

イニシャルの抽出

// 名前からイニシャルを取得
function getInitials(name) {
  return name
    .split(' ')
    .map(word => word[0])
    .join('')
    .toUpperCase();
}

console.log(getInitials('John Smith'));        // "JS"
console.log(getInitials('Taro Yamada'));       // "TY"
console.log(getInitials('Mary Jane Watson'));  // "MJW"

文字列のマスキング処理

// クレジットカード番号のマスキング
function maskCard(number) {
  const last4 = number.slice(-4);
  return '**** **** **** ' + last4;
}

console.log(maskCard('4111111111111111')); // "**** **** **** 1111"

// メールアドレスのマスキング
function maskEmail(email) {
  const [local, domain] = email.split('@');
  const first = local[0];
  const last = local.at(-1);
  const masked = '*'.repeat(local.length - 2);
  return `${first}${masked}${last}@${domain}`;
}

console.log(maskEmail('taro@example.com'));  // "t**o@example.com"
console.log(maskEmail('hello@gmail.com'));  // "h***o@gmail.com"

ランダム文字列の生成

// ランダムな英数字文字列を生成
function randomString(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * chars.length);
    result += chars[randomIndex];
  }
  return result;
}

console.log(randomString(8));   // 例: "kT3xB9mZ"
console.log(randomString(16));  // 例: "aB4cD8eF2gH1iJ7k"

回文(パリンドローム)判定

// 回文かどうかを判定
function isPalindrome(str) {
  const normalized = str.toLowerCase();
  const len = normalized.length;

  for (let i = 0; i < Math.floor(len / 2); i++) {
    if (normalized[i] !== normalized.at(-i - 1)) {
      return false;
    }
  }
  return true;
}

console.log(isPalindrome('racecar'));  // true
console.log(isPalindrome('Level'));    // true(大文字小文字を無視)
console.log(isPalindrome('Hello'));    // false

文字の出現回数をカウント

// 各文字の出現回数をカウント
function charCount(str) {
  const count = {};
  for (const char of str) {
    count[char] = (count[char] || 0) + 1;
  }
  return count;
}

console.log(charCount('banana'));
// { b: 1, a: 3, n: 2 }

// 最も多い文字を取得
function mostFrequentChar(str) {
  const count = charCount(str);
  return Object.entries(count)
    .sort((a, b) => b[1] - a[1])[0];
}

console.log(mostFrequentChar('banana')); // ["a", 3]

よくあるエラーと注意点

問題 原因 解決策
charAt() が空文字を返す インデックスが文字列の範囲外 インデックスが 0 ≤ i < str.length か確認
str[i]undefined インデックスが文字列の範囲外 存在チェック: if (str[i] !== undefined)
絵文字が文字化けする サロゲートペアがコードユニット単位で分割される [...str] または for...of を使う
at(-1) がエラーになる 古いブラウザ(IE など)が at() 非対応 str[str.length - 1] にフォールバック
length と実際の文字数が合わない サロゲートペア文字が2としてカウントされる [...str].length で正しい文字数を取得
複合絵文字がバラバラになる ZWJシーケンスや結合文字はスプレッド構文でも分解される Intl.Segmenter で書記素単位に分割

関連メソッドの一覧

文字列から文字を取得する方法と関連するメソッドを整理します。目的に応じて最適なメソッドを選びましょう。

目的 メソッド 説明
1文字を取得 charAt() / str[i] / at() この記事で解説
部分文字列を取得 slice() / substring() 範囲指定で文字列を切り出し
文字列の長さ length 文字列の長さ(UTF-16コードユニット数)を取得
文字コードを取得 codePointAt() Unicodeコードポイントを取得(サロゲートペア対応)
1文字ずつ分解 split('') / [...str] 文字列を配列に変換
文字の出現回数 カウント処理 特定文字の出現回数をカウントする方法
サロゲートペア対応ループ for...of コードポイント単位で文字列をイテレート

まとめ

場面 おすすめの方法 コード例
通常の1文字取得 ブラケット記法 str[0]
末尾の文字を取得 at() メソッド str.at(-1)
範囲外で空文字がほしい charAt() str.charAt(99)""
絵文字を含む文字列 スプレッド構文 [...str][0]
1文字ずつ走査 for...of for (const c of str)
複合絵文字を正確に処理 Intl.Segmenter segmenter.segment(str)
文字コードを取得 codePointAt() str.codePointAt(0)

charAt() は従来からある基本メソッドですが、現在のJavaScriptではブラケット記法 str[i] が最も一般的で、末尾からのアクセスには at() が最適です。サロゲートペアを含む文字列を扱う場合は、[...str]for...of でコードポイント単位に処理しましょう。

関連記事