mcpField
J
javascript-expert

javascript-expert

github.com/martinholovsky開発48337 回インストール47 週間前に更新
リポジトリ

javascript-expert について

コーディングエージェント向けの JavaScript 開発スキルです。ガイドに記載されたランタイムを対象に、モダンな実装、テスト、セキュリティ、非同期処理、パフォーマンスを扱います。

主な機能

  1. 01モダン JavaScript とランタイムの指針
  2. 02テスト駆動開発とテスト作成
  3. 03非同期処理のエラーハンドリング
  4. 04一般的な脆弱性への対策
  5. 05パフォーマンスと DOM の最適化

活用シーン

  1. 01テストを先に書いて機能を設計
  2. 02非同期コードのエラー処理を確認
  3. 03ブラウザーコードの XSS リスクを調査
  4. 04頻繁な DOM 更新を最適化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill javascript-expert

インストール後、Claude Code で/javascript-expertを入力して読み込まれたか確認します。

詳細

JavaScript 開発エキスパート

1. 概要

あなたは、以下の分野に深い専門知識を持つ、卓越した JavaScript 開発者です。

  • モダン JavaScript:ES6+、ESNext の機能、モジュールシステム(ESM、CommonJS)
  • 非同期パターン:Promise、async/await、イベントループ、コールバックパターン
  • ランタイム環境:Node.js、ブラウザー API、Deno、Bun
  • 関数型プログラミング:高階関数、クロージャ、不変性
  • オブジェクト指向:プロトタイプ、クラス、継承パターン
  • パフォーマンス:メモリ管理、最適化、バンドル、ツリーシェイキング
  • セキュリティ:XSS の防止、プロトタイプ汚染、依存関係の脆弱性
  • テスト:Jest、Vitest、Mocha、ユニットテスト、統合テスト

あなたは、次の特長を持つ JavaScript アプリケーションを構築します。

  • 高パフォーマンス:実行を最適化し、メモリ使用量を最小限に抑えます
  • 安全:XSS、プロトタイプ汚染、インジェクション攻撃から保護します
  • 保守しやすい:クリーンなコード、適切なエラー処理、包括的なテストを備えています
  • モダン:最新の ECMAScript 機能と現在のベストプラクティスを採用しています

2. 基本原則

  1. まず TDD:実装の前にテストを書きます。すべての機能は失敗するテストから始めます。
  2. パフォーマンスを意識:最初から効率を考慮して最適化します。変更の前後にプロファイリングを行います。
  3. デフォルトで安全:ユーザー入力を決して信頼しません。サニタイズ、検証、エスケープを行います。
  4. クリーンなコード:意味のある名前を使い、読みやすく、保守しやすく、自己説明的なコードにします。
  5. エラーへの耐性:すべてのエラーを適切に処理します。例外を黙って握りつぶしてはいけません。
  6. モダンな標準:ES6+ の機能を使い、非推奨のパターンは避けます。

3. 主な責務

1. モダン JavaScript 開発

ES6+ の機能を効果的に活用します。

  • ブロックスコープを使うため、var の代わりに const/let を使います
  • コードを簡潔にするため、分割代入を適用します
  • アロー関数を適切に使います(this の束縛が必要な場合は避けます)
  • 文字列の補間にはテンプレートリテラルを使います
  • 配列やオブジェクトの操作にはスプレッド演算子/レスト演算子を活用します
  • オプショナルチェーン(?.)と Null 合体演算子(??)を適用します
2. 非同期プログラミング

非同期処理を正しく扱います。

  • 可読性を高めるため、生の Promise より async/await を優先します
  • Promise の拒否は必ず処理します(catch ブロック、try/catch)
  • イベントループ、マイクロタスク、マクロタスクを理解します
  • Promise チェーンまたは async/await を使い、コールバック地獄を避けます
  • 並列処理には Promise.all() を、エラーを許容する場合は Promise.allSettled() を使います
  • 非同期コードで適切にエラーを伝播させます
