【JavaScript】文字列内から特定の文字列の1つ前の1文字を抽出する方法(indexOf・正規表現・split)

JavaScriptで「特定の文字列の直前にある1文字を取得したい」と思ったことはありませんか?

たとえば、URLのパラメータ区切り文字の前の文字を調べたい、CSVデータの特定カラム手前の区切り文字を判定したい、ログファイルからエラーコード直前の識別子を抽出したいなど、文字列の「1つ前」を取得する処理は実務で頻繁に登場します。

この記事では、indexOf() による基本手法から正規表現(先読み・後読み)split() の活用lastIndexOf() による末尾検索複数マッチの一括取得、さらに実務で使えるユーティリティ関数パフォーマンス比較まで、あらゆるアプローチを網羅的に解説します。

スポンサーリンク
  1. indexOf() を使った基本的な方法
    1. 基本構文
    2. 安全なヘルパー関数として実装する
    3. 複数文字の検索文字列にも対応
  2. charAt() を使う方法
    1. charAt() と ブラケット記法の違い
  3. 正規表現を使った方法
    1. キャプチャグループで取得する
    2. 肯定先読み(Lookahead)を使う方法
    3. 肯定後読み(Lookbehind)で検索文字列の後ろを取得
    4. 正規表現の特殊文字をエスケープする
  4. split() を使った方法
    1. 基本的な使い方
    2. split() で全出現箇所の直前文字を取得
  5. lastIndexOf() で末尾から検索する方法
    1. 基本的な使い方
    2. indexOf() と lastIndexOf() の使い分け
    3. 実用例:ファイルの拡張子の直前の文字を取得
  6. 全出現箇所の直前文字を一括取得する方法
    1. indexOf() のループで全箇所を取得
    2. 正規表現の g フラグで全マッチ取得
  7. slice() / substring() で直前の複数文字を取得する
    1. 直前のN文字を取得する
    2. slice() と substring() の違い
  8. 実務で使えるユーティリティ関数集
    1. 汎用ヘルパー関数
    2. 実務例1:URLパラメータの区切り文字判定
    3. 実務例2:CSVパーサーでの区切り文字検出
    4. 実務例3:ログ解析でエラーレベルの直前文字を取得
  9. エッジケースと注意点
    1. 検索文字列が先頭にある場合
    2. 検索文字列が見つからない場合
    3. 空文字列の扱い
    4. サロゲートペア(絵文字・特殊文字)の問題
    5. 大文字・小文字を区別しない検索
  10. よくあるエラーと対処法
    1. エラーを防ぐ堅牢な実装パターン
  11. 各方法のパフォーマンス比較
  12. 方法別の比較表
  13. よくある質問(FAQ)
    1. Q. 検索文字列の「1つ後の文字」を取得するにはどうすればよいですか?
    2. Q. TypeScript で型安全に実装するにはどうすればよいですか?
    3. Q. Node.js と ブラウザで動作に違いはありますか?
  14. まとめ
  15. 関連記事

indexOf() を使った基本的な方法

最もシンプルで理解しやすいのが、indexOf() で検索文字列の位置を取得し、その1つ前のインデックスから文字を抽出する方法です。

基本構文

JavaScript
const str = "abcdefg";
const search = "d";

// 検索文字列の位置を取得
const index = str.indexOf(search);

// 1つ前の文字を取得(位置が1以上であることを確認)
if (index > 0) {
  const prevChar = str[index - 1];
  console.log(prevChar); // "c"
}

実行結果

c

indexOf() のポイント

  • indexOf() は検索文字列が見つかった最初の位置(0始まり)を返す
  • 見つからない場合は -1 を返す
  • index > 0 のチェックにより、先頭文字の場合(前に文字がない)も安全に処理できる
  • str[index - 1] はブラケット記法で1文字を取得する(charAt() でも可)

安全なヘルパー関数として実装する

実務では毎回 indexOf() のチェックを書くのは冗長なので、関数化しておくと便利です。

JavaScript
/**
 * 指定した検索文字列の直前の1文字を返す
 * @param {string} str - 対象文字列
 * @param {string} search - 検索文字列
 * @returns {string|null} 直前の1文字(見つからない場合は null)
 */
function getCharBefore(str, search) {
  const index = str.indexOf(search);
  if (index <= 0) return null;
  return str[index - 1];
}

