在昨天的文章中,我谈到了在 Alpine.js 中构建色彩面板工具,随机性在其中扮演了重要角色:每个色样都是由随机选择的色调 Hue (0..360)
、饱和度 Saturation (0..100
) 和亮度 Lightness (0..100
) 值复合生成的。在制作那个演示时,我偶然发现了 Web Crypto API。通常,在生成随机值时,我会使用 Math.random()
方法;但是,MDN 文档提到 Crypto.getRandomValues()
更安全。因此,我最终尝试了 Crypto
(必要时回退到 Math
模块)。但这让我想知道 "更安全" 是否真的意味着在我的特定用例中 "更随机"。
**在 GitHub 上的 *JavaScript Demos 项目中运行此演示*。
**在 GitHub 上的 *JavaScript Demos 项目中查看此代码*。
从安全的角度来看,随机性有一个实际的含义。我不是安全专家;但是,我的理解是,当一个伪随机数生成器(PRNG)被认为是 "安全的",这意味着它将产生的数字序列——或者已经产生的——不能被攻击者推断。
当涉及到 "随机颜色生成器" 时,比如我的色彩面板工具,"随机性" 的概念就模糊得多了。在我的例子中,随机颜色生成的随机性只是对用户 "感觉" 上的随机性。换句话说,随机性的有效性是整体用户体验(UX)的一部分。
为此,我想尝试使用 Math.random()
和 crypto.getRandomValues()
生成一些随机的 视觉 元素,看看其中一种方法是否在 感觉 上有实质性的不同。每次试验将包含一个随机生成的 <canvas>
元素和一组随机生成的整数。然后,我将使用我(严重有缺陷的)人类直觉来看看其中一项试验是否看起来 "更好"。
Math.random()
方法通过返回一个介于 0
(包括)和 1
(不包括)之间的小数来工作。这可以用来通过将随机结果乘以可能值的范围来生成随机整数。
换句话说,如果 Math.random()
返回 0.25
,你会选择最接近给定最小-最大范围 25% 的值。而如果 Math.random()
返回 0.97
,你会选择最接近给定最小-最大范围 97% 的值。
crypto.getRandomValues()
方法的工作原理非常不同。与返回单个值不同,你传入一个预分配大小(长度)的 Typed Array。然后 .getRandomValues()
方法用可以由给定类型的最小/最大值存储的随机值填充该数组。
为了使这次探索更简单,我想让这两种方法大致上工作得相同。所以,我将迫使两种算法都依赖于小数生成。这意味着,我必须将 .getRandomValues()
返回的 value
强制转换为小数 (0..1
):
value / ( maxValue + 1 )
我将在两个方法中封装这种差异,randFloatWithMath()
和 randFloatWithCrypto()
:
/**
* 我使用 Math 模块返回一个介于 0(包括)和 1(不包括)之间的随机浮点数。
*/
function randFloatWithMath() {
return Math.random();
}
/**
* 我使用 Crypto 模块返回一个介于 0(包括)和 1(不包括)之间的随机浮点数。
*/
function randFloatWithCrypto() {
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
return ( randomInt / ( maxInt + 1 ) );
}
有了这两种方法,我可以将它们之一分配给 randFloat()
引用,该引用可以用来无缝地在给定范围内使用任一算法交替生成随机值:
/**
* 我在给定的最小和最大值之间(包括两者)生成一个随机整数。
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
现在来创建实验。用户界面很小,由 Alpine.js 驱动。每个试验使用相同的 Alpine.js 组件;但是,它的构造函数接收一个参数,该参数确定将使用哪种 randFloat()
实现:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1>
探索 JavaScript 中的随机性
</h1>
<div class="side-by-side">
<section x-data="Explore( 'math' )">
<h2>
Math 模块
</h2>
<!-- 一个非常大数量的随机 {X,Y} 坐标。 -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- 一个非常小数量的随机值坐标。 -->
<p x-ref="list"></p>
<p>
持续时间:<span x-text="duration"></span>
</p>
</section>
<section x-data="Explore( 'crypto' )">
<h2>
Crypto 模块
</h2>
<!-- 一个非常大数量的随机 {X,Y} 坐标。 -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- 一个非常小数量的随机值坐标。 -->
<p x-ref="list"></p>
<p>
持续时间:<span x-text="duration"></span>毫秒
</p>
</section>
</div>
<script type="text/javascript" src="./main.js" defer></script>
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
</body>
</html>
如你所见,每个 x-data="Explore"
组件包含两个 x-refs
:canvas
和 list
。当组件初始化时,它将使用 fillCanvas()
和 fillList()
方法分别填充这两个引用的随机值。
这是我的 JavaScript / Alpine.js 组件:
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Math module.
*/
function randFloatWithMath() {
return Math.random();
}
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Crypto module.
*/
function randFloatWithCrypto() {
// This method works by filling the given array with random values of the given type.
// In our case, we only need one random value, so we're going to pass in an array of
// length 1.
// --
// Note: For better performance, we could cache the typed array and just keep passing
// the same reference in (cuts performance in half). But, we're exploring the
// randomness, not the performance.
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
// Unlike Math.random(), crypto is giving us an integer. To feed this back into the
// same kind of math equation, we have to convert the integer into decimal so that we
// can figure out where in our range our randomness leads us.
return ( randomInt / ( maxInt + 1 ) );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
function Explore( algorithm ) {
// Each instance of this Alpine.js component is assigned a different randomization
// strategy for floats (0..1). Other than that, the component instances behave exactly
// the same.
var randFloat = ( algorithm === "math" )
? randFloatWithMath
: randFloatWithCrypto
;
return {
duration: 0,
// Public methods.
init: init,
// Private methods.
fillCanvas: fillCanvas,
fillList: fillList,
randRange: randRange
}
// ---
// PUBLIC METHODS.
// ---
/**
* I initialize the Alpine.js component.
*/
function init() {
var startedAt = Date.now();
this.fillCanvas();
this.fillList();
this.duration = ( Date.now() - startedAt );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I populate the canvas with random {X,Y} pixels.
*/
function fillCanvas() {
var pixelCount = 200000;
var canvas = this.$refs.canvas;
var width = canvas.width;
var height = canvas.height;
var context = canvas.getContext( "2d" );
context.fillStyle = "deeppink";
for ( var i = 0 ; i < pixelCount ; i++ ) {
var x = this.randRange( 0, width );
var y = this.randRange( 0, height );
// As we add more pixels, let's make the pixel colors increasingly opaque. I
// was hoping that this might help show potential clustering of values.
context.globalAlpha = ( i / pixelCount );
context.fillRect( x, y, 1, 1 );
}
}
/**
* I populate the list with random 0-9 values.
*/
function fillList() {
var list = this.$refs.list;
var valueCount = 105;
var values = [];
for ( var i = 0 ; i < valueCount ; i++ ) {
values.push( this.randRange( 0, 9 ) );
}
list.textContent = values.join( " " );
}
/**
* I generate a random integer in between the given min and max, inclusive.
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
}
如上所述,从人类的角度来看,随机性是非常模糊的。这更多是关于 "感觉" 而不是数学概率。例如,连续生成两个 相同 的值的概率与生成 任何两个 值的概率是相同的。但对人类来说,这很显眼 —— 它 感觉 不同。
话虽如此,从分布的角度来看,这些并排随机生成的可视化看起来没有实质性的不同。当然,Crypto
模块明显慢得多(其中一半是 Typed Array 分配的成本)。但从 "感觉" 的角度来看,一个并不明显优于另一个。
总之,当生成一个随机颜色面板时,我可能没有必要使用 Crypto
模块 —— 我本应该继续使用 Math
。它更快,并且 感觉 同样随机。我会把 Crypto
的东西留给任何客户端加密工作(我从未做过)。