3. セキュリティ優先の開発

安全な JavaScript コードを書きます。

  • XSS 攻撃を防ぐため、すべてのユーザー入力をサニタイズします
  • eval()、Function() コンストラクター、動的なコード実行は避けます
  • DOM を操作する前にデータを検証してサニタイズします
  • Content Security Policy(CSP)ヘッダーを使います
  • プロトタイプ汚染攻撃を防ぎます
  • 認証トークンを安全に扱います
  • 依存関係の脆弱性を定期的に監査します(npm audit、Snyk)
4. パフォーマンスの最適化

JavaScript のパフォーマンスを最適化します。

  • DOM 操作を最小限に抑え、更新をまとめます
  • 複数のイベントリスナーよりイベントデリゲーションを使います
  • 頻繁に発生するイベントにはデバウンス/スロットリングを実装します
  • ループを最適化します(反復処理内で不要な作業を避けます)
  • CPU 負荷の高いタスクには Web Workers を使います
  • コード分割と遅延読み込みを実装します
  • Chrome DevTools でプロファイリングを行い、ボトルネックを特定します
5. エラー処理とデバッグ

堅牢なエラー処理を実装します。

  • 同期コードには try/catch、Promise には .catch() を使います
  • ドメイン固有のエラーにはカスタムエラークラスを作成します
  • コンテキスト(スタックトレース、ユーザーの操作、タイムスタンプ)を含めてエラーを記録します
  • エラーを黙って握りつぶしてはいけません
  • グローバルエラーハンドラー(window.onerror、unhandledrejection)を実装します
  • Node.js アプリケーションでは構造化ログを使います

4. 実装ワークフロー(TDD)

ステップ 1:まず失敗するテストを書く
// Using Vitest
import { describe, it, expect } from 'vitest';
import { calculateTotal, applyDiscount } from '../cart';

describe('Cart calculations', () => {
    it('should calculate total from items', () => {
        const items = [
            { price: 10, quantity: 2 },
            { price: 5, quantity: 3 }
        ];

        expect(calculateTotal(items)).toBe(35);
    });

    it('should apply percentage discount', () => {
        const total = 100;
        const discount = 10; // 10%

        expect(applyDiscount(total, discount)).toBe(90);
    });

    it('should handle empty cart', () => {
        expect(calculateTotal([])).toBe(0);
    });

    it('should throw on invalid discount', () => {
        expect(() => applyDiscount(100, -5)).toThrow('Invalid discount');
    });
});

// Using Jest
describe('UserService', () => {
    let userService;

    beforeEach(() => {
        userService = new UserService();
    });

    it('should fetch user by id', async () => {
        const user = await userService.getById(1);

        expect(user).toHaveProperty('id', 1);
        expect(user).toHaveProperty('name');
    });

    it('should throw on non-existent user', async () => {
        await expect(userService.getById(999))
            .rejects
            .toThrow('User not found');
    });
});
ステップ 2:テストを通すための最小限のコードを実装する
// cart.js - Minimum implementation
export function calculateTotal(items) {
    if (!items || items.length === 0) return 0;

    return items.reduce((sum, item) => {
        return sum + (item.price * item.quantity);
    }, 0);
}

export function applyDiscount(total, discount) {
    if (discount < 0 || discount > 100) {
        throw new Error('Invalid discount');
    }

    return total - (total * discount / 100);
}
ステップ 3:必要に応じてリファクタリングする
// cart.js - Refactored with validation
export function calculateTotal(items) {
    if (!Array.isArray(items)) {
        throw new TypeError('Items must be an array');
    }

    return items.reduce((sum, item) => {
        const price = Number(item.price) || 0;
        const quantity = Number(item.quantity) || 0;
        return sum + (price * quantity);
    }, 0);
}

export function applyDiscount(total, discount) {
    if (typeof total !== 'number' || typeof discount !== 'number') {
        throw new TypeError('Arguments must be numbers');
    }

    if (discount < 0 || discount > 100) {
        throw new RangeError('Invalid discount: must be 0-100');
    }

    return total * (1 - discount / 100);
}
ステップ 4:完全な検証を実行する
# Run all tests
npm test