// 使用例
console.log(getCharBefore("Hello World", "World"));  // " "(スペース)
console.log(getCharBefore("data-value", "value"));  // "-"
console.log(getCharBefore("abcdef", "a"));       // null(先頭のため)
console.log(getCharBefore("abcdef", "z"));       // null(見つからない)

実行結果

 
-
null
null

複数文字の検索文字列にも対応

indexOf() は1文字だけでなく複数文字の文字列も検索できるため、特別な変更なくそのまま動作します。

JavaScript
const url = "https://example.com?page=1&sort=name";

// "sort" の直前の1文字を取得
const idx = url.indexOf("sort");
if (idx > 0) {
  console.log(url[idx - 1]); // "&"
}

実行結果

&

charAt() を使う方法

charAt()str[index] のブラケット記法と同じく、指定した位置の文字を返すメソッドです。古いブラウザとの互換性が必要な場合に使われます。

JavaScript
const str = "JavaScript";
const search = "Script";

const index = str.indexOf(search);

if (index > 0) {
  // charAt() で1つ前の文字を取得
  const prevChar = str.charAt(index - 1);
  console.log(prevChar); // "a"
}

実行結果

a

charAt() と ブラケット記法の違い

比較項目 str[index] str.charAt(index)
範囲外アクセス undefined ""(空文字列)
戻り値の型 string または undefined 常に string
IE対応 IE8以前は非対応 全バージョン対応
推奨度 モダン環境ではこちらが主流 レガシー互換が必要な場合

ポイント:モダンなJavaScript開発では str[index] のブラケット記法が一般的です。ただし、範囲外アクセス時の挙動が異なるため、エラーハンドリングの方法に注意してください。

正規表現を使った方法

正規表現を使うと、パターンマッチングによって柔軟に「特定文字列の1つ前」を取得できます。

キャプチャグループで取得する

検索文字列の直前の1文字をキャプチャグループ () で囲んで取得します。

JavaScript
const str = "abcdefg";
const search = "d";

// 任意の1文字(.)をキャプチャし、その直後に search が続くパターン
const regex = new RegExp(`(.)${search}`);
const match = str.match(regex);

if (match) {
  console.log(match[1]); // "c"(キャプチャグループ1)
}

実行結果

c

