Aquí hay una versión optimizada del código portado desde Python por @Derek, con la opción destructiva agregada (en el lugar) que lo convierte en el algoritmo más rápido posible si puede seguirlo. De lo contrario, realiza una copia completa o, para una pequeña cantidad de elementos solicitados de una matriz grande, cambia a un algoritmo basado en selección.
function sample(pool, k, destructive) {
var n = pool.length;
if (k < 0 || k > n)
throw new RangeError("Sample larger than population or is negative");
if (destructive || n <= (k <= 5 ? 21 : 21 + Math.pow(4, Math.ceil(Math.log(k*3, 4))))) {
if (!destructive)
pool = Array.prototype.slice.call(pool);
for (var i = 0; i < k; i++) {
var j = i + Math.random() * (n - i) | 0;
var x = pool[i];
pool[i] = pool[j];
pool[j] = x;
}
pool.length = k;
return pool;
} else {
var selected = new Set();
while (selected.add(Math.random() * n | 0).size < k) {}
return Array.prototype.map.call(selected, i => population[i]);
}
}
En comparación con la implementación de Derek, el primer algoritmo es mucho más rápido en Firefox y un poco más lento en Chrome, aunque ahora tiene la opción destructiva, la de mayor rendimiento. El segundo algoritmo es simplemente un 5-15% más rápido. Intento no dar números concretos, ya que varían según kyn y probablemente no signifiquen nada en el futuro con las nuevas versiones del navegador.
La heurística que hace la elección entre algoritmos se origina en el código Python. Lo dejé como está, aunque a veces selecciona el más lento. Debería estar optimizado para JS, pero es una tarea compleja ya que el rendimiento de los casos de esquina depende del navegador y de su versión. Por ejemplo, cuando intente seleccionar 20 de 1000 o 1050, cambiará al primer o segundo algoritmo en consecuencia. En este caso, el primero se ejecuta 2 veces más rápido que el segundo en Chrome 80, pero 3 veces más lento en Firefox 74.