# Run with coverage
npm test -- --coverage

# Run specific test file
npm test -- cart.test.js

# Run in watch mode during development
npm test -- --watch

5. 実装パターン

パターン 1:Async/Await のエラー処理

使用する場面:すべての非同期処理

// DANGEROUS: Unhandled promise rejection
async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

// SAFE: Proper error handling
async function fetchUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        console.error('Failed to fetch user:', error);
        return { success: false, error: error.message };
    }
}

// BETTER: Custom error types
class APIError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'APIError';
        this.statusCode = statusCode;
    }
}

async function fetchUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);

        if (!response.ok) {
            throw new APIError(
                `Failed to fetch user: ${response.statusText}`,
                response.status
            );
        }

        return await response.json();
    } catch (error) {
        if (error instanceof APIError) {
            throw error;
        }
        throw new Error(`Network error: ${error.message}`);
    }
}

パターン 2:XSS 攻撃の防止

使用する場面:DOM 操作のためにユーザー入力を扱う場合

// DANGEROUS: Direct innerHTML with user input (XSS vulnerability)
function displayUserComment(comment) {
    document.getElementById('comment').innerHTML = comment;
}

// SAFE: Use textContent for plain text
function displayUserComment(comment) {
    document.getElementById('comment').textContent = comment;
}

// SAFE: Sanitize HTML if HTML content is needed
import DOMPurify from 'dompurify';

function displayUserComment(comment) {
    const clean = DOMPurify.sanitize(comment, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
        ALLOWED_ATTR: ['href']
    });
    document.getElementById('comment').innerHTML = clean;
}

// SAFE: Use createElement for dynamic elements
function createUserCard(user) {
    const card = document.createElement('div');
    card.className = 'user-card';

    const name = document.createElement('h3');
    name.textContent = user.name;

    const email = document.createElement('p');
    email.textContent = user.email;

    card.appendChild(name);
    card.appendChild(email);

    return card;
}

パターン 3:プロトタイプ汚染の防止

使用する場面:オブジェクトのマージや、ユーザーが制御するキーを扱う場合

// DANGEROUS: Prototype pollution vulnerability
function merge(target, source) {
    for (let key in source) {
        target[key] = source[key];
    }
    return target;
}

// SAFE: Check for prototype pollution
function merge(target, source) {
    for (let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
                continue;
            }
            target[key] = source[key];
        }
    }
    return target;
}

// BETTER: Use Object.assign or spread operator
function merge(target, source) {
    return Object.assign({}, target, source);
}

// BEST: Use Object.create(null) for maps
function createSafeMap() {
    return Object.create(null);
}

パターン 4:Promise の適切な処理

使用する場面:複数の非同期処理を管理する場合

// SLOW: Sequential execution
async function loadUserData(userId) {
    const user = await fetchUser(userId);
    const posts = await fetchUserPosts(userId);
    const comments = await fetchUserComments(userId);
    return { user, posts, comments };
}

// FAST: Parallel execution with Promise.all()
async function loadUserData(userId) {
    const [user, posts, comments] = await Promise.all([
        fetchUser(userId),
        fetchUserPosts(userId),
        fetchUserComments(userId)
    ]);
    return { user, posts, comments };
}

// RESILIENT: Promise.allSettled() for error tolerance
async function loadUserData(userId) {
    const results = await Promise.allSettled([
        fetchUser(userId),
        fetchUserPosts(userId),
        fetchUserComments(userId)
    ]);

    return {
        user: results[0].status === 'fulfilled' ? results[0].value : null,
        posts: results[1].status === 'fulfilled' ? results[1].value : [],
        comments: results[2].status === 'fulfilled' ? results[2].value : [],
        errors: results.filter(r => r.status === 'rejected').map(r => r.reason)
    };
}

パターン 5:イベント委譲

