innerHTML とは何か
innerHTML は、要素の「中身(子要素やテキスト)」を、HTML文字列として読み書きするためのプロパティです。文字列を代入すると、その場でパースされて要素やテキストに展開されます。ここが重要です:innerHTML は強力ですが“HTMLを実行する”ため、扱い方を誤るとセキュリティ(XSS)や保守性の問題を招きます。外部データをそのまま渡すのは絶対に避け、信頼できるテンプレートだけに限定しましょう。
基本の使い方(HTMLを差し込む・読む)
差し込みの例
<div id="box"></div>
<script>
const box = document.getElementById("box");
box.innerHTML = "<h2>タイトル</h2><p>説明文です。</p>";
</script>
HTMLこの例では、文字列がパースされて h2 と p が生成され、要素内に展開されます。HTMLタグが“解釈される”点が textContent と決定的に異なります。
読み取りの例
<div id="card"><strong>重要</strong>なお知らせ</div>
<script>
const card = document.getElementById("card");
console.log(card.innerHTML); // "<strong>重要</strong>なお知らせ"
</script>
HTMLinnerHTML を読むと、現在の要素内の“HTML文字列”がそのまま返ります。構造を文字列として扱いたいときに便利ですが、解析には DOM API を使うほうが安全で確実です。
textContent・innerText との使い分け(安全性と目的の違い)
textContent は「テキストだけ」を入れる・読むための安全な手段で、タグは解釈されません。ユーザー入力や外部データの表示は原則 textContent を使います。innerText は「画面に見えている文字」を基準に読み書きしますが、レイアウト計算が走ることがあり、頻繁な取得には不向きです。ここが重要です:HTMLを本当に展開したい時だけ innerHTML。それ以外は textContent をデフォルトにするのが堅実です。
重要ポイントの深掘り(XSS・イベント消失・性能)
XSS の危険性
外部から受け取った文字列を innerHTML に渡すと、悪意あるスクリプトやタグがそのまま実行・展開される可能性があります。たとえばユーザー名に <script>...</script> が紛れていた場合、ページで実行されてしまいます。ここが重要です:外部データは必ず“文字として”扱う(textContent)。どうしても HTML を差す必要がある場合は、厳密に検証済みのテンプレートだけを使用し、動的部分はエスケープした文字列を差し込みます。
イベントが消える問題
innerHTML で要素の中身を置き換えると、既存の子要素は丸ごと破棄されます。そこに紐付けたイベントリスナも失われます。ここが重要です:部分更新はできるだけ要素を差し替えず、必要箇所だけを DOM API(createElement、append、classList、textContent)で更新すると、リスナの再登録が不要で安定します。
性能と再描画
大きな HTML を innerHTML で何度も差し替えると、毎回パースと再描画が走り、体感が重くなります。ここが重要です:大量追加は DocumentFragment で組み立てて一括挿入、微小更新は DOM を直接操作し、文字列の全置換は避けるのが効率的です。
実践的な安全なパターン(テンプレート+文字差し込み)
構造は固定、可変部分は文字で
<article class="card">
<h2><span id="title"></span></h2>
<p id="desc"></p>
</article>
<script>
const title = document.getElementById("title");
const desc = document.getElementById("desc");
const data = { title: "ようこそ <b>安全</b>", desc: "タグは文字として扱う" };
title.textContent = data.title; // タグは文字として表示
desc.textContent = data.desc;
</script>
HTML構造をあらかじめ HTML で用意し、可変部分だけ textContent で差し込みます。見た目の装飾が必要なら、CSS や固定のタグで表現し、動的テキストは常に“文字”で扱います。
テンプレート要素を使う
<template id="row-tpl">
<tr>
<td class="name"></td>
<td class="qty"></td>
</tr>
</template>
<table><tbody id="tbody"></tbody></table>
<script>
const tpl = document.getElementById("row-tpl");
const tbody = document.getElementById("tbody");
const rows = [{name: "Apple", qty: 3}, {name: "Banana", qty: 0}];
const frag = document.createDocumentFragment();
rows.forEach(r => {
const node = tpl.content.cloneNode(true);
node.querySelector(".name").textContent = r.name;
node.querySelector(".qty").textContent = String(r.qty);
frag.append(node);
});
tbody.append(frag);
</script>
HTMLtemplate の中は実行されず、クローンしてから安全に textContent で埋めることで、XSS を避けつつ柔軟なレンダリングができます。
innerHTML を使う“ありうる場面”と注意点
実運用で innerHTML を使うのは、信頼できる静的テンプレートを一括で展開したい、あるいは小規模な装飾をシンプルに差し込みたい場面に限ります。注意点として、イベントが消えること、スクリプトタグは通常実行されないこと(再挿入でも動かない)、パースコストがかかることを理解しておいてください。ここが重要です:差し替え前後で必要な状態(スクロール位置、フォーカス、イベント)を維持する工夫が必要で、可能なら DOM の粒度で更新するほうが安全で高速です。
実践例:限定的な装飾の差し込みと安全な更新
<p id="notice"></p>
<script>
const notice = document.getElementById("notice");
// 固定の小さなテンプレート:太字とリンクのみ(動的文字はエスケープして挿入)
function escapeHTML(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);
}
function showNotice(userName) {
const safeName = escapeHTML(userName);
notice.innerHTML = `ようこそ、<strong>${safeName}</strong> さん! <a href="/help">ヘルプ</a>`;
}
showNotice('田中<script>alert(1)</script>'); // スクリプトは文字に変換されて安全
</script>
HTMLこの例では、固定のテンプレートに“動的な部分だけ”をエスケープした文字列で差し込んでいます。それでも、できる限りテンプレートの構造は HTML 側に事前定義し、変動部分は textContent を使うのが基本です。
まとめ
innerHTML は、HTML文字列をその場で展開できる強力なプロパティですが、XSS・イベント消失・性能劣化という落とし穴があります。外部データは常に textContent、構造はテンプレートや既存の DOM を使って組み立て、どうしても innerHTML が必要な場合は厳密に管理された安全なテンプレートに限定する。小さな差し込みでも“イベントと状態が消える”ことを念頭に、可能な限り DOM API で粒度の細かい更新を選ぶ。この方針を守れば、見た目の柔軟性と安全性を両立できます。
