🧷
JavaScriptを安全に書くTips
はじめに
JavaScriptは柔軟で自由度の高い言語です。しかし、その自由さゆえに「同じことを実現する方法が複数存在する」という特徴があります。
この柔軟性は時にバグの温床となり、コードの可読性や保守性を損なう原因にもなります。
この記事では、より安全で堅牢なJavaScriptを書くためのTipsを紹介します。
1. letを避けてconstを使う
⚠️ 注意が必要な書き方
// letを使う(再代入しないのに)
let userName = 'Taro';
let maxRetry = 3;
❌ 起こりうる問題
let total = 100;
// ... 100行後 ...
total = 'エラー'; // 意図せず型が変わってしまう
// ... さらに後 ...
console.log(total * 2); // NaN になる
✅ こう書くとより安全
// constを使う
const userName = 'Taro';
const maxRetry = 3;
const config = { timeout: 5000 };
💡 メリット
- 意図の明確化 — 「この値は変更されない」という意図がコードから読み取れる
- 再代入バグの防止 — 誤って値を上書きしようとするとエラーになる
-
コードレビューの効率化 —
letを見たら「どこかで再代入される」と予測できる
2. 即時実行関数(IIFE)でスコープを限定
こちらは const を使いたいが、条件によって初期値を変えたい場合の発展テクニックです。
IIFE: Immediately Invoked Function Expressionについて
⚠️ 注意が必要な書き方
// letで宣言して後から代入
let message;
if (score >= 80) {
message = '優秀';
} else if (score >= 60) {
message = '合格';
} else {
message = '不合格';
}
❌ 起こりうる問題
let message;
if (score >= 80) {
message = '優秀';
}
// else ifを書き忘れると...
console.log(message); // undefined になる可能性
✅ こう書くとより安全
// IIFEでスコープを限定しconstを使う
const message = (() => {
if (score >= 80) return '優秀';
if (score >= 60) return '合格';
return '不合格';
})();
💡 メリット
-
constが使える — 条件分岐があっても再代入不要 - スコープの汚染防止 — IIFE内の変数は外部に漏れない
3. switch文を避けてオブジェクトを使う
⚠️ 注意が必要な書き方
// 長いswitch文
switch (statusCode) {
case 200:
message = '成功';
break;
case 400:
message = 'リクエストエラー';
break;
case 404:
message = '見つかりません';
break;
case 500:
message = 'サーバーエラー';
break;
default:
message = '不明なエラー';
}
❌ 起こりうる問題
switch (type) {
case 'A':
result = 1;
// breakを忘れた!
case 'B':
result = 2;
break;
}
// type === 'A' のとき result は 2 になってしまう
✅ こう書くとより安全
// オブジェクトを使う
const statusMessages = {
200: '成功',
400: 'リクエストエラー',
404: '見つかりません',
500: 'サーバーエラー',
};
const message = statusMessages[statusCode] ?? '不明なエラー';
// 関数のマッピングにも使える
const handlers = {
click: () => console.log('クリックされました'),
hover: () => console.log('ホバーされました'),
blur: () => console.log('フォーカスが外れました'),
};
handlers[eventType]?.();
💡 メリット
- 簡潔で読みやすい — 対応関係が一目でわかる
- break忘れの心配がない — fall-throughバグを防止
-
constが使える — switch文と違い宣言と同時に初期化できる
4. 条件分岐を簡潔に
⚠️ 冗長な書き方
// 冗長なif文
let status;
if (isActive) {
status = 'アクティブ';
} else {
status = '非アクティブ';
}
// 冗長なnullチェック
let displayName;
if (userName !== null && userName !== undefined) {
displayName = userName;
} else {
displayName = 'ゲスト';
}
✅ こう書くとより安全
// 三項演算子
const status = isActive ? 'アクティブ' : '非アクティブ';
// null合体演算子(??)
const displayName = userName ?? 'ゲスト';
// 論理OR演算子(||)- falsyな値全般
const count = inputCount || 1;
?? と || の違い
const value1 = 0 || 10; // 10(0はFalsyなので)
const value2 = 0 ?? 10; // 0(0はnullでもundefinedでもない)
const value3 = '' || 'default'; // 'default'
const value4 = '' ?? 'default'; // ''
📝 補足:結果がbooleanの場合はさらに簡潔に
⚠️ 冗長な書き方
const isActive = status === "active" ? true : false;
const hasItems = items.length > 0 ? true : false;
const isValid = value !== null && value !== undefined ? true : false;
✅ こう書くとより簡潔
// 比較演算子の結果はすでにboolean
const isActive = status === "active";
const hasItems = items.length > 0;
const isValid = value !== null && value !== undefined;
// 明示的にbooleanに変換したい場合は !! を使う
const hasText = !!inputText; // falsyな値をfalseに、truthyな値をtrueに
💡 メリット
-
constが使える — 1行で宣言と初期化ができる - コードが簡潔 — 意図が明確になり、レビュー時の認知不可も下がる
-
??は意図しない変換を防ぐ —0や''を有効な値として扱える
5. 三項演算子のネストは避ける
⚠️ 注意が必要な書き方
// ネストした三項演算子
const grade = score >= 90 ? 'S'
: score >= 80 ? 'A'
: score >= 70 ? 'B'
: score >= 60 ? 'C'
: 'D';
❌ 起こりうる問題
- 読みにくい、理解しにくい
- 条件の優先順位を誤解しやすい
- バグが入り込みやすく、発見しにくい
✅ こう書くとより安全
// 関数に切り出す
const getGrade = (score) => {
if (score >= 90) return 'S';
if (score >= 80) return 'A';
if (score >= 70) return 'B';
if (score >= 60) return 'C';
return 'D';
};
const grade = getGrade(score);
// IIFEを使う
const grade = (() => {
if (score >= 90) return 'S';
if (score >= 80) return 'A';
if (score >= 70) return 'B';
return 'C';
})();
💡 メリット
- 可読性の向上 — 条件の流れが追いやすい
- デバッグしやすい — 各条件にブレークポイントを設定できる
- 保守性の向上 — 条件の追加・変更が容易
6. オプショナルチェイニングで安全にプロパティアクセス
⚠️ 注意が必要な書き方
// 冗長なnullチェック
const city = user && user.address && user.address.city;
// 長い条件式
let city;
if (user !== null && user !== undefined) {
if (user.address !== null && user.address !== undefined) {
city = user.address.city;
}
}
❌ 起こりうる問題
// userがundefinedだと...
const city = user.address.city;
// TypeError: Cannot read property 'address' of undefined
// APIレスポンスが想定外の形式だと...
const name = response.data.users[0].name;
// どこかがundefinedならクラッシュする
✅ こう書くとより安全
// オプショナルチェイニング
const city = user?.address?.city;
const firstItem = items?.[0];
const result = obj?.method?.();
💡 メリット
- 簡潔で安全 — ネストしたプロパティに安全にアクセス
-
エラー防止 —
Cannot read property of undefinedを防ぐ - コードの可読性向上 — 意図が明確
7. 早期リターンでネストを浅く
⚠️ 注意が必要な書き方
// 深いネスト
const processUser = (user) => {
if (user) {
if (user.isActive) {
if (user.age >= 18) {
return {
name: user.name,
email: user.email,
};
}
}
}
return null;
};
❌ 起こりうる問題
- コードの見通しが悪くなる
- どの条件でどの処理が実行されるか把握しにくい
- バグの原因となる「閉じ括弧の対応ミス」が起きやすい
✅ こう書くとより安全
// 早期リターン(ガード節)
const processUser = (user) => {
if (!user) return null;
if (!user.isActive) return null;
if (user.age < 18) return null;
// メインロジック(ネストなし)
return {
name: user.name,
email: user.email,
};
};
💡 メリット
- 可読性の向上 — メインロジックが目立つ
- 認知負荷の軽減 — 条件を上から順に確認できる
- 保守性の向上 — 条件の追加・削除が容易
8. Object.freezeで定数オブジェクトを保護
⚠️ 注意が必要な書き方
// constだけでは中身は変更可能
const CONFIG = {
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
};
// これは可能(constは再代入を防ぐだけ)
CONFIG.TIMEOUT = 99999; // 変更できてしまう
❌ 起こりうる問題
const DEFAULT_OPTIONS = { retries: 3 };
const fetchData = (options = DEFAULT_OPTIONS) => {
options.retries--; // DEFAULT_OPTIONSも変更される!
// ...
};
fetchData();
fetchData();
fetchData();
console.log(DEFAULT_OPTIONS.retries); // 0 になっている
✅ こう書くとより安全
// Object.freezeで不変にする
const CONFIG = Object.freeze({
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
MAX_RETRY: 3,
});
// 変更しようとしても無視される(strictモードではエラー)
CONFIG.TIMEOUT = 10000; // 変更されない
// 深いフリーズが必要な場合
const deepFreeze = (obj) => {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return Object.freeze(obj);
};
const NESTED_CONFIG = deepFreeze({
server: { host: 'localhost', port: 3000 },
});
💡 メリット
- 不変性の保証 — 設定値の意図しない変更を防止
- バグの早期発見 — strictモードでは変更時にエラー
- 予測可能性 — オブジェクトの状態が常に一定
9. Array.fromやスプレッド構文で安全にコピー
⚠️ 注意が必要な書き方
// 配列のシャローコピー(実体は同じ)
const original = [1, 2, 3];
const notCopied = original;
notCopied.push(4);
console.log(original); // [1, 2, 3, 4] 元の配列も変わる!
// オブジェクトのシャローコピー(実体は同じ)
const originalObj = { a: 1, b: 2 };
const notCopiedObj = originalObj;
notCopiedObj.b = 3;
console.log(originalObj); // { a: 1, b: 3 } 元のオブジェクトも変わる!
❌ 起こりうる問題
const defaultItems = ['item1', 'item2'];
const createList = () => {
const items = defaultItems; // 参照をコピー
items.push('newItem'); // defaultItemsも変わる!
return items;
};
createList();
createList();
console.log(defaultItems); // ['item1', 'item2', 'newItem', 'newItem']
✅ こう書くとより安全
// スプレッド構文でコピー
const original = [1, 2, 3];
const copied = [...original];
// Array.fromでコピー
const copied2 = Array.from(original);
// スプレッド構文でコピー
const originalObj = { a: 1, b: 2 };
const copiedObj = { ...originalObj };
📝 補足:階層が深くなる場合のスプレッド構文は注意が必要
const original = [1, 2, [3, 4]];
const copied = [...original];
copied[2].push(5);
console.log(original) // [1, 2, [3, 4, 5]] 元の配列の3階層目も変わる!
// スプレッド構文でコピーが保証されるのは1階層目まで
const originalObj = {
a: 1,
b: { c: 2, d: 3 },
};
const copiedObj = { ...originalObj };
copiedObj.b.c = 99;
console.log(original) // { a: 1, b: { c: 99, d: 3 } } 元のオブジェクトのbも変わる!
全階層のコピーが必要な場合
// JSON経由(関数やundefinedは失われる点に注意)
const deepCopied = JSON.parse(JSON.stringify(original));
// Node v17~ 使える
const deepCopied2 = structuredClone(original);
💡 メリット
- 副作用の防止 — 元のデータを変更しない
- 予測可能な動作 — コピーへの変更が元に影響しない
- デバッグが容易 — データの流れを追跡しやすい
10. 配列操作は非破壊メソッドを優先
⚠️ 注意が必要な書き方
// 破壊的な操作
const numbers = [3, 1, 4, 1, 5];
numbers.sort((a, b) => a - b); // 元の配列が変わる
numbers.reverse(); // 元の配列が変わる
numbers.splice(2, 1); // 元の配列が変わる
❌ 起こりうる問題
const renderList = (items) => {
items.sort(); // 引数を破壊!
return items.map(item => `${item}`).join('');
};
const myItems = ['banana', 'apple', 'cherry'];
renderList(myItems);
console.log(myItems); // ['apple', 'banana', 'cherry'] 順番が変わった!
✅ こう書くとより安全
// 非破壊的な操作
const numbers = [3, 1, 4, 1, 5];
// ソート
const sorted = [...numbers].sort((a, b) => a - b);
// リバース
const reversed = [...numbers].reverse();
// 要素の削除
const removed = numbers.filter((_, i) => i !== 2);
// 要素の更新
const updated = numbers.map((n, i) => i === 0 ? 10 : n);
💡 メリット
- 予測可能性 — 元のデータが変わらない安心感
- デバッグの容易さ — 各ステップの状態を確認できる
- 並行処理での安全性 — 複数箇所で同じ配列を参照しても安全
まとめ
これらのTipsに共通するのは「予測可能で副作用の少ないコード」を書くということです。
| ポイント | 説明 |
|---|---|
| 値の不変性 | 値が変わらないことを保証する |
| 変更の防止 | 意図しない変更を防ぐ |
| 意図の明確化 | コードの意図を明確にする |
JavaScriptの自由さを活かしつつ、これらのプラクティスを意識することで、バグが少なく保守しやすいコードを書くことができます。
Discussion