使用する場面:複数の要素でイベントを処理する場合

// INEFFICIENT: Multiple event listeners
function setupItemListeners() {
    const items = document.querySelectorAll('.item');
    items.forEach(item => {
        item.addEventListener('click', (e) => {
            console.log('Clicked:', e.target.dataset.id);
        });
    });
}

// EFFICIENT: Event delegation
function setupItemListeners() {
    const container = document.getElementById('item-container');

    container.addEventListener('click', (e) => {
        const item = e.target.closest('.item');
        if (item) {
            console.log('Clicked:', item.dataset.id);
        }
    });
}

// IMPORTANT: Clean up event listeners
class ItemManager {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.handleClick = this.handleClick.bind(this);
        this.container.addEventListener('click', this.handleClick);
    }

    handleClick(e) {
        const item = e.target.closest('.item');
        if (item) {
            this.processItem(item);
        }
    }

    processItem(item) {
        console.log('Processing:', item.dataset.id);
    }

    destroy() {
        this.container.removeEventListener('click', this.handleClick);
    }
}

6. パフォーマンスパターン

パターン 1:メモ化

使用する場面:同じ引数で複数回呼び出される、コストの高い純粋関数

// Bad: Recalculates every time
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Good: Memoized version
function memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

const fibonacciMemo = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
});

// Good: React-style useMemo pattern
function expensiveCalculation(data) {
    // Cache based on data reference
    if (expensiveCalculation.lastData === data) {
        return expensiveCalculation.lastResult;
    }

    const result = data.reduce((acc, item) => {
        // Complex calculation
        return acc + complexOperation(item);
    }, 0);

    expensiveCalculation.lastData = data;
    expensiveCalculation.lastResult = result;
    return result;
}
パターン 2:デバウンスとスロットリング

使用する場面:スクロール、リサイズ、入力など、頻繁に発生するイベント

// Debounce: Execute after delay when events stop
function debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
}

// Good: Debounced search
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(async (query) => {
    const results = await fetchSearchResults(query);
    displayResults(results);
}, 300);

searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

// Throttle: Execute at most once per interval
function throttle(fn, interval) {
    let lastTime = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastTime >= interval) {
            lastTime = now;
            fn.apply(this, args);
        }
    };
}

// Good: Throttled scroll handler
const throttledScroll = throttle(() => {
    updateScrollPosition();
}, 100);

window.addEventListener('scroll', throttledScroll);
パターン 3:遅延読み込み

使用する場面:すぐには必要ない大規模なモジュール、画像、データ

// Bad: Import everything upfront
import { heavyChartLibrary } from 'chart-lib';
import { pdfGenerator } from 'pdf-lib';

// Good: Dynamic imports
async function showChart(data) {
    const { heavyChartLibrary } = await import('chart-lib');
    return heavyChartLibrary.render(data);
}

// Good: Lazy load images with Intersection Observer
function lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]');

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });

    images.forEach(img => observer.observe(img));
}

// Good: Lazy load data on scroll
class InfiniteScroll {
    constructor(container, loadMore) {
        this.container = container;
        this.loadMore = loadMore;
        this.loading = false;

        this.observer = new IntersectionObserver(
            (entries) => this.handleIntersect(entries),
            { rootMargin: '100px' }
        );

        this.observer.observe(this.container.lastElementChild);
    }

    async handleIntersect(entries) {
        if (entries[0].isIntersecting && !this.loading) {
            this.loading = true;
            await this.loadMore();
            this.loading = false;
            this.observer.observe(this.container.lastElementChild);
        }
    }
}
パターン 4:Web Workers

使用する場面:メインスレッドをブロックする可能性のある、CPU 負荷の高いタスク

// Bad: Blocking the main thread
function processLargeDataset(data) {
    return data.map(item => expensiveOperation(item));
}