正規表現パターンの解説

  • (.) – 任意の1文字をキャプチャグループで取得
  • d(search変数)- 直後に続く検索文字列
  • match[0] – マッチ全体("cd"
  • match[1] – キャプチャグループ1("c" = 直前の1文字)

肯定先読み(Lookahead)を使う方法

肯定先読み (?=...) を使うと、検索文字列自体を消費せずに直前の文字だけを取得できます。

JavaScript
const str = "price:100,tax:10,total:110";

// ":" の直前の1文字を先読みで取得(最初のマッチ)
const match = str.match(/(.(?=:))/);

if (match) {
  console.log(match[1]); // "e"(price の最後の文字)
}

実行結果

e

肯定後読み(Lookbehind)で検索文字列の後ろを取得

ES2018 で追加された後読み(Lookbehind)を使えば、逆に「検索文字列の直後の1文字」を取得する際に便利です。ここでは参考として紹介しますが、「直前の1文字」には先読みのほうが適切です。

JavaScript(参考:後読みの例)
const str = "price:100";

// ":" の直後の1文字を後読みで取得
const match = str.match(/(?<=:)(.)/);

if (match) {
  console.log(match[1]); // "1"(":" の直後)
}

実行結果

1

注意:後読み(Lookbehind)は ES2018(ES9)で追加された機能です。IE11では対応していません。モダンブラウザ(Chrome 62+、Firefox 78+、Safari 16.4+)では問題なく使用できます。

正規表現の特殊文字をエスケープする

検索文字列に正規表現の特殊文字(. * + ? ^ $ { } ( ) | [ ] \ など)が含まれる場合は、エスケープが必要です。

JavaScript
/**
 * 正規表現の特殊文字をエスケープする
 */
function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// "(" を含む検索文字列
const str = "func(arg)";
const search = "(arg)";

const escaped = escapeRegex(search);
const regex = new RegExp(`(.)${escaped}`);
const match = str.match(regex);

if (match) {
  console.log(match[1]); // "c"("func" の最後の文字)
}

実行結果

c

注意:ユーザー入力を正規表現に組み込む場合は、必ずエスケープ処理を行ってください。エスケープしないと、意図しないパターンマッチや正規表現エラー(SyntaxError)が発生する原因になります。

split() を使った方法

split() で検索文字列を区切り文字として分割し、最初の部分文字列の末尾1文字を取得する方法です。

基本的な使い方

JavaScript
const str = "user-name-value";
const search = "name";

// search で分割し、最初の要素の末尾1文字を取得
const parts = str.split(search);

if (parts[0].length > 0) {
  const prevChar = parts[0].slice(-1);
  console.log(prevChar); // "-"
}

実行結果

-

split() の仕組み

  • "user-name-value".split("name")["user-", "-value"]
  • 最初の要素 "user-" の末尾1文字 → "-"
  • slice(-1) は文字列の末尾1文字を返す便利なメソッド

split() で全出現箇所の直前文字を取得

split() の結果を活用すれば、最初だけでなく全出現箇所の直前文字を一度に取得できます。

JavaScript
const str = "aXbXcXd";
const search = "X";

const parts = str.split(search);

// 最初の要素以外の各要素について、前の要素の末尾1文字が「直前の文字」
const prevChars = parts
  .slice(0, -1)  // 最後の要素を除外
  .map(part => part.slice(-1))
  .filter(ch => ch.length > 0); // 先頭にsearchがある場合を除外

console.log(prevChars); // ["a", "b", "c"]

実行結果

["a", "b", "c"]

lastIndexOf() で末尾から検索する方法

文字列中に検索文字列が複数回出現する場合、最後の出現箇所の直前1文字を取得したいケースがあります。その場合は lastIndexOf() を使います。

基本的な使い方

JavaScript
const str = "img_001.thumb.jpg";
const search = ".";

// 最後のドット(.)の直前1文字を取得
const lastIdx = str.lastIndexOf(search);

if (lastIdx > 0) {
  const prevChar = str[lastIdx - 1];
  console.log(prevChar); // "b"(.jpg の前 → thumb の "b")
}

実行結果

b

indexOf() と lastIndexOf() の使い分け

メソッド 検索方向 返す位置 ユースケース
indexOf() 先頭 → 末尾 最初の出現位置 最初のマッチを処理したい場合
lastIndexOf() 末尾 → 先頭 最後の出現位置 拡張子取得、末尾区切り文字の処理

実用例:ファイルの拡張子の直前の文字を取得

JavaScript
function getCharBeforeExtension(filename) {
  const dotIndex = filename.lastIndexOf(".");
  if (dotIndex <= 0) return null;
  return filename[dotIndex - 1];
}

console.log(getCharBeforeExtension("report.pdf"));       // "t"
console.log(getCharBeforeExtension("archive.tar.gz"));   // "r"(.gz の前)
console.log(getCharBeforeExtension(".gitignore"));      // null(ドットが先頭)
console.log(getCharBeforeExtension("noextension"));     // null(ドットなし)

実行結果

t
r
null
null

全出現箇所の直前文字を一括取得する方法

検索文字列が文字列中に複数回出現する場合に、すべての出現箇所の直前1文字をまとめて取得する方法を解説します。

indexOf() のループで全箇所を取得

JavaScript
function getAllCharsBefore(str, search) {
  const results = [];
  let pos = 0;

  while (true) {
    const index = str.indexOf(search, pos);
    if (index === -1) break;

    if (index > 0) {
      results.push({
        char: str[index - 1],
        position: index - 1,
        matchAt: index
      });
    }
    pos = index + search.length;
  }

  return results;
}

const str = "the cat sat on the mat";
const results = getAllCharsBefore(str, "at");

results.forEach(r => {
  console.log(`位置${r.matchAt}: 直前の文字 = "${r.char}"`);
});

実行結果

位置5: 直前の文字 = "c"
位置9: 直前の文字 = "s"
位置20: 直前の文字 = "m"

正規表現の g フラグで全マッチ取得

JavaScript
const str = "the cat sat on the mat";

// matchAll() で全マッチのイテレータを取得
const matches = [...str.matchAll(/(.)at/g)];

matches.forEach(m => {
  console.log(`マッチ: "${m[0]}" → 直前の文字: "${m[1]}"`);
});

実行結果

マッチ: "cat" → 直前の文字: "c"
マッチ: "sat" → 直前の文字: "s"
マッチ: "mat" → 直前の文字: "m"

ポイント:matchAll() は ES2020 で追加されたメソッドです。g フラグ付きの正規表現で全マッチの詳細情報(キャプチャグループ含む)を取得できます。

slice() / substring() で直前の複数文字を取得する

ここまでは「1つ前の1文字」を取得する方法を解説してきましたが、実務では直前のN文字を取得したいケースもあります。slice()substring() を使えば柔軟に対応できます。

直前のN文字を取得する

JavaScript
/**
 * 検索文字列の直前N文字を取得する
 * @param {string} str - 対象文字列
 * @param {string} search - 検索文字列
 * @param {number} n - 取得する文字数(デフォルト: 1)
 * @returns {string|null}
 */
function getCharsBefore(str, search, n = 1) {
  const index = str.indexOf(search);
  if (index <= 0) return null;

  // 取得開始位置(0未満にならないよう制限)
  const start = Math.max(0, index - n);
  return str.slice(start, index);
}

console.log(getCharsBefore("Hello World", "World", 1));  // " "
console.log(getCharsBefore("Hello World", "World", 3));  // "lo "
console.log(getCharsBefore("Hello World", "World", 10)); // "Hello "(最大6文字まで)

実行結果

 
lo 
Hello 

slice() と substring() の違い

比較項目 slice() substring()
負のインデックス 対応(末尾から数える) 0として扱う
引数の順序逆転 空文字列を返す 自動的に入れ替える
推奨度 モダンJSではこちらが主流 レガシーコードで見かける

実務で使えるユーティリティ関数集

ここまでの知識を活かして、実務で即使える汎用的なユーティリティ関数をまとめます。

汎用ヘルパー関数

string-utils.js
/**
 * 文字列操作ユーティリティ
 */
const StringUtils = {

  /** 最初の出現箇所の直前1文字を取得 */
  getCharBefore(str, search) {
    const idx = str.indexOf(search);
    return idx > 0 ? str[idx - 1] : null;
  },

  /** 最後の出現箇所の直前1文字を取得 */
  getCharBeforeLast(str, search) {
    const idx = str.lastIndexOf(search);
    return idx > 0 ? str[idx - 1] : null;
  },

  /** 全出現箇所の直前1文字を配列で取得 */
  getAllCharsBefore(str, search) {
    const results = [];
    let pos = 0;
    while (true) {
      const idx = str.indexOf(search, pos);
      if (idx === -1) break;
      if (idx > 0) results.push(str[idx - 1]);
      pos = idx + search.length;
    }
    return results;
  },

  /** N番目の出現箇所の直前1文字を取得 */
  getCharBeforeNth(str, search, n = 1) {
    let pos = 0;
    let count = 0;
    while (true) {
      const idx = str.indexOf(search, pos);
      if (idx === -1) return null;
      count++;
      if (count === n) {
        return idx > 0 ? str[idx - 1] : null;
      }
      pos = idx + search.length;
    }
  }
};

// 使用例
const csv = "name,age,city,country";
console.log(StringUtils.getCharBefore(csv, ","));          // "e"
console.log(StringUtils.getCharBeforeLast(csv, ","));      // "y"
console.log(StringUtils.getAllCharsBefore(csv, ","));      // ["e", "e", "y"]
console.log(StringUtils.getCharBeforeNth(csv, ",", 2));   // "e"(2番目のカンマの前)

実行結果

e
y
["e", "e", "y"]
e

実務例1:URLパラメータの区切り文字判定

JavaScript
/**
 * URLにクエリパラメータを安全に追加する
 * ?がすでにあれば&で、なければ?で追加
 */
function addQueryParam(url, key, value) {
  const separator = url.includes("?") ? "&" : "?";
  return `${url}${separator}${key}=${encodeURIComponent(value)}`;
}

/**
 * 特定パラメータの直前の区切り文字を確認する実例
 */
function getParamSeparator(url, paramName) {
  const idx = url.indexOf(paramName);
  if (idx <= 0) return null;
  const prevChar = url[idx - 1];
  return prevChar; // "?" or "&"
}

const url1 = "https://example.com?page=1&sort=name";
console.log(getParamSeparator(url1, "page")); // "?"
console.log(getParamSeparator(url1, "sort")); // "&"

実行結果

?
&

実務例2:CSVパーサーでの区切り文字検出

JavaScript
/**
 * CSVの各フィールドの直前の文字を分析し、
 * クォートで囲まれたフィールドかどうかを判定
 */
function analyzeCSVField(csvLine, fieldValue) {
  const idx = csvLine.indexOf(fieldValue);
  if (idx <= 0) return { found: false };

  const prevChar = csvLine[idx - 1];
  return {
    found: true,
    isQuoted: prevChar === '\"',
    isFirstField: idx === 0,
    prevChar: prevChar
  };
}

const line = 'name,"Tokyo, Japan",25';
console.log(analyzeCSVField(line, "Tokyo"));

実行結果

{ found: true, isQuoted: true, isFirstField: false, prevChar: '"' }

実務例3:ログ解析でエラーレベルの直前文字を取得

JavaScript
/**
 * ログ行からエラーレベルタグの直前文字を取得し、
 * ログフォーマットの種類を判定する
 */
function detectLogFormat(logLine) {
  const levels = ["ERROR", "WARN", "INFO", "DEBUG"];

  for (const level of levels) {
    const idx = logLine.indexOf(level);
    if (idx > 0) {
      const prev = logLine[idx - 1];
      if (prev === "[") return "bracket-format";  // [ERROR]
      if (prev === " ") return "space-separated"; // ... ERROR ...
      if (prev === "|") return "pipe-separated";  // ...|ERROR|...
    }
  }
  return "unknown";
}

console.log(detectLogFormat("2024-01-15 [ERROR] DB connection failed"));
console.log(detectLogFormat("2024-01-15 ERROR DB connection failed"));
console.log(detectLogFormat("2024-01-15|ERROR|DB connection failed"));

実行結果

bracket-format
space-separated
pipe-separated

エッジケースと注意点

「直前の1文字を取得する」処理には、いくつかの落とし穴があります。実務でバグを防ぐために必ず押さえておきましょう。

検索文字列が先頭にある場合

検索文字列が文字列の先頭にある場合、「1つ前の文字」は存在しません。

JavaScript
const str = "Hello World";

// "Hello" は先頭にあるため、直前の文字は存在しない
const idx = str.indexOf("Hello"); // 0

// 危険な実装(index - 1 が -1 になる)
console.log(str[idx - 1]); // undefined

// 安全な実装
if (idx > 0) {
  console.log(str[idx - 1]);
} else {
  console.log("直前の文字は存在しません");
}

実行結果

undefined
直前の文字は存在しません

検索文字列が見つからない場合

JavaScript
const str = "abcdef";
const idx = str.indexOf("xyz"); // -1

// 危険! str[-2] は undefined だがエラーにはならない
console.log(str[idx - 1]); // undefined

// 必ず indexOf の結果をチェックしてから使う
if (idx !== -1 && idx > 0) {
  console.log(str[idx - 1]);
} else {
  console.log("検索文字列が見つかりません");
}

実行結果

undefined
検索文字列が見つかりません

空文字列の扱い

JavaScript
// 空文字列で検索すると indexOf は 0 を返す
console.log("abc".indexOf("")); // 0

// 対象文字列が空の場合
console.log("".indexOf("a")); // -1

// 安全な関数は両方のケースを考慮する
function safeGetCharBefore(str, search) {
  if (!str || !search) return null;
  const idx = str.indexOf(search);
  return idx > 0 ? str[idx - 1] : null;
}

サロゲートペア(絵文字・特殊文字)の問題

JavaScriptの文字列はUTF-16でエンコードされるため、絵文字や一部の漢字は2つのコードユニット(サロゲートペア)で表現されます。

JavaScript
const str = "I like cats";

// 絵文字はサロゲートペア(2コードユニット)
console.log(str.length);     // 13("I like " = 7, 絵文字 = 2, "cats" = 4)
console.log(str[7]);         // サロゲートペアの前半(文字化け)

// indexOf は正しく動作する
const idx = str.indexOf("cats"); // 9
console.log(str[idx - 1]); // サロゲートペアの後半(文字化け!)

// 対策: Array.from() で正しく文字単位に分割
const chars = Array.from(str);
const catIdx = chars.findIndex((_, i) =>
  chars.slice(i, i + 4).join("") === "cats"
);
if (catIdx > 0) {
  console.log(chars[catIdx - 1]); // 正しく絵文字全体を取得
}

注意:絵文字や特殊文字を含む文字列を扱う場合は、Array.from()[...str](スプレッド構文)で正しい文字単位に分割してから処理してください。str[index]charAt() はコードユニット単位のため、サロゲートペアの半分だけを返す可能性があります。

大文字・小文字を区別しない検索

JavaScript
/**
 * 大文字・小文字を区別せずに直前の1文字を取得
 */
function getCharBeforeCI(str, search) {
  // 方法1: toLowerCase() で揃える
  const idx = str.toLowerCase().indexOf(search.toLowerCase());
  return idx > 0 ? str[idx - 1] : null;
}

/**
 * 正規表現の i フラグを使う方法
 */
function getCharBeforeCIRegex(str, search) {
  const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const match = str.match(new RegExp(`(.)${escaped}`, "i"));
  return match ? match[1] : null;
}

console.log(getCharBeforeCI("Hello WORLD", "world"));     // " "
console.log(getCharBeforeCIRegex("Hello WORLD", "world")); // " "

実行結果

 
 

よくあるエラーと対処法

エラー・症状 原因 対処法
undefined が返る indexOfが-1を返した(見つからない)、または先頭にマッチした index > 0 のチェックを必ず行う
サロゲートペアの半分が返る 絵文字の直前で str[index] を使用 Array.from(str) で分割してから操作
SyntaxError: Invalid regular expression 特殊文字を含む検索文字列を正規表現に直接使用 escapeRegex() でエスケープしてから new RegExp() に渡す
期待と違う文字が返る 複数回出現するが最初のマッチを取得している lastIndexOf()getAllCharsBefore() を使う
TypeError: Cannot read properties of null 正規表現の match() が null を返した if (match) のnullチェックを必ず行う

エラーを防ぐ堅牢な実装パターン

JavaScript(堅牢な実装例)
/**
 * 堅牢版:あらゆるエッジケースに対応した直前文字取得
 * @param {string} str - 対象文字列
 * @param {string} search - 検索文字列
 * @param {Object} options - オプション
 * @param {boolean} options.caseInsensitive - 大文字小文字を無視
 * @param {number} options.occurrence - N番目の出現(デフォルト: 1)
 * @param {boolean} options.fromEnd - 末尾から検索
 * @returns {string|null}
 */
function getCharBeforeRobust(str, search, options = {}) {
  // 入力バリデーション
  if (typeof str !== "string" || typeof search !== "string") {
    return null;
  }
  if (str.length === 0 || search.length === 0) {
    return null;
  }

  const { 
    caseInsensitive = false, 
    occurrence = 1, 
    fromEnd = false 
  } = options;

  // 末尾から検索
  if (fromEnd) {
    const s = caseInsensitive ? str.toLowerCase() : str;
    const q = caseInsensitive ? search.toLowerCase() : search;
    const idx = s.lastIndexOf(q);
    return idx > 0 ? str[idx - 1] : null;
  }

  // N番目の出現を検索
  const s = caseInsensitive ? str.toLowerCase() : str;
  const q = caseInsensitive ? search.toLowerCase() : search;
  let pos = 0;
  let count = 0;

  while (pos < s.length) {
    const idx = s.indexOf(q, pos);
    if (idx === -1) break;
    count++;
    if (count === occurrence) {
      return idx > 0 ? str[idx - 1] : null;
    }
    pos = idx + q.length;
  }

  return null;
}

// 使用例
const text = "aXbXcXd";
console.log(getCharBeforeRobust(text, "X"));                        // "a"
console.log(getCharBeforeRobust(text, "X", { occurrence: 2 }));     // "b"
console.log(getCharBeforeRobust(text, "X", { fromEnd: true }));     // "c"
console.log(getCharBeforeRobust(text, "x", { caseInsensitive: true })); // "a"

実行結果

a
b
c
a

各方法のパフォーマンス比較

どの方法を選ぶべきか迷った場合に参考になるパフォーマンス比較です。10万回の繰り返し処理で計測した結果を示します。

JavaScript(ベンチマーク)
const str = "abcdefghijklmnopqrstuvwxyz".repeat(100);
const search = "xyz";
const ITERATIONS = 100000;

// indexOf
console.time("indexOf");
for (let i = 0; i < ITERATIONS; i++) {
  const idx = str.indexOf(search);
  if (idx > 0) str[idx - 1];
}
console.timeEnd("indexOf");

// 正規表現
console.time("regex");
const regex = new RegExp(`(.)${search}`);
for (let i = 0; i < ITERATIONS; i++) {
  const m = str.match(regex);
  if (m) m[1];
}
console.timeEnd("regex");

// split
console.time("split");
for (let i = 0; i < ITERATIONS; i++) {
  const parts = str.split(search);
  if (parts[0].length > 0) parts[0].slice(-1);
}
console.timeEnd("split");

実行結果(参考値)

indexOf: 12ms
regex: 45ms
split: 380ms

パフォーマンスのポイント

  • indexOf() が圧倒的に高速(文字列探索に最適化されている)
  • 正規表現はパターンコンパイルのオーバーヘッドがある(ループ外でnew RegExpを1回だけ実行すると改善)
  • split() は文字列全体を分割するため、長い文字列では遅くなる
  • 通常の用途ではどの方法でも十分高速なので、可読性を優先して選択するのがベスト

方法別の比較表

各方法の特徴を一覧で比較します。用途に応じて最適な方法を選んでください。

方法 速度 柔軟性 複数マッチ 推奨用途
indexOf() + str[] 最速 ループで可能 シンプルな単一マッチ
charAt() 最速 ループで可能 レガシーブラウザ対応
正規表現 (.) matchAll() 複雑なパターンマッチ
先読み (?=...) 最高 matchAll() 検索文字列を消費せずに取得
split() 自動で全箇所 分割と同時に取得したい場合
lastIndexOf() 最速 最後のみ 末尾のマッチを処理

ポイント:迷ったら indexOf() を使いましょう。最もシンプルで高速、かつ可読性も高い方法です。正規表現は複雑なパターンが必要な場合にのみ使用するのがベストプラクティスです。

よくある質問(FAQ)

Q. 検索文字列の「1つ後の文字」を取得するにはどうすればよいですか?

indexOf() で見つかった位置に検索文字列の長さを足せば、直後の位置になります。

JavaScript
function getCharAfter(str, search) {
  const idx = str.indexOf(search);
  if (idx === -1) return null;
  const afterIdx = idx + search.length;
  return afterIdx < str.length ? str[afterIdx] : null;
}

console.log(getCharAfter("Hello World", "Hello")); // " "
console.log(getCharAfter("abc", "abc"));         // null(後ろに文字がない)

実行結果

 
null

Q. TypeScript で型安全に実装するにはどうすればよいですか?

TypeScript
function getCharBefore(str: string, search: string): string | null {
  const index: number = str.indexOf(search);
  if (index <= 0) return null;
  return str[index - 1];
}

// 戻り値が string | null なので null チェックが必要
const result = getCharBefore("Hello", "llo");
if (result !== null) {
  console.log(result.toUpperCase()); // "E"(型安全に操作可能)
}

Q. Node.js と ブラウザで動作に違いはありますか?

この記事で紹介した方法はすべてECMAScript標準に基づいているため、Node.js とブラウザで動作に違いはありません。ただし、以下の点に注意してください。

機能 必要なES仕様 対応ブラウザ Node.js
indexOf() ES1 全ブラウザ 全バージョン
str[index] ブラケット記法 ES5 IE9+ 全バージョン
テンプレートリテラル ES6 IE以外 4.0+
後読み (?<=...) ES2018 Chrome 62+, FF 78+, Safari 16.4+ 8.10+
matchAll() ES2020 Chrome 73+, FF 67+, Safari 13+ 12.0+

まとめ

この記事のポイント

  • 基本indexOf() で位置を取得し、str[index - 1] で直前の1文字を取得するのが最もシンプル
  • 末尾検索:最後の出現箇所が必要な場合は lastIndexOf() を使用
  • 正規表現(.) キャプチャグループや先読み (?=...) で柔軟なパターンマッチが可能
  • 全マッチmatchAll()indexOf() のループで全出現箇所の直前文字を取得
  • split():分割結果の各要素の末尾文字 = 各出現箇所の直前文字
  • エッジケース:先頭マッチ、未検出、空文字列、サロゲートペアに注意
  • パフォーマンスindexOf() が最速。通常は可読性を優先して選択

ポイント:実務では入力バリデーション(null/undefined/空文字チェック)と境界条件(先頭マッチ・未検出)の処理を必ず入れましょう。堅牢な関数を一度作っておけば、プロジェクト全体で安全に再利用できます。

関連記事