JavaScriptでアプリケーションを開発する際、乱数の生成は避けて通れない技術の一つと言えます。
ゲームの演出からパスワードの生成、データのサンプリング、さらにはユニークなIDの発行まで、その用途は多岐にわたります。
しかし、JavaScriptには複数の乱数生成手法が存在し、用途に合わせて適切な手法を選ばなければ、予期せぬバグやセキュリティリスクを招く可能性があります。
本記事では、標準的なMath.random()の基礎から、セキュリティ要件を満たすためのWeb Crypto APIまで、エンジニアが知っておくべき乱数生成の知識を網羅して解説します。
Math.random()の基本とその仕組み
JavaScriptにおいて最も手軽に乱数を得る方法が、Math.random()関数を使用することです。
この関数は、0以上1未満(0は含むが1は含まない)の範囲で浮動小数点数の疑似乱数を返します。
まずは、最もシンプルな実行例を確認してみましょう。
// 基本的な乱数生成
const randomNumber = Math.random();
console.log(randomNumber);
0.743128594321054
このように、実行するたびに異なる小数が出力されます。
注意点として、Math.random()は「疑似乱数生成器(PRNG)」と呼ばれ、アルゴリズムによって計算で算出されています。
決定論的なアルゴリズムに基づいているため、厳密な意味での真の乱数ではない点に留意してください。
範囲を指定した乱数の生成
実務では0から1の範囲ではなく、「1から10まで」や「50から100まで」といった特定の範囲の数値が欲しい場合がほとんどです。
任意の範囲(minからmaxまで)の乱数を得るには、以下の数式を利用します。
/**
* minからmaxまでの範囲で浮動小数点数の乱数を返す
*/
function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}
console.log(getRandomFloat(1, 10));
4.8291045231
この数式では、まずMath.random()の結果に範囲の幅(max – min)を掛け、そこに最小値(min)を足すことで、目的の範囲にシフトさせています。
整数値の乱数を取得する
サイコロの目や配列のインデックスなど、整数が必要な場合はMath.floor()やMath.ceil()を組み合わせて端数を処理します。
一般的に最もよく使われる「min以上max以下の整数」を取得するコードは以下の通りです。
/**
* min以上max以下の整数を返す関数
*/
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
// maxも含めるための計算式
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(getRandomInt(1, 6)); // サイコロの目
3
「maxの値も含む」ようにするためには、範囲の幅に「+1」を加算することが重要なポイントです。
これを忘れると、最大値が絶対に出現しないという論理的なバグに繋がります。
配列と乱数の組み合わせ
JavaScriptの開発において、配列の中からランダムに要素を抽出したり、配列そのものをシャッフルしたりするケースは非常に多いです。
配列からランダムに1つの要素を選ぶ
配列の長さをMath.random()に掛けることで、有効なインデックス番号をランダムに生成できます。
const fruits = ['Apple', 'Banana', 'Orange', 'Grape', 'Melon'];
// 配列の長さ未満の整数インデックスを生成
const randomIndex = Math.floor(Math.random() * fruits.length);
const selectedFruit = fruits[randomIndex];
console.log(`今日のラッキーフルーツは: ${selectedFruit}`);
今日のラッキーフルーツは: Orange
この手法は、バナー広告のランダム表示や、おみくじ機能の実装などに広く活用されています。
配列をランダムに並び替える(フィッシャー–イェーツのシャッフル)
配列をシャッフルする場合、sort(() => Math.random() - 0.5)という手法がよく紹介されますが、これは偏りが発生するため推奨されません。
公平なシャッフルを実現するためには、「フィッシャー–イェーツ(Fisher-Yates)のアルゴリズム」を使用するのが最適です。
function shuffleArray(array) {
// 配列の末尾から順に入れ替えていく
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
// 分割代入を使って要素をスワップ
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(shuffleArray(numbers));
[5, 2, 8, 1, 10, 3, 7, 4, 9, 6]
このアルゴリズムは、すべての並び順が等確率で出現することが数学的に証明されており、非常に効率的です。
Math.random()の限界とセキュリティリスク
ここまで紹介したMath.random()は便利ですが、決定的な弱点があります。
それは、暗号学的に安全ではない(Cryptographically Secureではない)という点です。
現代のブラウザで採用されているMath.random()のエンジン(V8など)は、内部的に「xorshift128+」などのアルゴリズムを使用しています。
これらは高速ですが、過去に出力された値のパターンから、次に生成される値を予測することが理論上可能です。
そのため、以下のような用途にMath.random()を使用してはいけません。
- ユーザーのパスワード生成
- セッションIDの発行
- 暗号化キーの生成
- 高額な賞金が掛かったギャンブルの抽選
セキュリティが重要な場面では、次に解説するWeb Crypto APIを利用する必要があります。
Web Crypto APIによる安全な乱数生成
セキュリティ要件を満たす必要がある場合、ブラウザ標準のcrypto.getRandomValues()メソッドを使用します。
これは、OSレベルで生成されるエントロピー(予測不可能な情報源)を利用するため、極めて高い安全性が確保されています。
crypto.getRandomValues()の使い方
このメソッドは、引数として渡された「型付き配列(TypedArray)」を乱数で埋めます。
// 32ビットの整数を格納する配列を1つ用意
const array = new Uint32Array(1);
// 安全な乱数で配列を埋める
crypto.getRandomValues(array);
console.log(`安全な乱数: ${array[0]}`);
安全な乱数: 3829104752
Uint32Arrayを使用した場合、0から4,294,967,295までの範囲で整数が得られます。
得られた数値を最大値で割れば、Math.random()のように0から1の範囲の数値に変換することも可能です。
function secureRandom() {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
// 2の32乗で割って0〜1の範囲にする
return array[0] / (0xffffffff + 1);
}
console.log(secureRandom());
0.1294857213
UUIDの生成:crypto.randomUUID()
一意の識別子が必要な場合、以前は複雑なライブラリが必要でしたが、現在のJavaScript環境では標準でUUIDを生成できます。
crypto.randomUUID()を使用すれば、重複の可能性が事実上ゼロであるバージョン4のUUIDを即座に取得できます。
const uuid = crypto.randomUUID();
console.log(uuid);
f47ac10b-58cc-4372-a567-0e02b2c3d479
データベースの主キーや、フロントエンドでの一意な要素IDの割り振りに非常に便利です。
手法の比較表
どちらの手法を選択すべきか迷った際は、以下の比較表を参考にしてください。
| 特徴 | Math.random() | Web Crypto API |
|---|---|---|
| 実行速度 | 非常に高速 | Math.randomに比べるとやや低速 |
| 予測可能性 | 予測可能(疑似乱数) | 予測不可能(暗号学的乱数) |
| セキュリティ | 不適合 | 適合(高セキュリティ) |
| 主な用途 | UI演出、ゲーム、簡単なシミュレーション | パスワード、トークン、暗号化、ID生成 |
| 使いやすさ | 非常にシンプル | 型付き配列の知識が必要 |
応用編:重み付き乱数の実装
単純な一様分布ではなく、特定の項目が出現しやすい「重み付き」の乱数が必要なケースもあります。
例えば、ゲームのガチャで「レアアイテムの排出率を低くする」といった実装です。
function getWeightedRandom(items) {
// 重みの合計を計算
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
// 0から合計値の間で乱数を生成
let random = Math.random() * totalWeight;
// 乱数から各重みを引いていき、0以下になった要素を返す
for (const item of items) {
if (random < item.weight) {
return item.name;
}
random -= item.weight;
}
}
const gacha = [
{ name: 'スライム', weight: 80 },
{ name: 'ドラゴン', weight: 15 },
{ name: '伝説の勇者', weight: 5 }
];
console.log(getWeightedRandom(gacha));
スライム
このロジックを応用すれば、ビジネスロジックにおける柔軟なランダム抽選が実現できます。
まとめ
JavaScriptにおける乱数生成は、シンプルなようで奥が深いテーマです。
日常的なUIの実装や、予測されても問題がない数値の生成であれば、軽量で高速なMath.random()で十分対応できます。
しかし、セキュリティに関わるデータや、厳格な公平性が求められるシステムでは、必ずWeb Crypto APIを選択するようにしてください。
また、整数の範囲計算や配列のシャッフルなど、頻出するパターンについてはユーティリティ関数として共通化しておくと開発効率が高まります。
用途に応じた正しいメソッドを選択し、バグのない安全なプログラムを構築しましょう。