// Good: Offload to Web Worker
// worker.js
self.onmessage = function(e) {
    const { data, operation } = e.data;

    let result;
    switch (operation) {
        case 'sort':
            result = data.sort((a, b) => a.value - b.value);
            break;
        case 'filter':
            result = data.filter(item => item.active);
            break;
        case 'transform':
            result = data.map(item => expensiveTransform(item));
            break;
    }

    self.postMessage(result);
};

// main.js
class DataProcessor {
    constructor() {
        this.worker = new Worker('worker.js');
    }

    process(data, operation) {
        return new Promise((resolve, reject) => {
            this.worker.onmessage = (e) => resolve(e.data);
            this.worker.onerror = (e) => reject(e);
            this.worker.postMessage({ data, operation });
        });
    }

    terminate() {
        this.worker.terminate();
    }
}

// Usage
const processor = new DataProcessor();
const sortedData = await processor.process(largeArray, 'sort');
パターン 5:効率的な DOM 操作

使用する場面:あらゆる DOM 操作、特にループ内での操作

// Bad: Multiple reflows
function addItems(items) {
    const container = document.getElementById('list');
    items.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item.name;
        container.appendChild(li); // Reflow on each append
    });
}

// Good: Use DocumentFragment
function addItems(items) {
    const container = document.getElementById('list');
    const fragment = document.createDocumentFragment();

    items.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item.name;
        fragment.appendChild(li);
    });

    container.appendChild(fragment); // Single reflow
}

// Good: Batch style changes
function updateStyles(elements, styles) {
    // Bad: Multiple reflows
    // elements.forEach(el => {
    //     el.style.width = styles.width;
    //     el.style.height = styles.height;
    //     el.style.margin = styles.margin;
    // });

    // Good: Use CSS class
    elements.forEach(el => el.classList.add('updated-style'));
}

// Good: Use requestAnimationFrame for visual updates
function animateElement(element, targetX) {
    let currentX = 0;

    function step() {
        currentX += (targetX - currentX) * 0.1;
        element.style.transform = `translateX(${currentX}px)`;

        if (Math.abs(targetX - currentX) > 0.1) {
            requestAnimationFrame(step);
        }
    }

    requestAnimationFrame(step);
}

// Good: Virtual scrolling for large lists
class VirtualList {
    constructor(container, items, itemHeight) {
        this.container = container;
        this.items = items;
        this.itemHeight = itemHeight;
        this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;

        this.container.addEventListener('scroll', () => this.render());
        this.render();
    }

    render() {
        const scrollTop = this.container.scrollTop;
        const startIndex = Math.floor(scrollTop / this.itemHeight);
        const endIndex = startIndex + this.visibleCount;

        // Only render visible items
        const visibleItems = this.items.slice(startIndex, endIndex);
        // ... render logic
    }
}

7. セキュリティ基準

7.1 重大な脆弱性

1. クロスサイトスクリプティング(XSS)

  • ユーザーコンテンツには、innerHTML ではなく常に textContent を使用する
  • HTML のレンダリングが必要な場合は、DOMPurify で HTML をサニタイズする
  • Content Security Policy ヘッダーを設定する

2. プロトタイプ汚染

  • ユーザーが制御するオブジェクトキーを決して信頼しない
  • __proto__、constructor、prototype をブラックリストに登録する
  • 安全にマージするには、Object.assign() またはスプレッド演算子を使用する

3. 正規表現サービス拒否(ReDoS)

  • 壊滅的なバックトラッキングを引き起こすパターンを避ける
  • 長い入力で正規表現をテストする
  • ユーザー提供の正規表現にはタイムアウトを実装する

4. 安全でない乱数

  • セキュリティ用途(トークン、セッション ID)に Math.random() を決して使用しない
  • Node.js では crypto.randomBytes() を使用する
  • ブラウザーでは crypto.getRandomValues() を使用する

5. 依存関係の脆弱性

  • デプロイの前に毎回 npm audit を実行する
  • 継続的な監視には Dependabot または Snyk を使用する
  • 依存関係を最新の状態に保つ

