Java経験者のためのJavaScirpt入門
1. JavaScriptの概要
こんにちは。ゆうせいです。
Javaに慣れ親しんだ皆さん、ようこそJavaScriptの世界へ!
この章では、Javaとの違いを意識しながら、JavaScriptがどのような言語で、どのように動作するのか、その全体像を掴んでいきます。サーバーサイドで堅牢なアプリケーションを構築してきた皆さんが、今度はブラウザという新しい舞台で、ユーザーの操作に即座に反応する動的なフロントエンドを構築するための第一歩です。
1.1 JavaScriptとは(歴史と特徴)
JavaScriptは、主にウェブブラウザ上で動作し、ウェブページに動的な機能や対話性を持たせるために使われるプログラミング言語です。
元々は1995年にNetscape社によって、ブラウザ内で簡単な処理を行うために開発されました。その歴史の中でECMAScriptという標準規格が策定され、現在では単なるブラウザ上の言語に留まらず、Node.jsを使えばサーバーサイドでも動作する、非常に汎用性の高い言語へと進化しています。
主な特徴:
- インタープリタ型言語: Javaのように事前にコンパイルする必要がなく、ブラウザがコードを解釈しながら直接実行します。
- マルチパラダイム: 手続き型、オブジェクト指向、関数型といった複数のプログラミングスタイルをサポートしており、柔軟な記述が可能です。
- クライアントサイドで実行: 主な実行環境はユーザーのブラウザです。サーバーに問い合わせることなく、ユーザーのアクションに即座に反応できます。
1.2 Javaとの主な違い
JavaとJavaScriptは名前が似ていますが、全く異なる言語です。Javaエンジニアが最初に戸惑うポイントを3つ見ていきましょう。
項目 | Java | JavaScript |
型付け | 静的型付け | 動的型付け |
コンパイル | 必要(.java → .class ) | 不要(ブラウザが直接解釈) |
実行環境 | JVM(サーバー、PC、Android等) | ブラウザ(クライアントサイド) |
静的型付け vs 動的型付け
これが最も大きな違いです。Javaでは変数を宣言する際に型を明示します。
// Java: 型を最初に宣言し、違う型の代入はコンパイルエラー
String message = "Hello";
// message = 123; // Error!
一方、JavaScriptでは型を宣言しません。値が代入されたときに型が決まります。
// JavaScript: letで宣言し、何でも代入可能
let message = 'Hello'; // この時点では文字列型
message = 123; // エラーにならない! この時点で数値型になる
この動的型付けは、柔軟で書きやすい反面、意図しない型が入ってしまうバグを生みやすいという側面もあります。
コンパイル不要
Java開発では「ソースコードを書いて → コンパイルして → 実行」というサイクルが基本です。
JavaScriptにはこのコンパイルのステップがありません。書いたコードはそのままブラウザに読み込ませて実行します。開発サイクルが非常に速いのが特徴です。
(※内部的にはブラウザのエンジンがJITコンパイルなどで高度な最適化を行っていますが、開発者が意識する必要はありません)
1.3 実行環境(ブラウザ)と開発者ツール
JavaScriptの主な実行環境は、Chrome, Firefox, Edgeといったウェブブラウザです。ブラウザにはJavaScriptエンジンが搭載されており、これがコードを解釈・実行します。
JavaでいうIDEのデバッガのように、ブラウザには非常に強力な開発者ツールが標準で備わっています。
開発者ツールの開き方:
- Windows:
F12
キー またはCtrl + Shift + I
- Mac:
Command + Option + I
特に「Console(コンソール)」タブは、JavaのSystem.out.println()
のように、変数の内容を確認したり、簡単なコードを試したりするのに頻繁に使います。
// このコードをコンソールに打ち込んでEnterキーを押してみてください
console.log('Hello from Developer Tools!');
1.4 HTML内での読み込み方法
JavaScriptはHTMLファイル内で<script>
タグを使って読み込みます。方法は主に2つです。
1. HTML内に直接記述する(インライン)
短いコードを試す際に使います。
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>こんにちは</h1>
<script>
// HTML内に直接スクリプトを記述
console.log('これはインラインスクリプトです。');
</script>
</body>
</html>
2. 外部ファイルとして読み込む
一般的にはこちらの方法を使います。HTMLとJavaScriptの関心を分離でき、コードの再利用性や管理性が高まります。
console.log('これは外部ファイルから読み込まれました。');
HTML
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>こんにちは</h1>
<script src="my-script.js"></script>
</body>
</html>
defer と async 属性
<script>
タグを置く場所は重要です。通常は</body>
の直前に置くことが推奨されますが、defer
やasync
属性を使うことで、読み込みのタイミングを制御できます。
<script src="path.js"></script>
- 通常の読み込み。ブラウザはスクリプトのダウンロードと実行が終わるまで、ページの描画を停止します。
<script defer src="path.js"></script>
- ページの描画を止めずにスクリプトをダウンロードし、HTMLの解析が終わった後に実行します。複数の
defer
スクリプトは記述された順に実行されるため、最も安全で推奨される方法です。
- ページの描画を止めずにスクリプトをダウンロードし、HTMLの解析が終わった後に実行します。複数の
<script async src="path.js"></script>
- ページの描画を止めずにスクリプトをダウンロードし、ダウンロードが完了次第すぐに実行します。実行順序は保証されないため、他のスクリプトに依存しない独立した処理(広告やアクセス解析など)に向いています。
以上がJavaScriptの概要です。Javaとの根本的な違いを理解し、ブラウザという実行環境に慣れることが最初のステップです。
次の章では、いよいよ具体的な文法について学んでいきましょう。
2. 基本文法
この章では、JavaScriptの基本的な文法を学びます。Javaと似ている部分も多いですが、「型」の扱いや変数のスコープなど、JavaScript特有の重要な違いがいくつも存在します。これらの違いをしっかり押さえることが、後の学習をスムーズに進める鍵となります。
2.1 変数宣言(var
/ let
/ const
の違い)
Javaでは変数を宣言する方法は基本的に一つでしたが、JavaScriptには3つのキーワードがあります。現在では var
を使うことは推奨されておらず、基本的に const
を使い、再代入が必要な場合のみ let
を使うのがベストプラク'ティスです。
キーワード | 再宣言 | 再代入 | スコープ | 特徴 |
const | ❌ | ❌ | ブロック | 推奨。定数(一度代入したら変更しない変数)を宣言する。 |
let | ❌ | ✅ | ブロック | 再代入が必要な変数を宣言する。 |
var | ✅ | ✅ | 関数 | 非推奨。古い書き方。意図しない動作の原因になりやすい。 |
constは再代入できません。ただし、constで宣言したオブジェクトや配列の「中身」は変更可能です。以下のサンプルコードを見てください。
// 【追加】constは再代入できないことを示す例
const PAI = 3.14;
// PAI = 3.14159; // TypeError: Assignment to constant variable.
// ただし、constで宣言したオブジェクトや配列の「中身」は変更可能
const user = { name: "Tanaka" };
user.name = "Sato"; // これはエラーにならない
console.log(user.name); // -> "Sato"
// user = { name: "Suzuki" }; // これは再代入なのでエラーになる
【Javaとの比較:スコープの扱い】
Javaの変数は基本的にブロックスコープ({}
で囲まれた範囲)です。let
と const
も同じブロックスコープなので、Java経験者には直感的でしょう。
// letとconstはブロックスコープ
function testScope() {
if (true) {
let blockVar = "I'm in a block";
console.log(blockVar); // -> I'm in a block
}
// console.log(blockVar); // Error! blockVar is not defined
}
一方、var
は関数スコープという少し特殊な動きをします。
// varは関数スコープ
function testVarScope() {
if (true) {
var functionVar = "I'm in a function";
}
console.log(functionVar); // -> I'm in a function (エラーにならない!)
}
このように、var
はブロックを越えて存在できてしまうため、大規模な開発ではバグの原因となりがちです。
2.2 データ型
JavaScriptのデータ型はプリミティブ型とオブジェクト型に大別されます。
プリミティブ型:
- 文字列 (String):
'Hello'
や"World"
のようにシングルクォートまたはダブルクォートで囲みます。 - 数値 (Number):
100
,3.14
のように整数・小数を区別なく扱います。 - 真偽値 (Boolean):
true
またはfalse
。 - null: 「値がない」ことを意図的に示す値。
- undefined: 【Javaにない型】 変数を宣言したが、まだ値が代入されていない状態。
- シンボル (Symbol): ES2015から追加された、一意で不変のデータ型。オブジェクトのプロパティキーが衝突しないようにする目的などで使われます。
オブジェクト型:
- オブジェクト (Object): キーと値のペアの集まり。
{ name: 'Taro', age: 30 }
のように記述します。 - 配列 (Array): 複数の値を順序付けて格納するオブジェクト。
[1, 'apple', true]
のように記述します。 - 関数 (Function): 処理をまとめたもの。JavaScriptでは関数もオブジェクトの一種です。
2.3 演算子
算術演算子(+
, -
, *
, /
)や比較演算子(>
, <
)、論理演算子(&&
, ||
)などはJavaとほぼ同じ感覚で使えます。しかし、JavaScriptには型に関する重要な演算子があります。
【===
と ==
の型比較違い】
これはJavaScriptの非常に重要な特徴です。
==
(等価演算子): 型が違っても、よしなに変換して比較する。(例:1 == '1'
はtrue
になる)===
(厳密等価演算子): 型も含めて厳密に比較する。(例:1 === '1'
はfalse
になる)
意図しないバグを防ぐため、常に ===
を使うことを強く推奨します。
console.log(10 == '10'); // -> true (型を自動変換して比較)
console.log(10 === '10'); // -> false (型が違うのでfalse)
console.log(0 == false); // -> true
console.log(0 === false); // -> false
2.4 テンプレートリテラル
Javaの String.format()
や文字列連結(+
)よりもはるかに直感的で強力な文字列組み立て機能です。バッククォート (`
) を使い、${}
の中に変数や式を埋め込むことができます。
Javaの場合:
String name = "Yamazaki";
int age = 30;
String message = String.format("私の名前は%sです。年齢は%d歳です。", name, age);
JavaScriptのテンプレートリテラル:
const name = 'Yamazaki';
const age = 30;
const message = `私の名前は${name}です。年齢は${age}歳です。`; // こちらの方が直感的!
console.log(message); // -> 私の名前はYamazakiです。年齢は30歳です。
改行もそのまま反映されるため、複数行の文字列を扱う際にも非常に便利です。
2.5 コメントの書き方
コメントの書き方はJavaと全く同じです。
// 1行コメント
/*
複数行コメント
も同じように
書くことができます。
*/
以上が、JavaScriptの基本的な文法です。特に変数宣言の let
/const
の使い分けと、比較演算子の ===
の使用は、現代的なJavaScriptを書く上で必須の知識です。
次の章では、if
文やfor
ループといった制御構文について見ていきましょう。
3. 制御構文
プログラムの流れを制御するための構文は、Javaと非常に似通っており、多くのJavaプログラマーにとって学習コストが低い部分です。しかし、switch
文の型比較や、Javaの拡張forループに似た新しいループ構文など、注意すべき違いも存在します。
3.1 if
/ else
/ else if
Javaと全く同じです。
条件分岐の基本的な構文は、Javaと変わりありません。()内の条件がtrueと評価されれば、続く{}内のブロックが実行されます。
const score = 85;
if (score >= 90) {
console.log('A評価です');
} else if (score >= 80) {
console.log('B評価です'); // -> このブロックが実行される
} else {
console.log('C評価です');
}
3.2 switch
文
構文はJavaとほぼ同じで、break
を書き忘れると次のcase
まで処理が続いてしまう「フォールスルー」の挙動も共通です。
ただし、比較の挙動が異なります。Javaのswitch
文が内部的に.equals()
や==
で比較するのに対し、JavaScriptのswitch
文は厳密等価演算子(===
)で比較を行います。つまり、値と型の両方が一致しないとcase
に合致しません。
const value = '1';
switch (value) {
case 1: // number型
console.log('数値の1です');
break;
case '1': // string型
console.log('文字列の1です'); // -> ここが実行される
break;
default:
console.log('どちらでもありません');
break;
}
この挙動の違いを覚えておかないと、意図しないdefault
処理に流れることがあるため注意が必要です。
3.3 for
/ while
/ do-while
これらの基本的なループ構文は、Javaと全く同じです。
// forループ (Javaと同じ)
for (let i = 0; i < 5; i++) {
console.log(`現在のカウント: ${i}`);
}
// whileループ (Javaと同じ)
let count = 0;
while (count < 3) {
console.log('whileループ');
count++;
}
3.4 for...of
/ for...in
Javaの拡張forループ (for (String str : stringList)
) に相当するのがfor...of
です。配列などの反復可能(iterable)なオブジェクトの値を順番に取り出す際に使います。
for...of
(Javaの拡張forループに相当)
配列の各要素に対して処理を行いたい場合に最適です。
const fruits = ['apple', 'banana', 'cherry'];
// Java: for (String fruit : fruits)
for (const fruit of fruits) {
console.log(fruit);
}
// 出力:
// apple
// banana
// cherry
for...in
(オブジェクトのプロパティを走査)
for...in
は、オブジェクトが持つプロパティ名(キー)を順番に取り出すためのループです。JavaのMapのキーセットをループする感覚に近いです。
const user = {
name: 'Sato',
age: 28,
country: 'Japan'
};
for (const key in user) {
console.log(`${key}: ${user[key]}`);
}
// 出力:
// name: Sato
// age: 28
// country: Japan
注意: for...in
を配列に使うと、0
, 1
, 2
... というインデックスが文字列として取得されるため、配列の要素を扱いたい場合はfor...of
を使いましょう。
3.5 break
/ continue
Javaと全く同じです。
breakはループを完全に中断し、continueは現在の繰り返しをスキップして次の繰り返しに進みます。
以上がJavaScriptの制御構文です。ほとんどの構文はJavaの知識をそのまま活かせますが、switch
文の厳密比較と、for...of
/ for...in
の使い分けはJavaScript特有のポイントとしてしっかり押さえておきましょう。
次の章では、JavaScriptの根幹をなす「関数」について掘り下げていきます。
4. 関数
JavaScriptにおいて、関数は単なる処理のまとまり以上の存在です。Javaのメソッドとは異なり、クラスに属さず独立して存在でき、変数に代入したり、他の関数の引数として渡したりできます。このような性質を「第一級オブジェクト(First-class citizen)」と呼び、JavaScriptの柔軟で強力なプログラミングを支える中心的な機能です。
4.1 関数宣言と関数式
JavaScriptには、関数を定義する方法が主に2つあります。
1. 関数宣言 (Function Declaration)
最も基本的な、名前を付けた関数の定義方法です。
// 関数宣言
function greet(name) {
return `Hello, ${name}!`;
}
// 呼び出し
console.log(greet('Sato')); // -> Hello, Sato!
この方法は「巻き上げ(hoisting)」という特徴があり、コード上のどこで定義しても、そのスコープの開始地点から呼び出すことが可能です。
2. 関数式 (Function Expression)
関数をオブジェクトとして扱い、変数に代入する方法です。多くの場合、const
と組み合わせて使われます。
// 関数式(無名関数を変数に代入)
const greet = function(name) {
return `Hello, ${name}!`;
};
// 呼び出し
console.log(greet('Suzuki')); // -> Hello, Suzuki!
【Javaのメソッドとの違い】
最大のポイントは、クラス定義が不要であることです。Javaではすべてのメソッドがいずれかのクラスに属している必要がありますが、JavaScriptの関数はトップレベルで自由に定義できます。
4.2 アロー関数 (Arrow Functions)
ES2015から導入された、より簡潔に関数を書くための構文です。Javaのラムダ式 (->
) に非常によく似ています。
通常の関数式:
const add = function(a, b) {
return a + b;
};
アロー関数:
// functionキーワードを省略し、=> を使う
const add = (a, b) => {
return a + b;
};
// 処理が1行で、その結果をreturnするだけなら、{}とreturnも省略可能
const subtract = (a, b) => a - b;
console.log(add(5, 3)); // -> 8
console.log(subtract(10, 4)); // -> 6
【Javaラムダ式との違い】
構文は似ていますが、大きな違いは this キーワードの扱いです。通常の関数では this が呼び出され方によって変わるのに対し、アロー関数は this を束縛せず、外側のスコープの this をそのまま引き継ぎます。この違いは、後々オブジェクト指向プログラミングやイベント処理で重要になります。
4.3 引数とデフォルト値
JavaScriptの関数の引数は非常に柔軟です。
- 引数の数が合わなくてもエラーにならない(足りない引数は
undefined
になる)。 - 引数にデフォルト値を設定できる。
// nameのデフォルト値を 'Guest' に設定
function welcome(name = 'Guest', message = 'Welcome!') {
console.log(`${name}, ${message}`);
}
welcome('Tanaka', 'Hello!'); // -> Tanaka, Hello!
welcome('Sato'); // -> Sato, Welcome! (messageはデフォルト値が使われる)
welcome(); // -> Guest, Welcome! (両方デフォルト値)
【Javaのオーバーロードとの違い】
Javaでは、引数の型や数が異なる同名メソッドを複数定義する「オーバーロード」で同様の機能を実現します。JavaScriptにはオーバーロードの概念がなく、代わりに1つの関数とデフォルト値で柔軟に対応します。
4.4 可変長引数 (Rest Parameters)
Javaの可変長引数 (String... args
) と非常によく似た機能として、「残余引数 (Rest Parameters)」があります。...
を付けた引数に、残りの引数がすべて配列として格納されます。
// Java: public int sum(int... numbers)
function sum(...numbers) {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}
console.log(sum(1, 2, 3)); // -> 6
console.log(sum(10, 20, 30, 40)); // -> 100
4.5 戻り値
return 文の動作はJavaと同じですが、一つ大きな違いがあります。
Javaでは戻り値の型を宣言したメソッドは必ずreturn文で値を返す必要がありますが、JavaScriptではreturn文がない場合、暗黙的に undefined が返されます。
function doSomething() {
// return文がない
console.log('処理を実行');
}
const result = doSomething();
console.log(result); // -> undefined
4.6 スコープとクロージャ
クロージャ(Closure)は、多くのJavaエンジニアが初めて遭遇するJavaScriptの重要な概念です。
クロージャとは、「自身が定義されたときの環境(スコープ)を記憶している関数」のことです。
言葉だけでは難しいので、例を見てみましょう。
// ①カウンターを作成する関数
function createCounter() {
let count = 0; // この変数が「記憶」される
// ②内部関数を定義して、それを返す
return function() {
count++;
console.log(count);
};
}
// createCounterを実行すると、内部の関数が返される
const counterA = createCounter();
const counterB = createCounter();
// 返された関数を実行する
counterA(); // -> 1
counterA(); // -> 2 (count変数が維持されている)
counterB(); // -> 1 (counterAとは別のcount変数を持っている)
counterA(); // -> 3
createCounter
関数は実行を終えているにも関わらず、その内部変数だったcount
が、返された内部関数からアクセスされ続けています。このように、関数の外側にある変数を内部に閉じ込めて(close over)、後からでも使えるようにする仕組みがクロージャです。
これはJavaの「カプセル化」の考え方に似ており、プライベートな状態を持つ関数を作成する際などに強力な武器となります。
以上がJavaScriptの関数に関する基本です。関数が単なる手続きではなく「値」として扱えること、そしてクロージャの概念を理解することが、JavaScriptを使いこなす上で非常に重要です。
次の章では、JavaのMap
やArrayList
のようにデータを扱うための「オブジェクト」と「配列」を学びます。
5. オブジェクトと配列
JavaScriptにおいて、オブジェクトと配列は最も基本的なデータ構造です。Javaの厳密なクラス定義や型付けされた配列・Listとは異なり、非常に柔軟で動的な性質を持っています。この章では、JavaのMap
やArrayList
との対比を交えながら、その強力な使い方を学びます。
5.1 オブジェクトリテラル
JavaScriptのオブジェクトは、キーと値のペアを持つ単純なデータ集合です。JavaのMap<String, Object>
に非常に近いものだと考えてください。クラスを定義しなくても、{}
(波括弧)を使う「オブジェクトリテラル」記法で手軽にオブジェクトを作成できます。
// オブジェクトリテラルでオブジェクトを作成
const user = {
name: 'Taro Yamazaki', // キー: 値
age: 35,
isAdmin: true,
// 値には関数も設定でき、この場合は「メソッド」と呼ぶ
greet: function() {
console.log(`Hello, I'm ${this.name}.`);
}
};
// プロパティへのアクセス
// 1. ドット記法 (Javaのフィールドアクセスと同じ)
console.log(user.name); // -> Taro Yamazaki
// 2. ブラケット記法 (キーを文字列で指定。Mapのgetに近い)
console.log(user['age']); // -> 35
// メソッドの呼び出し
user.greet(); // -> Hello, I'm Taro Yamazaki.
// プロパティの追加と変更
user.country = 'Japan'; // 新しいプロパティを動的に追加
user.age = 36; // 既存のプロパティを更新
5.2 配列の作成と基本操作
JavaScriptの配列は、Javaの固定長配列とArrayList
の両方の性質を併せ持ったような、動的で便利なデータ構造です。[]
(角括弧)を使う「配列リテラル」で簡単に作成できます。
主な特徴:
- サイズが動的: 要素を自由に追加・削除できます。
- 異なるデータ型を混在可能:
[1, 'apple', true]
のような配列も作れます。
const fruits = ['apple', 'banana', 'cherry'];
// 長さの取得 (Javaの .length や .size() に相当)
console.log(fruits.length); // -> 3
// インデックスによるアクセス (Javaの配列と同じ)
console.log(fruits[0]); // -> apple
// 末尾に要素を追加 (ArrayListの add と同じ)
fruits.push('orange');
console.log(fruits); // -> ['apple', 'banana', 'cherry', 'orange']
// 末尾の要素を削除
const removedFruit = fruits.pop();
console.log(removedFruit); // -> orange
console.log(fruits); // -> ['apple', 'banana', 'cherry']
配列の先頭要素の追加・削除、任意の位置の要素を置換・削除のサンプルコードです。
const colors = ['red', 'green', 'blue'];
// 【追加】先頭に要素を追加
colors.unshift('yellow');
console.log(colors); // -> ['yellow', 'red', 'green', 'blue']
// 【追加】先頭の要素を削除
const firstColor = colors.shift();
console.log(firstColor); // -> 'yellow'
console.log(colors); // -> ['red', 'green', 'blue']
// 【追加】任意の位置の要素を置換・削除 (splice)
// 2番目(インデックス1)の位置から1つの要素を削除し、'lightgreen'を追加
colors.splice(1, 1, 'lightgreen');
console.log(colors); // -> ['red', 'lightgreen', 'blue']
5.3 配列の高階関数
ここがモダンJavaScriptの真骨頂です。JavaのStream API (.stream().map().filter()...
) と全く同じ考え方で、配列を非常に簡潔に、かつ宣言的に操作できます。これらのメソッドは元の配列を変更せず、新しい配列を返すのが特徴です(非破壊的)。
メソッド | 説明 | Java Stream API |
forEach | 各要素に対して、指定された関数を一度ずつ実行する。 | .forEach() |
map | 各要素を変換した新しい配列を生成する。 | .map() |
filter | 条件に合う要素だけを抽出した新しい配列を生成する。 | .filter() |
reduce | 全要素を一つの値に集約する(合計値の計算など)。 | .reduce() |
const numbers = [1, 2, 3, 4, 5];
// forEach: 各要素をコンソールに出力
numbers.forEach(num => {
console.log(num);
});
// map: 各要素を2倍した新しい配列を作成
const doubled = numbers.map(num => num * 2);
console.log(doubled); // -> [2, 4, 6, 8, 10]
// filter: 偶数だけを抽出した新しい配列を作成
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // -> [2, 4]
// reduce: 全要素の合計を計算
// (totalが累積値, currentが現在の要素, 0はtotalの初期値)
const sum = numbers.reduce((total, current) => total + current, 0);
console.log(sum); // -> 15
5.4 分割代入
Javaにはない、JavaScriptの便利な構文です。 オブジェクトや配列から、必要な値を取り出して個別の変数に代入できます。
オブジェクトの分割代入
const person = {
fullName: 'Hanako Suzuki',
age: 28,
country: 'Japan'
};
// personオブジェクトからfullNameとageプロパティを取り出して変数に代入
const { fullName, age } = person;
console.log(fullName); // -> Hanako Suzuki
console.log(age); // -> 28
配列の分割代入
const colors = ['red', 'green', 'blue'];
// 配列の先頭から順番に変数に代入
const [firstColor, secondColor] = colors;
console.log(firstColor); // -> red
console.log(secondColor); // -> green
この構文は、Reactなどのフレームワークで頻繁に使われます。
5.5 スプレッド構文
こちらもJavaにはない強力な構文です。...
(ドット3つ)を使い、オブジェクトや配列を展開して、その中身を取り出します。
配列の結合やコピー
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 配列の結合
const combined = [...arr1, ...arr2];
console.log(combined); // -> [1, 2, 3, 4, 5, 6]
// 配列のコピー(シャローコピー)
const arr1Copy = [...arr1];
console.log(arr1Copy); // -> [1, 2, 3]
オブジェクトのマージやコピー
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
// オブジェクトのマージ
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // -> { a: 1, b: 2, c: 3, d: 4 }
// オブジェクトのコピー
const obj1Copy = { ...obj1 };
console.log(obj1Copy); // -> { a: 1, b: 2 }
スプレッド構文は、前章で学んだ残余引数(...args
)と同じ記号を使いますが、使われる文脈によって意味が変わるので注意してください。
以上がオブジェクトと配列の基本です。Javaの厳格なデータ構造に比べ、その柔軟性に最初は戸惑うかもしれませんが、map
やfilter
といった高階関数、分割代入、スプレッド構文を使いこなすことで、非常に簡潔で表現力豊かなコードを書くことができます。
次の章では、いよいよブラウザを操作する「DOM操作」について学びます。
6. DOM操作
ここからが、ブラウザで動作するJavaScriptの真骨頂です。DOM (Document Object Model) を操作することで、JavaScriptはHTMLドキュメントを動的に変更し、ユーザーのアクションに応じて画面を書き換えることができます。この章で学ぶ内容は、あらゆるフロントエンド開発の基礎となります。
6.1 DOMとは
DOMとは、ブラウザがHTMLドキュメントを解釈して作り上げる、オブジェクトのツリー構造表現のことです。JavaScriptは、このツリー構造を介してHTMLの各要素にアクセスし、それらを操作します。
【JavaのSwing/AWTのUI部品ツリーに類似】
Javaでデスクトップアプリを開発した経験があるなら、これはJFrameの中にJPanelがあり、さらにその中にJButtonやJLabelが配置されるUIコンポーネントの階層構造と非常によく似ています。
HTMLの<body>
タグがルートパネルで、その中の<div>
や<p>
、<img>
タグが個々のUI部品に相当すると考えると、非常に理解しやすいでしょう。
JavaScriptの役割は、このDOMツリーから特定の「部品(要素)」を見つけ出し、そのテキストを書き換えたり、色を変えたり、新しい部品を追加したりすることです。
6.2 要素の取得
DOM操作の第一歩は、操作したいHTML要素(DOMノード)を取得することです。
getElementById
最も古くからあり、高速な方法です。HTMLタグに付けられたユニークなid
属性を頼りに要素を一つだけ取得します。
HTML:
<h1 id="main-title">ようこそ</h1>
JavaScript:
// idが "main-title" の要素を取得
const titleElement = document.getElementById('main-title');
console.log(titleElement);
querySelector
/ querySelectorAll
よりモダンで強力な方法です。CSSセレクタと同じ記法で、柔軟に要素を取得できます。
querySelector
: セレクタに一致する最初の1つの要素を返します。querySelectorAll
: セレクタに一致するすべての要素をNodeList
(配列のようなもの)として返します。
HTML:
<div class="content">
<p>最初の段落です。</p>
<p class="highlight">2番目の段落です。</p>
</div>
JavaScript:
// classが "highlight" の要素を取得
const highlightP = document.querySelector('.highlight');
// divタグの中にあるpタグをすべて取得
const allParagraphs = document.querySelectorAll('div.content p');
console.log(highlightP.textContent); // -> 2番目の段落です。
// allParagraphsは配列のように扱える
allParagraphs.forEach(p => {
console.log(p.textContent);
});
現在では、このquerySelector
系を使うのが主流です。
6.3 要素の内容変更
取得した要素の中身を書き換えるには、主に2つのプロパティを使います。
textContent
: 要素内のテキストのみを操作します。HTMLタグはすべて無視(除去)されます。innerHTML
: 要素内のHTML構造を含めて操作します。
const message = document.getElementById('message');
// テキストとして設定(安全)
message.textContent = 'これは<strong>重要</strong>なメッセージです。';
// 画面上の表示: これは<strong>重要</strong>なメッセージです。
// HTMLとして設定
message.innerHTML = 'これは<strong>重要</strong>なメッセージです。';
// 画面上の表示: これは重要なメッセージです。
セキュリティ注意: ユーザーが入力した内容など、信頼できない文字列をinnerHTML
に設定すると、XSS(クロスサイトスクリプティング)という脆弱性の原因になります。テキストを扱う際は、原則としてtextContent
を使いましょう。
6.4 属性の操作
<a>
タグのhref
属性や、<img>
タグのsrc
属性などを操作します。
const link = document.querySelector('#my-link');
const image = document.querySelector('#my-image');
// 属性値を取得
const currentUrl = link.getAttribute('href');
console.log(currentUrl);
// 属性値を設定
link.setAttribute('href', 'https://www.google.com');
image.setAttribute('src', '/images/new-image.png');
// idやhrefなどの主要な属性は、プロパティとしても直接アクセス可能
// link.href = 'https://www.google.com';
6.5 CSSクラスの操作
JavaScriptで直接element.style.color = 'red'
のようにスタイルを書き換えることもできますが、一般的にはCSSクラスを付け外しすることで見た目を変更する方が、関心の分離ができて保守性が高まります。そのためにclassList
プロパティを使います。
const alertBox = document.querySelector('#alert');
// 'visible'クラスを追加
alertBox.classList.add('visible');
// 'error'クラスを削除
alertBox.classList.remove('error');
// 'active'クラスがあれば削除、なければ追加(トグル)
alertBox.classList.toggle('active');
// 'visible'クラスを持っているか確認 (true/false)
if (alertBox.classList.contains('visible')) {
console.log('表示されています');
}
6.6 要素の追加・削除
JavaScriptで新しいHTML要素を動的に作成し、DOMツリーに追加したり、既存の要素を削除したりできます。
1. 要素を作成: document.createElement('タグ名')
2. 要素を親に追加: parentElement.appendChild(newElement)
3. 要素を削除: element.remove()(モダンな方法)
// <ul>要素を取得
const list = document.querySelector('#item-list');
// 1. <li>要素を新規作成
const newItem = document.createElement('li');
newItem.textContent = '新しいアイテム'; // 中身のテキストを設定
// 2. 作成した<li>を<ul>の子として末尾に追加
list.appendChild(newItem);
// 既存の要素を削除
const itemToRemove = document.querySelector('#item-to-remove');
if (itemToRemove) {
// モダンな書き方
itemToRemove.remove();
// 古典的な書き方
// itemToRemove.parentNode.removeChild(itemToRemove);
}
以上がDOM操作の基本です。「①要素を取得し → ②内容や属性、クラスを操作する → ③時には要素を追加・削除する」という流れが、インタラクティブなウェブページ作成の核となります。
次の章では、ユーザーのクリックやキーボード入力といったアクションを捉える「イベント処理」について学びます。
7. イベント処理
DOM操作によってウェブページの見た目を変更する方法を学びましたが、それだけでは静的なページの書き換えにすぎません。イベント処理は、ユーザーのアクション(イベント)をきっかけにJavaScriptのコードを実行するための仕組みです。アプリケーションはインタラクティブ(対話的)になります。
7.1 イベントとは
イベントとは、ウェブページ上で発生する出来事のことです。
- ユーザーのアクション
- ボタンがクリックされた
- マウスカーソルが要素の上に乗った
- フォームの入力内容が変更された
- キーボードのキーが押された
- ブラウザの状態変化
- ページの読み込みが完了した
- ウィンドウサイズが変更された
【JavaのListenerモデルに近い】
これは、Java Swing/AWTのイベント処理と全く同じ考え方です。JButtonにActionListenerを登録し、actionPerformedメソッド内にクリックされたときの処理を書くのと同様に、JavaScriptではHTML要素(イベントターゲット)にイベントリスナー(処理内容を記述した関数)を登録します。
7.2 イベントリスナーの登録
イベントを処理するための最もモダンで推奨される方法は、addEventListener
メソッドを使うことです。
構文:
要素.addEventListener('イベント名', 実行したい関数);
例:ボタンがクリックされたらメッセージを表示する
HTML:
<button id="my-button">クリックしてください</button>
JavaScript:
// 1. 対象の要素を取得
const button = document.getElementById('my-button');
// 2. 実行したい処理を関数として定義
function handleClick() {
alert('ボタンがクリックされました!');
}
// 3. 要素にイベントリスナーを登録
button.addEventListener('click', handleClick);
アロー関数を使えば、より簡潔に書くこともできます。
const button = document.getElementById('my-button');
button.addEventListener('click', () => {
alert('ボタンがクリックされました!');
});
7.3 イベントオブジェクト
イベントリスナーとして登録された関数には、引数としてイベントオブジェクトが自動的に渡されます。このオブジェクトには、発生したイベントに関する詳細な情報が含まれています。
【JavaのEventオブジェクトとの共通点】
ActionEventやMouseEventオブジェクトからイベントソースやマウスの座標を取得したように、JavaScriptのイベントオブジェクトからも同様の情報を取得できます。
const button = document.getElementById('my-button');
button.addEventListener('click', (event) => {
// イベントオブジェクトをコンソールで確認
console.log(event);
// event.target: イベントが発生した要素 (この場合はbutton)
console.log(event.target);
// event.type: イベントの種類 (この場合は 'click')
console.log(event.type);
});
マウスイベントであればevent.clientX
やevent.clientY
で座標を、キーボードイベントであればevent.key
で押されたキーの情報を取得できます。
7.4 フォームイベント
ウェブアプリケーションで頻繁に使われる、フォーム関連の代表的なイベントです。
submit
:<form>
要素が送信されるときに発生します。change
:<input>
,<select>
,<textarea>
などの値が変更され、フォーカスが外れたときに発生します。input
:<input>
や<textarea>
の値が変更されるたびに(一文字ごと)発生します。リアルタイムな入力チェックなどに使われます。
例:submitイベントでページの再読み込みをキャンセルする
<form>のデフォルトの動作は、送信時にページをリロードすることです。これをevent.preventDefault()で防ぐのが定石です。
const myForm = document.getElementById('my-form');
myForm.addEventListener('submit', (event) => {
// デフォルトの送信動作をキャンセル
event.preventDefault();
console.log('フォームが送信されようとしましたが、キャンセルしました。');
// ここで、入力データを取得してAjaxでサーバーに送信するなどの処理を行う
});
7.5 マウス・キーボードイベント
- マウスイベント:
click
: クリックされたときmouseover
: マウスカーソルが要素の上に乗ったときmouseout
: マウスカーソルが要素から離れたときmousemove
: マウスカーソルが要素の上で動いたとき
- キーボードイベント:
keydown
: キーが押されたときkeyup
: キーが離されたときkeypress
: 文字キーが押されたとき(現在はkeydown
を使うのが一般的)
7.6 イベント伝播(バブリングとキャプチャ)
【Javaにはない仕組み】
HTML要素が入れ子になっている場合、内側の要素で発生したイベントは、外側の親要素へと伝わっていきます。この伝播には2つのフェーズがあります。
- キャプチャフェーズ:
window
からイベントターゲットに向かって、親から子へとイベントが伝わっていく段階。 - バブリングフェーズ: イベントターゲットから
window
に向かって、子から親へとイベントが「泡(bubble)が昇るように」伝わっていく段階。
addEventListener
は、デフォルトでバブリングフェーズのイベントを捉えます。
例:バブリング
<div id="parent">
親要素
<button id="child">子要素 (ここをクリック)</button>
</div>
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => {
console.log('親要素のリスナーが実行されました');
});
child.addEventListener('click', () => {
console.log('子要素のリスナーが実行されました');
});
// 子要素をクリックすると、コンソールには以下のように表示される
// -> 子要素のリスナーが実行されました
// -> 親要素のリスナーが実行されました
この伝播は、event.stopPropagation()
を呼び出すことで意図的に止めることができます。この仕組みは、イベントを効率的に管理する「イベント委任(Event Delegation)」というテクニックで活用されます。
// (既存のコードの下に追記)
// 【追加】イベントの伝播を停止する例
child.addEventListener('click', (event) => {
console.log('子要素のリスナーが実行されました(伝播停止)');
event.stopPropagation(); // ここで親要素への伝播を止める
});
// この場合、子要素をクリックしても親要素のリスナーは実行されない
// -> 子要素のリスナーが実行されました(伝播停止)
以上がイベント処理の基本です。「どの要素」で「何が起きたら(イベント)」、「何をするか(リスナー関数)」という3点を設定することが、ユーザーの操作に応答する動的なウェブページを作るための中心的な考え方です。
次の章では、ページ全体をリロードすることなくサーバーと通信する「非同期処理」について学びます。
8. 非同期処理
ウェブアプリケーションがリッチになるにつれて、サーバーからデータを取得したり、重い処理を行ったりする必要が出てきます。もしこれらの処理を同期的 (Synchronous) に行うと、処理が終わるまでブラウザ全体がフリーズしてしまい、ユーザー体験を著しく損ないます。そこで重要になるのが非同期処理 (Asynchronous) です。
8.1 非同期処理の必要性
ブラウザで動作するJavaScriptは、基本的にシングルスレッドで動作します。これは、一度に一つの処理しか実行できないことを意味します。UIの描画、ユーザーのクリックイベントの処理、そしてJavaScriptのコード実行、これらすべてが同じ一つのスレッド(メインスレッド)で順番に処理されます。
もし、時間のかかる処理(例: サーバーへの問い合わせ)を同期的に実行すると、その処理が終わるまでメインスレッドが完全にブロック(占有)されてしまいます。その結果、ユーザーはボタンをクリックすることも、スクロールすることもできなくなり、アプリケーションが「固まった」状態になります。
非同期処理は、この問題を解決します。時間のかかる処理をメインスレッドから切り離してバックグラウンドで実行させ、「処理が終わったら、この関数を実行してね」と予約だけしておくことで、メインスレッドはブロックされずに他のタスク(UIの応答など)を続けることができます。
8.2 コールバック関数
非同期処理の最も古典的な方法がコールバック関数です。これは、「処理が終わった後に呼び出してほしい(call back)関数」を、非同期処理を行う関数に引数として渡すスタイルです。
【JavaのCallback的役割】
Javaでも、特定のイベントが発生したときに実行される処理をメソッドとして渡すコールバックパターンは存在します。それと同じ考え方です。
JavaScriptで非同期処理を手軽に体験できるsetTimeout
関数で例を見てみましょう。
console.log('処理1: 開始');
// 2秒後 (2000ミリ秒後) に、指定したコールバック関数を実行する
setTimeout(() => {
console.log('処理2: 非同期処理が完了しました');
}, 2000);
console.log('処理3: 終了');
// --- 実行結果 ---
// 処理1: 開始
// 処理3: 終了
// (約2秒後)
// 処理2: 非同期処理が完了しました
setTimeout
は非同期のため、すぐに次の処理3
が実行され、2秒後に予約されたコールバック関数が実行されるのがわかります。
しかし、非同期処理が連続すると、コールバック関数が入れ子になり、コードが非常に読みにくくなる「コールバック地獄(Pyramid of Doom)」という問題がありました。
8.3 Promise
コールバック地獄を解決するためにES2015で導入されたのがPromiseです。Promiseは、非同期処理の最終的な完了(または失敗)とその結果の値を表現するオブジェクトです。
【JavaのCompletableFutureとの比較】
Promiseの考え方は、Java 8で導入されたCompletableFutureと非常によく似ています。どちらも「未来のある時点で完了する処理」をオブジェクトとして表現し、処理の成功時(.then() / .thenApply())と失敗時(.catch() / .exceptionally())の処理をメソッドチェーンで繋げることができます。
Promiseは3つの状態を持ちます:
pending
: 処理中fulfilled
: 処理が成功して完了rejected
: 処理が失敗
// 非同期処理をPromiseでラップする関数の例
function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 処理が成功したと仮定
if (success) {
resolve('成功しました!'); // 成功時にresolveを呼ぶ
} else {
reject('失敗しました...'); // 失敗時にrejectを呼ぶ
}
}, 1000);
});
}
// Promiseの利用
asyncTask()
.then(result => {
// 成功した場合の処理 (resolveの値がresultに入る)
console.log(`結果: ${result}`);
})
.catch(error => {
// 失敗した場合の処理 (rejectの値がerrorに入る)
console.error(`エラー: ${error}`);
});
このように、処理の成功と失敗を分離し、メソッドチェーンで記述できるため、コールバックよりもはるかに見通しが良くなります。
8.4 async / await
async / await
は、Promiseをさらに直感的に、まるで同期処理のように書けるようにしたシンタックスシュガー(糖衣構文)です。現代のJavaScript非同期処理の主流となっています。
async
: 関数宣言の前に付けると、その関数は暗黙的にPromiseを返すようになります。await
:async
関数内でのみ使用可能。Promiseが完了するまで関数の実行を一時停止し、完了後に結果を返します。
先ほどのPromiseの例をasync / await
で書き換えてみましょう。
// async / await を使った書き方
async function run() {
console.log('処理を開始します');
try {
// asyncTask()が返すPromiseが完了するのを「待つ」
const result = await asyncTask();
console.log(`結果: ${result}`);
} catch (error) {
console.error(`エラー: ${error}`);
}
console.log('処理が完了しました');
}
run();
then
やcatch
のチェーンがなくなり、上から下に流れる、非常に読みやすいコードになりました。
【Javaの非同期APIとの書き方の違い】
JavaのCompletableFutureがメソッドチェーンとラムダ式を多用するのに対し、async / awaitは命令型の同期的なコードスタイルを維持できる点が大きな違いであり、多くの開発者にとって直感的です。
8.5 エラーハンドリング
非同期処理のエラーハンドリングは、async / awaitを使うと非常にシンプルです。
Javaの例外処理と全く同じ、try-catch構文が使えます。
async function fetchUserData() {
try {
// Promiseを返す非同期関数を待つ
const response = await fetch('/api/user');
if (!response.ok) {
// 自分でエラーを発生させることもできる
throw new Error('ユーザーデータの取得に失敗しました');
}
const data = await response.json();
console.log(data);
} catch (error) {
// await 中のPromiseがrejectされた場合や、throwされたエラーはここでキャッチされる
console.error('エラーが発生しました:', error);
}
}
以上が非同期処理の基本です。コールバックからPromise、そしてasync / await
へと進化してきた歴史を理解し、現代的なasync / await
を使いこなすことが、サーバー通信を伴うリッチなWebアプリケーション開発には不可欠です。
次の章では、この非同期処理を使い、実際にサーバーと通信する「AjaxとAPI連携」について学びます。
9. AjaxとAPI連携
Ajax (Asynchronous JavaScript + XML) は、前章で学んだ非同期処理技術を使い、ページ全体を再読み込みすることなく、サーバーとデータを送受信するための技術の総称です。(現在ではデータの形式としてXMLよりもJSONが主流ですが、名前だけが残っています。)
Ajaxを利用することで、ユーザーの操作に応じて必要なデータだけをサーバーから取得し、画面の一部を動的に更新する、といったモダンなWebアプリケーションの挙動を実現できます。
9.1 Ajaxの概要
従来のWebページでは、ユーザーがリンクをクリックしたりフォームを送信したりすると、ブラウザがサーバーにリクエストを送り、サーバーは新しいHTMLページ全体を返すため、画面が一瞬白くなる「ページ遷移」が発生していました。
Ajaxを使ったアプリケーションでは、
- ユーザーのアクションをきっかけに、JavaScriptがバックグラウンドでサーバーにリクエストを送信します。
- サーバーはHTML全体ではなく、データ(主にJSON形式)だけを返します。
- JavaScriptは受け取ったデータを処理し、DOM操作を使って現在のページの一部だけを書き換えます。
この仕組みにより、ページ遷移のないスムーズなユーザー体験(Single Page Application, SPAなどで活用)が可能になります。
9.2 fetch
APIの基本構文
JavaScriptでAjax通信を行うための標準的なAPIがfetch
APIです。fetch
はPromiseを返すため、async / await
と組み合わせることで非常に直感的に記述できます。
GETリクエスト(データの取得):
// async関数内でfetchを使う
async function getUserData() {
try {
// サーバーのAPIエンドポイントにGETリクエストを送信
const response = await fetch('https://api.example.com/users/1');
// レスポンスが正常でない場合はエラーを投げる
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// レスポンスボディをJSONとして解析
const userData = await response.json();
console.log(userData); // -> { id: 1, name: 'John Doe', ... }
} catch (error) {
console.error('データの取得に失敗しました:', error);
}
}
getUserData();
POSTリクエスト(データの送信):
データをサーバーに送信する場合は、fetchの第2引数にオプションオブジェクトを渡します。
async function createNewUser(newUser) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST', // HTTPメソッドを指定
headers: {
// 送信するデータがJSON形式であることをサーバーに伝える
'Content-Type': 'application/json'
},
// JavaScriptオブジェクトをJSON文字列に変換してbodyに設定
body: JSON.stringify(newUser)
});
if (!response.ok) {
throw new Error('ユーザーの作成に失敗しました');
}
const createdUser = await response.json();
console.log('作成されたユーザー:', createdUser);
} catch (error) {
console.error('エラー:', error);
}
}
const user = { name: 'Jane Doe', email: 'jane@example.com' };
createNewUser(user);
9.3 JSONデータの取得と加工
サーバーから返されるレスポンスボディは、そのままではただのテキストストリームです。これをJavaScriptのオブジェクトとして扱えるように変換するメソッドがresponse.json()
です。
response.json()
: レスポンスボディをJSONとして解析し、結果のJavaScriptオブジェクトを返すPromiseを返します。
【JavaのJackson的役割】
このresponse.json()の役割は、Javaのサーバーサイド開発でよく使われるJacksonやGsonといったライブラリが、JSON文字列をJavaオブジェクトにマッピング(デシリアライズ)するのと同じだと考えてください。
逆に、JSON.stringify()
は、JavaScriptオブジェクトをJSON形式の文字列に変換(シリアライズ)する関数で、これもJacksonなどがJavaオブジェクトをJSON文字列に変換する役割に対応します。
9.4 外部APIの利用例(天気・ニュースなど)
世の中には、天気情報、ニュース、地図、翻訳など、様々な機能を提供する公開API(Web API)が無数に存在します。fetch
APIを使えば、これらの外部サービスと簡単に連携できます。
例:気象情報APIから今日の天気予報を取得する
async function fetchWeather(cityCode) {
// APIのエンドポイントURL
const apiUrl = `https://weather.tsukumijima.net/api/forecast/city/${cityCode}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('天気情報の取得に失敗しました');
}
const weatherData = await response.json();
// 取得したデータを使ってDOMを操作
const titleElement = document.getElementById('weather-title');
const forecastElement = document.getElementById('weather-forecast');
titleElement.textContent = `${weatherData.title} の天気`;
// 最初の予報(今日)のテキストを取得
forecastElement.textContent = weatherData.forecasts[0].detail.weather;
} catch (error) {
console.error(error);
}
}
// 東京の市町村コード '130010' を指定して関数を実行
fetchWeather('130010');
このように、外部APIと連携することで、自分のウェブサイトに簡単に高度な機能を追加することができます。
以上がAjaxとAPI連携の基本です。fetch
とasync / await
を使いこなすことで、バックエンド(Java/Spring Bootなど)と連携し、動的でリッチなWebアプリケーションを構築する道が開かれます。
次の章では、ブラウザ側にデータを一時的に保存する「ローカルデータ管理」について見ていきましょう。
10. ローカルデータ管理
Webアプリケーションによっては、ユーザーの設定や入力途中のデータなどを、ユーザーのブラウザ側に一時的に保存しておきたい場合があります。サーバーに毎回問い合わせることなくデータを保持できるため、オフライン対応やアプリケーションの応答性向上に繋がります。
【JavaのSession属性やプロパティファイルに相当】
サーバーサイドJava開発におけるセッション管理(ユーザーがログインしている間だけデータを保持)や、アプリケーションの設定値を保持するプロパティファイルの考え方に似ています。ただし、JavaScriptのローカルデータ管理は、あくまで個々のユーザーのブラウザ内に閉じたものです。
10.1 LocalStorage
/ SessionStorage
LocalStorage
とSessionStorage
は、キーと値のペアで文字列データを保存するためのシンプルな仕組み(Web Storage API)です。両者の使い方は全く同じですが、データの寿命が異なります。
API | データの寿命 | 主な用途 |
LocalStorage | 永続的。ユーザーが手動で削除するか、コードで削除しない限り残り続ける。 | ユーザー設定(テーマカラーなど)、ログイン状態の記憶(「ログインしたままにする」) |
SessionStorage | タブ(またはウィンドウ)を閉じるまで。ページをリロードしてもデータは残るが、タブを閉じると消える。 | 入力フォームの一時保存、複数ページにまたがるウィザードの途中データ |
基本的な使い方:
// --- LocalStorage ---
// 1. データの保存 (キー, 値)
localStorage.setItem('username', 'Taro');
// 2. データの取得
const username = localStorage.getItem('username');
console.log(username); // -> Taro
// 3. データの削除
localStorage.removeItem('username');
// 4. すべてのデータを削除
// localStorage.clear();
// --- SessionStorageも全く同じメソッド ---
sessionStorage.setItem('inputData', '入力途中のテキスト...');
const data = sessionStorage.getItem('inputData');
注意点:
- 保存できるのは文字列のみです。オブジェクトや配列を保存したい場合は、
JSON.stringify()
で文字列に変換し、取り出す際にJSON.parse()
で元に戻す必要があります。 - 保存容量には上限があります(通常ブラウザごとに5MB〜10MB程度)。
- 機密性の高い情報(パスワードなど)を保存すべきではありません。
// オブジェクトをLocalStorageに保存する例
const userSettings = {
theme: 'dark',
notifications: true
};
// JSON文字列に変換して保存
localStorage.setItem('settings', JSON.stringify(userSettings));
// 取り出してJSONオブジェクトに戻す
const savedSettings = JSON.parse(localStorage.getItem('settings'));
console.log(savedSettings.theme); // -> dark
10.2 Cookieの操作
Cookieもブラウザにデータを保存する仕組みですが、Web Storageに比べて古く、いくつかの特徴があります。
- サーバーとの通信: Cookieは、リクエストのたびに自動的にサーバーに送信されます。
- 容量が小さい: 約4KBまでしか保存できません。
- 有効期限を設定可能: サーバー側またはクライアント側で有効期限を指定できます。
JavaScriptから直接Cookieを操作することもできますが、document.cookie
を直接読み書きするのは少し煩雑です。
// Cookieの書き込み (有効期限をUTCで指定)
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (7 * 24 * 60 * 60 * 1000)); // 7日後
document.cookie = `userId=123; expires=${expiryDate.toUTCString()}; path=/`;
現在では、クライアント側でのデータ保持はLocalStorage
やSessionStorage
を使うのが主流です。Cookieは、サーバー側でセッション管理を行うための識別子(セッションIDなど)を保持する、といったサーバーとの連携が必須の用途で主に使われます。
10.3 データ永続化の活用例
例1:入力フォームの内容をSessionStorageに一時保存する
ユーザーが長いフォームを入力中に誤ってページをリロードしてしまっても、入力内容が復元されるようにします。
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
// ページ読み込み時に入力内容を復元
nameInput.value = sessionStorage.getItem('form_name') || '';
emailInput.value = sessionStorage.getItem('form_email') || '';
// 入力があるたびに内容を保存
nameInput.addEventListener('input', (e) => {
sessionStorage.setItem('form_name', e.target.value);
});
emailInput.addEventListener('input', (e) => {
sessionStorage.setItem('form_email', e.target.value);
});
例2:ダークモード設定をLocalStorageに保存する
ユーザーが一度ダークモードに設定したら、次回訪問時もその設定が維持されるようにします。
const toggleButton = document.getElementById('theme-toggle');
// ページ読み込み時に設定を反映
const currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// ボタンクリックで設定を切り替え・保存
toggleButton.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
});
以上がブラウザでのローカルデータ管理の基本です。サーバーサイドの状態管理とは異なり、あくまでクライアント(ブラウザ)側に閉じたデータストアですが、これを活用することでアプリケーションの使い勝手を大きく向上させることができます。
次の章では、開発中に必ず直面するエラーへの対処法「エラー処理とデバッグ」について学びます。
11. エラー処理とデバッグ
どんなプログラミング言語でも、エラーの発生は避けられません。エラーを未然に防ぎ、発生したエラーに適切に対処し、そしてバグの原因を効率的に突き止めることは、堅牢なアプリケーションを開発する上で不可欠なスキルです。幸い、JavaScriptのエラー処理構文はJavaと非常によく似ており、ブラウザには強力なデバッグツールが組み込まれています。
11.1 try-catch
構文はJavaと全く同じです。
エラーが発生する可能性のあるコードをtryブロックで囲み、エラーが発生した場合の処理をcatchブロックに記述します。
try {
// エラーが発生する可能性のある処理
const data = JSON.parse('{ "name": "Taro", "age": 30, }'); // 不正なJSON形式(末尾のカンマ)
console.log(data);
} catch (error) {
// tryブロック内でエラーが発生した場合に、このブロックが実行される
console.error('パースエラーが発生しました!');
console.error('エラー名:', error.name); // 例: SyntaxError
console.error('メッセージ:', error.message); // 例: Unexpected token } in JSON at position ...
// ここでユーザーにエラーメッセージを表示するなどの処理を行う
} finally {
// finallyブロックもJava同様、エラーの有無にかかわらず必ず実行される
console.log('処理を終了します。');
}
catch
ブロックで受け取るerror
オブジェクトには、エラーの種類を示すname
プロパティと、詳細なmessage
プロパティが含まれています。
11.2 throw
による例外送出
こちらもJavaと全く同じ考え方です。
throw文を使うことで、意図的にエラー(例外)を発生させることができます。通常は、標準のErrorオブジェクトまたはその派生オブジェクトをインスタンス化してthrowします。
function calculateDivision(a, b) {
if (b === 0) {
// 0での除算は不正なので、エラーをスローする
throw new Error('0で割ることはできません。');
}
return a / b;
}
try {
const result = calculateDivision(10, 0);
console.log(result);
} catch (error) {
console.error(error.message); // -> 0で割ることはできません。
}
※Javaと異なり、JavaScriptでは技術的にはどんな値(文字列や数値など)でもthrow
できますが、スタックトレースなどの有用な情報が含まれるError
オブジェクトをthrow
するのがベストプラクティスです。
11.3 console
APIの活用
System.out.println()
を使ったデバッグに慣れているJavaエンジニアにとって、console
オブジェクトは最も身近なツールでしょう。console.log()
以外にも、状況に応じて使い分けることでデバッグ効率が向上します。
console.log()
: 最も一般的な出力。変数の中身や処理の通過点を確認するのに使う。console.error()
: エラーメッセージを出力する。通常、コンソール上で赤く表示され、スタックトレースも併せて表示されるため、エラー箇所を特定しやすい。console.warn()
: 警告メッセージを出力する。黄色で表示されることが多い。console.table()
: 非常の便利な機能です。 配列やオブジェクトを渡すと、見やすいテーブル形式で表示してくれます。
const users = [
{ id: 1, name: 'Sato', email: 'sato@example.com' },
{ id: 2, name: 'Suzuki', email: 'suzuki@example.com' },
{ id: 3, name: 'Takahashi', email: 'takahashi@example.com' }
];
// 配列やオブジェクトをテーブル形式で表示
console.table(users);
11.4 開発者ツールでのブレークポイントデバッグ
console.log
によるデバッグには限界があります。EclipseやIntelliJ IDEAのデバッガと同じように、ブラウザの開発者ツールを使えば、コードの実行を任意の場所で一時停止し、その時点での変数の状態を詳しく調べることができます。
デバッグの基本的な流れ:
- ブラウザで開発者ツール(
F12
)を開き、「Sources」(ソース)タブに移動します。 - 左側のファイルツリーから、デバッグしたいJavaScriptファイルを選択します。
- コードが表示されたら、実行を一時停止したい行の行番号をクリックします。赤い(または青い)印が付き、ブレークポイントが設定されます。
- ページをリロードしたり、ブレークポイントを設定した処理が実行されるような操作(ボタンクリックなど)を行います。
コードの実行がブレークポイントで一時停止すると、以下の操作が可能になります。
- 変数の確認:
- コード上の変数にマウスカーソルを合わせると、その時点での値がポップアップ表示されます。
- 右側の「Scope」(スコープ)パネルで、現在のスコープで参照可能なすべての変数のリストを確認できます。
- ステップ実行(JavaのIDEと同じ):
- Resume (▶︎): 次のブレークポイントまで実行を再開します。
- Step Over (↷): 現在の行を実行し、次の行へ進みます(関数の中には入らない)。
- Step Into (⇣): 現在の行が関数呼び出しの場合、その関数の中に入ります。
- Step Out (⇡): 現在の関数を最後まで実行し、呼び出し元の次の行に戻ります。
ブレークポイントデバッグは、複雑なバグの原因を特定するための最も強力な手法です。
以上がエラー処理とデバッグの基本です。try-catch-throw
の構文はJavaの知識をそのまま活かせますし、console
APIと開発者ツールのデバッガを使いこなすことが、効率的なフロントエンド開発の鍵となります。
いよいよ次の最終章では、これまで学んだJavaScriptの知識を総動員し、「Spring Bootとの連携」について解説します。
12. Spring Bootとの連携
おめでとうございます!いよいよ最終章です。これまで学んできたJavaScriptの知識を、Javaエンジニアにとって最も馴染み深いバックエンドフレームワークであるSpring Bootと結びつけていきます。フロントエンドとサーバーサイドがどのように連携して一つのWebアプリケーションとして機能するのか、その全体像を掴んでいきましょう。
12.1 JavaScriptとSpring Bootの役割分担
モダンなWebアプリケーション開発では、フロントエンドとサーバーサイドの役割を明確に分離するのが一般的です。
JavaScript (フロントエンド)
- 担当領域: ユーザーのブラウザ内
- 主な責務:
- UI/UX: ユーザーが見て、操作する画面の構築と体験の向上。
- DOM操作: ユーザーのアクションに応じて画面を動的に書き換える。
- イベント処理: クリックや入力などのユーザー操作を捉える。
- APIリクエスト: Ajax(
fetch
)を使い、サーバー(Spring Boot)に必要なデータを要求したり、データを送信したりする。
Spring Boot (サーバーサイド)
- 担当領域: サーバー上
- 主な責務:
- ビジネスロジック: アプリケーションの核となる処理。
- データ永続化: データベースとのやり取り(JPA/Hibernateなど)。
- API提供: フロントエンドからのリクエストに応じ、JSON形式でデータを提供するREST APIを公開する。
- セキュリティ: 認証・認可、データ検証。
レストランに例えるなら、JavaScriptがフロア担当のウェイター(顧客と対話し、注文を取り、料理を運ぶ)、Spring Bootが厨房のシェフ(注文を受け、調理し、料理を完成させる)です。
12.2 フォーム送信
JavaScriptは、従来の単純なフォーム送信をよりリッチなものにします。
HTMLフォームを用意:
<form id="user-form">
<input type="text" id="username" required>
<button type="submit">登録</button>
</form>
JavaScriptでsubmitイベントを捕捉:
event.preventDefault()で、フォーム送信による従来のページ遷移をキャンセルします。
const form = document.getElementById('user-form');
form.addEventListener('submit', event => {
event.preventDefault(); // デフォルトの送信動作を停止
// この後、入力値のチェックやfetchによるデータ送信を行う
console.log('フォーム送信がJavaScriptによってインターセプトされました。');
});
ページをリロードすることなく、フォームのデータをサーバーに送信し、結果を画面に反映させることができます。
12.3 fetch
APIでSpring Boot REST APIを呼び出す
これが連携の核となる部分です。
Spring Boot側: @RestControllerでAPIを定義
@RestControllerを付けたクラスでは、メソッドの戻り値が自動的にJSONに変換されてレスポンスボディとして返されます。
// User.java (DTO or Entity)
public class User {
private Long id;
private String name;
// Getters, Setters, Constructor...
}
// UserController.java
@RestController
@RequestMapping("/api/users")
public class UserApiController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// 本来はDBから取得する
return new User(id, "Taro from Spring Boot");
}
}
JavaScript側: fetch
でAPIを呼び出す
async function displayUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('ユーザーが見つかりません');
const user = await response.json(); // JSONをJSオブジェクトに変換
// 取得したデータでDOMを更新
document.getElementById('user-name').textContent = user.name;
} catch (error) {
console.error(error);
}
}
displayUser(1);
12.4 JSON形式でのデータ送受信
データの送受信には、Spring BootのJacksonライブラリ(デフォルトで内包)とJavaScriptのJSON
オブジェクトが大きな役割を果たします。
- Spring Boot → JavaScript (シリアライズ)
@RestController
や@ResponseBody
があると、SpringはJavaオブジェクトをJacksonを使ってJSON文字列に変換します。
- JavaScript → Spring Boot (デシリアライズ)
- JavaScript側で
JSON.stringify()
を使ってオブジェクトをJSON文字列に変換し、fetch
のbody
に詰めて送信します。 - Spring側は
@RequestBody
アノテーションでそれを受け取り、JacksonがJSON文字列をJavaオブジェクトに変換します。
- JavaScript側で
例:ユーザー作成 (POST)
Spring Boot Controller:
@PostMapping
public User createUser(@RequestBody User newUser) {
// DBに保存する処理...
System.out.println("作成されたユーザー: " + newUser.getName());
return newUser; // 保存した結果を返す
}
JavaScript fetch
:
async function registerUser(name) {
const newUser = { name: name };
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser) // JSオブジェクトをJSON文字列に
});
const createdUser = await response.json();
console.log(createdUser);
}
registerUser('Hanako JS');
12.5 バリデーションの組み合わせ
堅牢なアプリケーションでは、クライアントサイドとサーバーサイドの両方でバリデーション(入力値検証)を行うのが鉄則です。
- JavaScript (クライアントサイド):
- 目的: ユーザーへの即時フィードバックによるUX向上。不要なリクエストを減らす。
- 方法:
required
属性、正規表現によるフォーマットチェックなど。
クライアントサイドバリデーションのコード例
const form = document.getElementById('user-form');
const usernameInput = document.getElementById('username');
form.addEventListener('submit', (event) => {
event.preventDefault(); // まずは送信を止める
// 簡単な入力チェック
if (usernameInput.value.length < 3) {
alert('ユーザー名は3文字以上で入力してください。');
return; // 処理を中断
}
// ここに、バリデーション通過後のfetch処理などを記述する
console.log('クライアントサイドバリデーションを通過しました。');
});
- Spring Boot (サーバーサイド):
- 目的: データの整合性とセキュリティの確保。こちらが本物の検証。
- 方法: Bean Validation (
@Valid
,@NotNull
,@Size
など) を使う。
クライアントの検証は容易にバイパス可能なので、サーバーサイドの検証は必須です。
12.6 簡易TODOアプリの連携例
これまでの知識を組み合わせると、以下のようなTODOアプリが作れます。
- 画面表示時:
fetch
でGET /api/todos
を呼び出し、TODOリストを取得して表示。 - TODO追加時: フォームの
submit
イベントを捕捉し、fetch
でPOST /api/todos
に新しいTODOのテキストを送信。成功したらリストに新しい項目を追加。 - TODO削除時: 削除ボタンの
click
イベントを捕捉し、fetch
でDELETE /api/todos/{id}
を呼び出す。成功したらリストからその項目を削除。
はい、承知いたしました。 12.6項の「簡易TODOアプリの連携例」について、これまでの知識を総動員した具体的なサンプルコードを提示します。
以下に、Spring Boot(バックエンド)とJavaScript(フロントエンド)で構成される、基本的なTODOアプリケーションの完全なサンプルコードを示します。
1. Spring Boot側:REST APIの実装
まず、TODOデータのCRUD(作成、読み取り、削除)操作を行うためのシンプルなREST APIを@RestController
として作成します。 ここでは簡単のため、データベースは使わずメモリ上にデータを保持します。
ここでは簡単のため、データベースは使わずメモリ上にデータを保持します。
// Lombokを使うとより簡潔になります
public class Todo {
private Long id;
private String text;
private boolean completed;
// Constructors, Getters, Setters
public Todo() {}
public Todo(Long id, String text) {
this.id = id;
this.text = text;
this.completed = false;
}
// Getters and setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
}
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/todos")
@CrossOrigin // 開発のため、localhostからのアクセスを許可
public class TodoApiController {
// データベースの代わりとなるメモリ上のデータストア
private final ConcurrentHashMap<Long, Todo> todos = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong();
public TodoApiController() {
// 初期データ
long initialId = counter.incrementAndGet();
todos.put(initialId, new Todo(initialId, "Spring Bootの学習"));
initialId = counter.incrementAndGet();
todos.put(initialId, new Todo(initialId, "JavaScriptの復習"));
}
// GET /api/todos: 全てのTODOを取得
@GetMapping
public List<Todo> getTodos() {
return new ArrayList<>(todos.values());
}
// POST /api/todos: 新しいTODOを作成
@PostMapping
public Todo createTodo(@RequestBody Todo newTodo) {
long newId = counter.incrementAndGet();
newTodo.setId(newId);
todos.put(newId, newTodo);
return newTodo;
}
// DELETE /api/todos/{id}: 指定されたIDのTODOを削除
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTodo(@PathVariable Long id) {
todos.remove(id);
return ResponseEntity.ok().build();
}
}
2. フロントエンド側:HTMLとJavaScript
次に、ユーザーが操作する画面(HTML)と、Spring BootのAPIと通信して画面を動的に更新するJavaScriptを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>簡易TODOアプリ</title>
<style>
body { font-family: sans-serif; }
ul { list-style: none; padding: 0; }
li { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #ccc; }
.delete-btn { cursor: pointer; color: red; }
</style>
</head>
<body>
<h1>簡易TODOアプリ</h1>
<form id="add-todo-form">
<input type="text" id="todo-text-input" placeholder="新しいTODOを入力" required>
<button type="submit">追加</button>
</form>
<ul id="todo-list">
</ul>
<script src="app.js"></script>
</body>
</html>
// Spring BootサーバーのベースURL
const API_BASE_URL = 'http://localhost:8080/api/todos';
// DOM要素の取得
const todoList = document.getElementById('todo-list');
const addTodoForm = document.getElementById('add-todo-form');
const textInput = document.getElementById('todo-text-input');
/**
* サーバーからTODOリストを取得して画面に描画する
*/
async function fetchAndRenderTodos() {
try {
const response = await fetch(API_BASE_URL);
const todos = await response.json();
// 既存のリストをクリア
todoList.innerHTML = '';
// 取得したTODOを描画
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
const deleteButton = document.createElement('span');
deleteButton.textContent = '削除';
deleteButton.className = 'delete-btn';
deleteButton.dataset.id = todo.id; // 削除ボタンにIDを持たせる
li.appendChild(deleteButton);
todoList.appendChild(li);
});
} catch (error) {
console.error('TODOリストの取得に失敗しました:', error);
}
}
/**
* 新しいTODOをサーバーに追加する
*/
addTodoForm.addEventListener('submit', async (event) => {
event.preventDefault();
const text = textInput.value.trim();
if (!text) return;
const newTodo = { text: text, completed: false };
try {
await fetch(API_BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo)
});
// 入力欄をクリアしてリストを再読み込み
textInput.value = '';
await fetchAndRenderTodos();
} catch (error) {
console.error('TODOの追加に失敗しました:', error);
}
});
/**
* TODOを削除する(イベント委任)
*/
todoList.addEventListener('click', async (event) => {
// クリックされたのが削除ボタンの場合のみ処理
if (event.target.classList.contains('delete-btn')) {
const id = event.target.dataset.id;
try {
await fetch(`${API_BASE_URL}/${id}`, {
method: 'DELETE'
});
// リストを再読み込み
await fetchAndRenderTodos();
} catch (error) {
console.error('TODOの削除に失敗しました:', error);
}
}
});
// -- 初期化処理 --
// ページ読み込み時にTODOリストを初回取得
fetchAndRenderTodos();
12.7 セキュリティ考慮
フロントエンドとサーバーを分離すると、特有のセキュリティ問題に対応する必要があります。
- CORS (Cross-Origin Resource Sharing):
- 問題: ブラウザの安全機構により、異なるオリジン(ドメイン、ポート)間の
fetch
リクエストはデフォルトでブロックされます。(例:localhost:3000
のJSからlocalhost:8080
のSpring Bootへ) - 対策: Spring Boot側で
@CrossOrigin
アノテーションをコントローラーやメソッドに付けることで、特定オリジンからのアクセスを許可します。
- 問題: ブラウザの安全機構により、異なるオリジン(ドメイン、ポート)間の
- CSRF (Cross-Site Request Forgery):
- 問題: 悪意のあるサイトが、ログイン済みユーザーのブラウザを使って意図しないリクエスト(パスワード変更など)を強制的に送信する攻撃。
- 対策: Spring SecurityはデフォルトでCSRF対策が有効です。APIサーバーとして機能させる場合、ステートレスな認証方式(JWTなど)を採用し、Spring SecurityのCSRF設定を適切に構成する必要があります。
終わりに
このテキストは、Javaエンジニアである皆さんがJavaScriptの世界へ踏み出すための第一歩となることを目指して作成しました。基本文法からDOM操作、非同期処理、そして使い慣れたSpring Bootとの連携まで、Webフロントエンド開発の基礎を一通り学びました。
ここから先には、React, Vue, Angularといった強力なフレームワークの世界が広がっています。しかし、どのフレームワークを学ぶにしても、このテキストで習得したJavaScriptの基礎知識、特にDOM、イベント、非同期処理の理解が揺るぎない土台となるはずです。
サーバーサイドの知識とフロントエンドのスキルを両方持つことで、皆さんのエンジニアとしての価値は飛躍的に高まります。このテキストが、その素晴らしい旅の一助となれば幸いです。Happy Coding!
セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク
投稿者プロフィール
- 代表取締役
-
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。
最新の投稿
山崎講師2025年8月16日PHPで作ったAPIを外部のドメインからアクセス可能にするには?
山崎講師2025年8月16日Java経験者のためのJavaScirpt入門
山崎講師2025年8月12日Java経験者のためのPHP入門
山崎講師2025年8月12日Java経験者がPHPでハマる10選の1 「型のゆるさ(動的型付け)に戸惑う」