JavaScriptにおける「this」とデータ保存の謎を徹底解説
ブラウザのキャッシュから関数呼び出しまで、フロントエンドの中核概念を徹底解説
フロントエンド開発者として、私たちは毎日、避けて通れない2つのトピックに直面しています。それは、データストレージと、thisの参照先です。一見無関係に見えますが、日常の開発では頻繁に絡み合っています。今日は、ストレージ技術から切り込み、JavaScriptで頭を悩ませる「this」へと進みながら、その謎を一つずつ解き明かしていきましょう。
一、データ保存のマルチバース
データ保存は決して単一の概念ではなく、まるで階層構造を持つ高層ビルのようなもので、各階にはそれぞれの役割があります。
1.1 永続化ストレージ:MySQL とリレーショナルデータベース
データを永続的に保存する必要がある場合、MySQL のようなリレーショナルデータベースが第一の選択肢となります。これはデータをテーブル形式で整理し、SQL 言語を用いてCRUD(作成・読み取り・更新・削除)操作を行い、データの一貫性と永続性を保証します。ユーザー登録情報、注文履歴、記事の内容……こうした長期保存が必要なデータは、最終的にすべて MySQL の「懐」に収まります。
-- 典型的なMySQLクエリ
SELECT * FROM articles WHERE user_id = 123;
しかし、MySQLにも「弱点」があります――クエリを実行するたびにディスクI/Oと複雑な解析・実行が必要となり、同時アクセス数が増加すると、パフォーマンスのボトルネックが発生します。
1.2 キャッシュ層:Redisのキーバリュー(KV)哲学
MySQLのパフォーマンス上の弱点を補うために、Redisが登場しました。Redisはメモリ上で動作し、キーバリュー(Key-Value)方式でデータを保存するため、読み書き速度はMySQLよりも数桁速くなります。
最初のリクエスト:MySQL クエリ → 結果を Redis に保存
その後のリクエスト:直接 Redis 経由 → MySQL を参照する必要なし
この「まずキャッシュを読み込み、キャッシュにない場合にのみデータベースを参照する」というモデルは、現代の高並行処理システムにおける標準仕様です。Redis はまるで MySQL の「ブースター」のような存在であり、データアクセスを飛躍的に高速化します。
1.3 ブラウザキャッシュとローカルストレージ
データがフロントエンドに到達すると、保存形態は再び変化します:
- ブラウザキャッシュ(HTTP Cache):ページを再度開く際に「瞬時に表示」されるようにし、リソースファイル(CSS、JS、画像)がキャッシュされるため、再ダウンロードの必要がありません。
- LocalStorage / SessionStorage:キーと値のペアとしてブラウザに保存され、容量は約5~10MBです。ユーザーの好みやテーマ設定など、軽量なデータの保存に適しています。
- Cookie:容量は小さい(約4KB)ですが、リクエストヘッダーに自動的に含まれるため、認証によく使用されます。
1.4 最前線:LLMとエンベディングの保存
AI時代において、ストレージの境界はさらに拡大しています。大規模言語モデル(LLM)は、エンベディングベクトルを使用してテキストのセマンティック情報を保存し、ベクトルデータベース(Pinecone、Milvusなど)を通じて類似度検索を行います。これは全く新しいストレージのパラダイム――データインテリジェンス――である。
ストレージ技術の発展は、MySQLからRedisへ、LocalStorageからベクトルデータベースへと進んできたが、その本質は速度、容量、永続性、インテリジェンスの間のバランスを見出すことにある。
二、フロントエンドのフォーム:デフォルトの送信からAjaxへの進化
フロントエンド開発の日常的なシーン――フォーム――に立ち返ってみましょう。
index.html には、次のような典型的なフォーム構造があります:
<form class="add-items">
<input type="text" name="item" placeholder="Add a new tapas">
<input type="submit" value="+ Add Item">
</form>
ユーザーが type=「submit」 ボタンをクリックすると、フォームは submit イベントをトリガーし、ブラウザは デフォルトの動作 を実行します。つまり、action 属性で指定されたアドレスにリクエストを送信し、ページをリロードします。
この手法は従来のWebアプリケーションでは一般的ですが、現代のフロントエンド開発では、よりスムーズなユーザー体験を提供するために、JavaScriptで送信ロジックを制御する方が好まれます。
const oForm = document.querySelector(『.add-items』);
function addItem(e) {
console.log(e);
console.log(this);
// 送信のデフォルト動作を阻止 ------ ページをリロードしない
e.preventDefault();
// ここに fetch / ajax のロジックを記述できます
}
// submit イベントをリッスン
oForm.addEventListener(『submit』, addItem);
e.preventDefault() を使用してデフォルトのページリロードを阻止し、fetch または XMLHttpRequest を使ってデータを非同期で送信します。これが Ajax の核心となる考え方です。ページがリロードされなくなり、ユーザーが認識するのはデータの部分的な更新のみとなるため、ユーザー体験が大幅に向上します。
3. this:関数実行時の「魂」
JavaScriptにおいて最も理解を惑わせる概念といえば、thisは間違いなくトップクラスに挙げられるでしょう。多くの人がthisを「関数宣言時に」決定されるものと理解していますが、これは誤りです。正しい理解は以下の通りです:
this は関数の実行時に決定され、関数を呼び出したオブジェクトを指します。
あるコードを見てみましょう(出典:2.html):
var name = 「ユーザーA」;
let obj = {
name: 「ユーザーB」,
say: function() {
console.log(this.name);
}
}
obj.say(); // 出力:ユーザーB
ここでは、sayがオブジェクトobjのメソッドとして呼び出され、thisはobjを指すため、「ユーザーB」が出力されます。
しかし、関数参照を変数に代入してから呼び出した場合はどうなるでしょうか?
const fn = obj.say;
fn(); // 出力:ユーザーA(厳格モードでない場合)
この場合、fnは通常の関数として呼び出され、thisはグローバルオブジェクトwindowを指すため、「ユーザーA」が出力されます。しかし、common.jsで厳格モードを有効にした場合はどうでしょうか:
「use strict」;
// 厳格モードでは、通常の関数の this は undefined になります
これこそが、厳格モードの意義の一つです:暗黙的なグローバル汚染を減らすことです。
3.1 this の5つのシナリオ
| シナリオ | this が指す対象 |
|---|---|
| 通常の関数呼び出し(厳格モード以外) | window / global |
| 通常の関数呼び出し(厳格モード) | undefined |
| オブジェクトメソッドの呼び出し | そのメソッドを呼び出したオブジェクト |
| コンストラクタ関数の呼び出し(new) | 新しく作成されたインスタンスオブジェクト |
| イベントハンドラ | イベントをトリガーしたDOM要素 |
3.2 イベントハンドラにおけるthis
common.js では、リンクにクリックイベントをバインドしています:
document.querySelector(『.lnk』)
.addEventListener(『click』, goBaidu);
function goBaidu(e) {
console.log(this); // <a class="lnk" href="..."> 要素を指す
e.preventDefault();
}
イベントハンドラ関数として、thisはデフォルトでイベントをトリガーしたDOM要素を指します。これは通常の関数の呼び出し方法とは異なります。
4. thisの手動指定:call / apply / bind
多くの場合、thisが呼び出し方法によって決定されるのではなく、手動で制御したいことがあります。JavaScriptでは、call、apply、bindの3つのメソッドが提供されています。
4.1 call:即時呼び出し、引数を1つずつ渡す
let obj = {
name: 「ユーザーC」,
speak: function(a, b) {
console.log(a, b);
console.log(this.name);
}
}
let obj2 = { name: 「ユーザーD」 };
// call を使用して、this を obj2 に設定
obj.speak.call(obj2, 『こんにちは』, 『私はユーザーDです』);
// 出力:こんにちは 私はユーザーDです ユーザーD
call の最初の引数は、バインドしたい this オブジェクトであり、その後の引数は順に関数に渡されます。
4.2 apply:即時呼び出し、配列による引数渡し
apply は call とほぼ同じですが、引数の渡し方が異なります:
obj.speak.apply(obj2, [『こんにちは』, 『私はユーザーDです』]);
// 効果は call と同じですが、引数は配列形式で渡されます
引数の数が不定の場合や、配列から渡される場合には、apply の方が便利です。
4.3 bind:遅延呼び出し、新しい関数を返す
bind と前述の2つの最大の違いは、関数を即座に実行せず、this がバインドされた新しい関数を返す点です。
const fn2 = obj.speak.bind(obj2);
console.log(fn2); // 新しい関数を返す
fn2(『こんにちは』, 『私はユーザーDです』); // この時点で this はすでに obj2 にバインドされている
これは、setTimeoutやイベントリスナーなど、非同期のシナリオで非常に役立ちます:
// common.js 内の例
const addItemBind = addItem.bind(obj);
oForm.addEventListener(『submit』, addItemBind);
ここでは、bindを使用して、addItem関数のthisをobjに恒久的にバインドしています。これにより、イベントが発生した際、thisはフォーム要素ではなく、指定したオブジェクトを指すようになります。
4.4 非同期処理における this の落とし穴
2.html には、次のようなコメントアウトされたコードがあります:
setTimeout((function() {
console.log(this.name)
}).bind(this), 1000);
なぜ bind(this) を使う必要があるのでしょうか?それは、setTimeout では、コールバック関数が通常の関数として呼び出されるため、this が window を指してしまうからです(非厳格モードの場合)。this を現在のコンテキストに保持させるためには、手動でバインドする必要があります。
一方、矢印関数はこの問題を自然に解決しています:
setTimeout(() => {
console.log(this.name)
}, 1000);
五、矢印関数:this を持たない「異色の存在」
矢印関数は ES6 で導入された構文であり、独自の this を持たず、代わりに外側のスコープの this を継承します。
var name = 「ユーザーA」;
let obj = {
name: 「ユーザーB」,
say: function() {
setTimeout(() => {
console.log(this.name); // say 関数の this を継承 → obj
}, 1000);
}
}
obj.say(); // 1秒後に出力:ユーザーB
sayメソッド内では、矢印関数のthisは外側のsay関数のthis(つまりobj)を引き継ぐため、「ユーザーB」が出力されます。
通常の関数を使用する場合:
setTimeout(function() {
console.log(this.name); // 通常の関数。this は window を指す
}, 1000);
// 出力:ユーザーA(非厳格モード)
これが、非同期コールバックにおいて矢印関数が広く支持されている理由です------これにより、thisの動的バインディングの問題を回避できるからです。
ただし、矢印関数はコンストラクタとして使用できず(newは使用不可)、call、apply、bindを使用して this を変更することもできません(そもそも独自の this を持たないため)。
六、総合的な考察:フォーム送信から this のバインディングまで
これらの知識を結びつけて、完全なシナリオを見てみましょう:
common.js では、bind を使用して、addItem の this を obj にバインドしています:
const oForm = document.querySelector(『.add-items』);
let obj = {
name: 「ユーザーC」,
// ...
}
const addItemBind = addItem.bind(obj);
oForm.addEventListener(『submit』, addItemBind);
なぜこのようにするのでしょうか?それは、イベントリスナー関数のデフォルトの this が DOM 要素(oForm)を指しているのに対し、addItem 関数内で obj のデータやメソッドにアクセスしたいからです。bind を使用することで、 イベントオブジェクト e を保持しつつ、this を目的のオブジェクトに向けることができます。
もし call や apply を使用すると、関数が即座に実行されてしまいますが、addEventListener には(イベントが発生した際に呼び出される)関数参照が必要であるため、bind が最適な選択肢となります。
7. まとめ
| 概念 | 核心となるポイント |
|---|---|
| データストレージ | MySQL(永続化)→ Redis(キャッシュによる高速化)→ LocalStorage(ブラウザストレージ)→ Embedding(AIストレージ)、それぞれに適したシナリオがあります |
| フォーム送信 | デフォルトでは送信時にページがリロードされるが、e.preventDefault() + Ajax を使用することで、ページをリロードせずに送信を実現 |
| this の参照先 | 関数の実行時に決定され、呼び出し方法(通常の呼び出し、メソッド呼び出し、コンストラクタ、イベントハンドラ)によって異なる |
| call / apply | 即時実行、thisを手動で指定。違いは引数の渡し方(個別に vs 配列) |
| bind | 新しい関数を返し、実行を遅延させる。イベントリスナーや非同期コールバックに適している |
| 矢印関数 | 独自の this を持たず、外側のスコープを継承する。コードを簡潔にできるが、使用シーンは限定的 |
最後に考えること :this を理解する鍵は、ルールを暗記することではなく、「関数が呼び出されたとき、誰がそれを呼び出しているのか」を理解することにある 。また、ストレージを理解する鍵は、「データがいつ、どのような方法でアクセスされるか」を把握することにある。この二つは一見異なるように見えるが、どちらも同じ核心――コンテキスト(Context)に対する深い理解――を指し示している。
この記事が、これらの概念を整理し、開発をよりスムーズに進める一助となれば幸いだ。ご質問があれば、コメント欄でぜひ議論しましょう!
この記事のすべてのサンプルコードは、添付の HTML ファイルで実行・検証できます。