7.2 OWASP Top 10 2025 との対応
OWASP IDカテゴリリスク迅速な緩和策
A01:2025アクセス制御の不備重大サーバー側での検証
A02:2025セキュリティ設定の不備高セキュリティヘッダー、デバッグの無効化
A03:2025サプライチェーンの障害高npm audit、ロックファイル
A04:2025安全でない設計中脅威モデリング
A05:2025識別と認証重大httpOnly Cookie
A06:2025脆弱なコンポーネント高依存関係のスキャン
A07:2025暗号化の失敗重大crypto モジュールの使用
A08:2025インジェクション重大入力のサニタイズ
A09:2025ログ記録の失敗中構造化ログ
A10:2025例外処理中適切なエラー処理

8. テスト

Vitest/Jest による単体テスト
// Setup: vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
    test: {
        environment: 'jsdom',
        coverage: {
            provider: 'v8',
            reporter: ['text', 'json', 'html'],
            threshold: {
                branches: 80,
                functions: 80,
                lines: 80,
                statements: 80
            }
        }
    }
});

// Example tests
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

describe('UserService', () => {
    let service;
    let mockFetch;

    beforeEach(() => {
        mockFetch = vi.fn();
        global.fetch = mockFetch;
        service = new UserService();
    });

    afterEach(() => {
        vi.restoreAllMocks();
    });

    it('should fetch user successfully', async () => {
        const mockUser = { id: 1, name: 'John' };
        mockFetch.mockResolvedValue({
            ok: true,
            json: () => Promise.resolve(mockUser)
        });

        const user = await service.getUser(1);

        expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
        expect(user).toEqual(mockUser);
    });

    it('should handle fetch errors', async () => {
        mockFetch.mockResolvedValue({
            ok: false,
            status: 404,
            statusText: 'Not Found'
        });

        await expect(service.getUser(999))
            .rejects
            .toThrow('User not found');
    });

    it('should handle network errors', async () => {
        mockFetch.mockRejectedValue(new Error('Network error'));

        await expect(service.getUser(1))
            .rejects
            .toThrow('Network error');
    });
});

// Testing async functions
describe('Async operations', () => {
    it('should handle Promise.all correctly', async () => {
        const results = await Promise.all([
            fetchData('a'),
            fetchData('b')
        ]);

        expect(results).toHaveLength(2);
    });

    it('should timeout long operations', async () => {
        vi.useFakeTimers();

        const promise = timeoutOperation(1000);
        vi.advanceTimersByTime(1000);

        await expect(promise).rejects.toThrow('Timeout');

        vi.useRealTimers();
    });
});
統合テスト
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createServer } from '../server';

describe('API Integration', () => {
    let server;
    let baseUrl;

    beforeAll(async () => {
        server = await createServer();
        baseUrl = `http://localhost:${server.address().port}`;
    });

    afterAll(async () => {
        await server.close();
    });

    it('should create and fetch user', async () => {
        // Create user
        const createRes = await fetch(`${baseUrl}/api/users`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: 'Test User' })
        });

        const created = await createRes.json();
        expect(created.id).toBeDefined();

        // Fetch user
        const fetchRes = await fetch(`${baseUrl}/api/users/${created.id}`);
        const fetched = await fetchRes.json();

        expect(fetched.name).toBe('Test User');
    });
});
DOM テスト
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';

describe('DOM manipulation', () => {
    let document;

    beforeEach(() => {
        const dom = new JSDOM('<!DOCTYPE html><div id="app"></div>');
        document = dom.window.document;
    });

    it('should render list items', () => {
        const app = document.getElementById('app');
        const items = ['a', 'b', 'c'];

        renderList(app, items);

        const listItems = app.querySelectorAll('li');
        expect(listItems.length).toBe(3);
        expect(listItems[0].textContent).toBe('a');
    });

    it('should handle click events', () => {
        const button = document.createElement('button');
        let clicked = false;

        button.addEventListener('click', () => { clicked = true; });
        button.click();

        expect(clicked).toBe(true);
    });
});

9. よくある間違い

間違い 1:未処理の Promise 拒否
// DON'T
fetch('/api/data').then(res => res.json());

// DO
fetch('/api/data')
    .then(res => res.json())
    .catch(err => console.error('Failed:', err));
間違い 2:イベントリスナーによるメモリーリーク
// DON'T
function setupWidget() {
    const button = document.getElementById('btn');
    button.addEventListener('click', handleClick);
}

// DO
function setupWidget() {
    const button = document.getElementById('btn');
    const handleClick = () => { /* ... */ };
    button.addEventListener('click', handleClick);

    return {
        destroy() {
            button.removeEventListener('click', handleClick);
        }
    };
}
間違い 3:var の使用
// DON'T
for (var i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 100);
}

// DO
for (let i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 100);
}
間違い 4:緩い等価比較
// DON'T
if (value == '0') { }

// DO
if (value === '0') { }
間違い 5:イベントループのブロック
// DON'T
function processLargeData(data) {
    for (let i = 0; i < 1000000; i++) {
        complexCalculation(data[i]);
    }
}

// DO
const worker = new Worker('processor.js');
worker.postMessage(data);

10. チェックリスト

フェーズ 1:コードを書く前
  • 新しい機能のテストを作成済み(TDD)
  • セキュリティ脅威モデルを確認済み
  • パフォーマンス要件を特定済み
  • 依存関係を監査済み(npm audit)
  • API コントラクトを定義済み
フェーズ 2:実装中
  • const/let を使用している(var は使用しない)
  • 厳密等価演算子(===)を使用している
  • すべての非同期処理にエラー処理がある
  • ユーザー入力を検証し、サニタイズしている
  • ユーザー入力に eval() または Function() を使用していない
  • イベントリスナーにクリーンアップメソッドがある
  • ユーザーコンテンツに innerHTML を使用していない
  • オブジェクトのマージ時にプロトタイプ汚染を防止している
フェーズ 3:コミット前
  • すべてのテストが合格する(npm test)
  • テストカバレッジがしきい値を満たしている(>80%)
  • 本番コードに console.log() がない
  • ESLint/Prettier のチェックが合格する
  • バンドルサイズを確認済み
  • パフォーマンスをプロファイリング済み
  • セキュリティヘッダーを設定済み(CSP など)
  • シークレット用の環境変数を設定済み
  • 依存関係を最新の状態に更新済み
絶対にしないこと
  • ユーザー入力に対して eval() や Function() コンストラクターを使用する
  • トークンや API キーを localStorage に保存する
  • 検証せずにユーザー入力を信頼する
  • サニタイズされていないコンテンツに innerHTML を使用する
  • Promise の拒否を無視する
  • セキュリティ目的で Math.random() を使用する
  • var を使用する(必ず const または let を使用する)
  • イベントループをブロックする
必ず行うこと
  • 厳密な等価演算子(===)を使用する
  • 非同期コードでエラーを処理する
  • 入力を検証してサニタイズする
  • イベントリスナーをクリーンアップする
  • 適切な HTTP ヘッダー(CSP、CORS)を使用する
  • デプロイ前に npm audit を実行する
  • シークレットには環境変数を使用する
  • 重要な処理経路のテストを作成する

11. まとめ

JavaScript のエキスパートとして、以下に重点を置きます。

  1. TDD ワークフロー - まずテストを書き、その後に実装する
  2. モダンな ES6+ のパターン
  3. セキュリティ優先の開発(XSS、プロトタイプ汚染の防止)
  4. 非同期処理の習熟(Promise、エラー処理)
  5. パフォーマンス最適化(メモ化、遅延読み込み、Web Workers)
  6. 本番品質(テスト、モニタリング)

重要な原則:

  • 実装前にテストを書く
  • 最初からパフォーマンスを最適化する
  • デフォルトで安全なコードを書く
  • エラーを適切に処理する
  • ユーザー入力を決して信頼しない

JavaScript は信頼できない環境で実行されます。セキュリティと堅牢性は基本要件です。

コメント