Arreglos 2

Si bien la forma correcta de crear las clases, respetando los 4 principios de la POO, es la empleada en el capítulo 7, por claridad, en este y en los siguientes capítulos se es mucho más flexible y por ejemplo, no siempre se declararán todas las propiedades como privadas, no se escribirán todos los métodos getter/setter y no se sobreescribirán todos los métodos "toString" y "valueOf".

En este capítulo se resuelven algunos problemas que involucran vectores. Para ese fin se emplean las estructuras for y for-of, así como los métodos iterativos disponibles para el trabajo con matrices. En general, los problemas que involucran arreglos, se resuelven de forma más sencilla (aunque no siempre más eficiente) empleando los métodos disponibles.

Por ejemplo, para calcular el valor de la siguiente expresión:

\[ \sum_{i=1}^{n}{v_i} \]

Es decir, calcular la sumatoria de los elementos de un vector "v", como el siguiente:

v = [1, 2, 4, 5, 6, 8, 12, 23, 25, 42, 2, 6, 7, 3, 112];

La forma más simple consiste en llamar al método sum:

v.sum()

Igualmente (sobre todo si no se trabaja en la calculadora) se puede recurrir al método reduce:

v.reduce((s,e)=>s+e)

Es posible, también, emplear el método forEach:

s = 0;
v.forEach(e => s+=e);
s

En este caso, como forEach simplemente ejecuta la función para cada uno de los elementos del array (v), el acumulador "s" debe ser iniciado antes del método y mostrado al final del mismo.

Se puede emplear también la estructura for-of, que es una variante de la estructura for y que tiene la siguiente sintaxis:

for (e of array) instrucciones;

Donde las instrucciones son ejecutadas para cada uno de los elementos e del array. Estando el elemento e disponible en las instrucciones del ciclo.

Empleando esta estructura, la solución es similar a la del método forEach:

s = 0;
for (let e of v) s+=e;
s

Como for-of no recibe una función, sino que ejecuta directamente las instrucciones, se escribe la expresión que suma los elementos "e" del vector.

Por supuesto, también se puede emplear la recursividad para resolver este tipo de problemas. Así, con una función flecha recursiva inmediatamente invocada, la solución es:

(sum=(v,i=v.length-1)=>i===0?v[0]:sum(v,i-1)+v[i])(v)

Como ya se vio previamente, una sumatoria (o productoria) con límites definidos, puede ser traducida directamente en un ciclo for, siendo los límites del ciclo los límites de la sumatoria (o productoria) y la variable de la sumatoria (o productoria) el contador del ciclo. El acumulador (la variable que almacena el resultado) debe iniciar en 0 si es una sumatoria y en 1 si es una productoria.

Así, para calcular la sumatoria con la estructura for, se escribe:

s = 0;
for (let i=0; i<v.length; i++) s+=v[i];
s

Es importante recordar que cuando se trabaja con vectores (y objetos en general) las variables son punteros, es decir variables que guardan la dirección de memoria del objeto no el objeto en sí. Por esa razón, para efectivamente copiar un vector, se debe emplear el método slice (o si se trabaja en la calculadora, se puede emplear también el método copy).

Añadir métodos y atributos a una clase

Aunque no es una práctica recomendada, en ocasiones resulta útil añadir un nuevo método (rara vez un nuevo atributo) a una clase.

En JavaScript, es posible añadir nuevos atributos (propiedades) y métodos estáticos y dinámicos, a las clases tanto predefinidas, como creadas. Como se recordará, los métodos/atributos estáticos son aquellos que pertenecen únicamente a la clase, los objetos de esa clase no tienen acceso directo a los atributos estáticos y los métodos estáticos no tienen acceso directo a los atributos de los objetos de esa clase. Los métodos y atributos estáticos se comportan igual que las variables y funciones de la PE, sólo que deben ser llamadas escribiendo previamente el nombre de la clase (y un punto), tal como se ha hecho con Math, que sólo cuenta con métodos (funciones) y atributos (variables) estáticos.

Los métodos y atributos dinámicos, por otra parte, son propiamente métodos y atributos de la POO. Los métodos dinámicos de una clase tienen acceso a los atributos de los objetos de esa clase y los objetos de una clase tienen acceso directo a los métodos de esa clase. Los atributos dinámicas permiten proteger los datos del objeto (el estado del objeto) validando los valores recibidos y permitiendo que sólo se tenga acceso a un conjunto limitado de los atributos desde los objetos de la clase o desde otras la clases.

Los métodos y atributos estáticos se añaden a la clase, simplemente escribiendo el nombre del nuevo método (o propiedad) al nombre de la clase (separados con un punto) y asignándole la nueva función (o valor). Por ejemplo en las siguientes instrucciones se añaden a Math la propiedad "miNúmero", con el valor 5 y el método "cubo", que calcula el cubo del número que recibe. Luego se emplea el nuevo método y el nuevo atributo:

Math.miNúmero = 5;
Math.cubo = x => x*x*x;
Math.cubo(Math.miNúmero)

Los métodos dinámicos estándar, se añaden a la propiedad prototype de la clase, en la práctica, casi nunca se añaden nuevas propiedades dinámicas, porque no es útil que todos los objetos de una clase tengan un atributo con el mismo valor. Por ejemplo, en las siguientes instrucciones se añade a la clase Number el método dinámico "cubo" (que calcula el cubo del número). Luego se emplea el nuevo método para calcular el cubo de los números 5 y 7:

Number.prototype.cubo = function() { return this*this*this; };
(5).cubo()
(7).cubo()

Como se muestra en el ejemplo, el nuevo método se añade como una función anónima. En estos casos no se puede emplear una función flecha, porque las funciones flecha no tienen la variable this (como se recordará "this" es el nombre que le da JavaScript al objeto desde el cual se llama al método).

Observe que el nuevo método es llamado directamente desde dos números. Esto es posible porque JavaScript convierte automáticamente el número en un objeto de tipo Number y luego llama al método desde ese objeto. Se emplean los paréntesis únicamente para que el punto del método no sea confundido con el punto decimal. En lugar de escribir paréntesis, se pude emplear igualmente un espacio:

5 .cubo()
7 .cubo()

Para añadir métodos getter/setter, se debe emplear el método estático defineProperty de la clase Object, de acuerdo a la siguiente sintaxis:

Object.defineProperty(objeto o clase, propiedad, { get() { ... }, set(valor) { ... }, enumerable: true/false, configurable: true/false })

Donde solo es imprescindible definir un método getter (get) o setter (set), el resto las propiedades son opcionales.

Por defecto enumerable y configurable son falsos, lo que implica que él o los métodos definidos no aparecen como propiedades de la clase (enumerable) y que no pueden ser modificados (configurable). Dado que, casi siempre, es necesario modificar o corregir el código, es prácticamente imprescindible (cuando se trabaja en la calculadora Javascript) colocar la propiedad configurable en true.

Con el método defineProperty se puede añadir también una propiedad a la clase, de acuerdo a la siguiente sintaxis:

Object.defineProperty(objeto o clase, propiedad, { value: valor, writable: true/false, enumerable: true/false, configurable: true/false })

Donde sólo es necesario asignar el valor a la propiedad value. El resto de las campos son opcionales y por defecto tienen un valor igual a false, que, en el caso de writable, significa que el valor asignado a la propiedad no puede ser modificado.

Para definir dos o más métodos (o propiedades) al mismo tiempo, una mejor alternativa es el método estático defineProperties, que tiene la sintaxis:

Object.defineProperties(objeto o clase, { propiedad1/método1: { ... }, propiedad2/método2: { ... }, ... propiedadn/métodon: { ... } })

Donde, cada propiedad/método, puede tener todos los campos mostrados en defineProperty.

Sin embargo, se recalca que el añadir propiedades y métodos dinámicos a una clase predefinida, no es una práctica aconsejable, porque los métodos dinámicos afectan a todos los objetos creados a partir de esa clase. En la asignatura se añaden porque simplifica la elaboración de programas en la calculadora y porque, los cambios hechos, sólo afecta al código escrito en el capítulo.

Ejemplos

Promedio de un vector de números

En este ejemplo se programa un método para calcular el promedio de los elementos de un vector:

\[ \bar{x} = \dfrac{\displaystyle\sum_{i=0}^{n-1}{x_i}}{n} \]

Para ese fin se crea la clase "SV", donde el vector se recibe en la propiedad privada "x". El promedio se calcula en el método getter "promedio" y se programan el método getter para devolver el valor de la propiedad privada "x":

SV = class { #x; constructor(...elementos) { this.#x = elementos; } get promedio() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); let s = 0; for (let i=0; i<n; i++) s+=x[i]; return s/n; } get x() { return this.#x; } };

Observe que en el constructor se ha empleado un parámetro de tipo rest (resto), el cual se identifica escribiendo 3 puntos delante del nombre del parámetro (...elementos). Los parámetros de tipo rest deben ser o el único parámetro de la función o el último de los parámetros de la función. Un parámetro rest crea un array con todos los datos que recibe, por eso es empleado en el constructor, para que todos los datos que se pasen al mismo sean convertidos en un array.

Las constantes "x" y "n"", en el getter "promedio", se declaran por comodidad y claridad, para no tener que trabajar con "this.x" y "this.x.length".

Llamando al método con algunos valores de prueba, se obtiene:

v1 = new SV(1, 9, 12, 3, 5, 7, 12); print(v1.x)
v1.promedio

No obstante, si el objeto se emplea únicamente para hacer una llamado (como ocurre en este caso) lo más práctico es crear el objeto y llamar al método directamente desde el objeto creado (sin guardar el objeto en una variable):

new SV(1, 9, 12, 3, 5, 7, 12).promedio

Se procede de manera similar en el siguiete ejemplod, donde primero se genera un vector con 20 números enteros aleatorios comprendidos entre 1 y 30. Luego se crea un objeto de la clase SV con los elementos de ese vector y se llama al getter "promedio":

randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promedio

En este ejemplo, para que el constructor de la clase reciba los elementos del vector (no el vector), se emplea el operador spread (esparcir) que tiene el mismo símbolo que el operador rest (es decir 3 puntos consecutivos ...), pero que hace lo contrario, es decir, convierte un array en una secuencia de valores separados con comas (que son los datos que requiere el constructor de la clase).

Observe también que antes de generar el vector con números aleatorios, se inicia la semilla (randseed) en 1, para que genere siempre la misma secuencia de números pseudoaleatorios (de manera que el ejemplo pueda ser repetido). Recuerde que para volver a generar números realmente aleatorios, se debe asignar null a la variable randseed ).

Otros ejemplos similares, pero generando los vectores con los métodos range y linspace, son:

c = [].range(17, 51, 2); print(c); new SV(...c).promedio
d = [].linspace(5, 10, 21); print(d); new SV(...d).promedio

Por supuesto, el problema puede ser resuelto con otras estructuras y métodos, así con la estructura for-of, la solución, es:

Object.defineProperty(SV.prototype, "promedioa", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); let s = 0; for (let e of x) s+=e; return s/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promedioa
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promedioa

El problema resuelto con el método forEach, es:

Object.defineProperty(SV.prototype, "promediob", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); let s = 0; x.forEach(e=>s+=e); return s/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promediob
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promediob

Como se vio en el anterior capítulo, además del valor del elemento ("e"), la función (en este caso la función flecha: e=>s+=e) puede recibir también el índice y el array, sin embargo, en este ejemplo, no se reciben esos datos, porque no son empleados dentro de la función.

El problema resuelto con el método reduce, es:

Object.defineProperty(SV.prototype, "promedioc", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); return x.reduce((s,e)=>s+e)/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promedioc
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promedioc

En este caso, reduce devuelve la sumatoria, la cual se divide entre el número de elementos para obtener el promedio.

Al igual que la función de forEach y como ocurre con la mayoría de las funciones de los métodos de la clase Array, reduce, puede recibir, además del acumulador, el valor actual del elemento, el índice y el array, pero, en este caso no son recibidos, porque no se utilizan en la función.

Cono ya se vio en el anterior capítulo y aunque no es necesario en este caso, se puede asignar también un valor inicial al acumulador "s" (en este caso 0, porque se trata de una sumatoria):

Object.defineProperty(SV.prototype, "promediod", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); return x.reduce((s,e)=>s+e, 0)/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promediod
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promediod

El problema resuelto con el método sum, es:

Object.defineProperty(SV.prototype, "promedioe", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); return x.sum()/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promedioe
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promedioe

Igualmente, el problema puede ser resuelto con el método map:

Object.defineProperty(SV.prototype, "promediof", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); s = 0; x.map(e=>s+=e); return s/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promediof
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promediof

Desde el punto de vista de la eficiencia, esta no es la mejor opción, porque map devuelve un nuevo array con el mismo número de elementos que el array original (con los valores intermedios de la sumatoria), sin embargo, en este ejemplo, ese array no es empleado y por esa razón esta solución es menos eficiente (se pierde el tiempo en generar un array que, finalmente, no se emplea).

También se puede resolver el problema con el método every:

Object.defineProperty(SV.prototype, "promediog", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); s = 0; x.every(e=>(s+=e,true)); return s/n; }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promediog
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promediog

Como el método every termina de iterar cuando uno de los resultados es falso y en este caso es necesario que se itere con todos los elementos del array, la función debe devolver, siempre, un valor verdadero (true, 1 o cualquier valor equivalente a verdadero), porque de no ser así, un resultado equivalente a falso (como 0, cuando alguno de los elementos del array es 0) haría que las iteraciones terminen, devolviendo ub resultado erróneo.

Note que, en este ejemplo, para no emplear el comando return, las dos instrucciones de la función flecha (s+=e y true) han sido encerradas entre paréntesis y separadas con una coma, tal como se ha hecho en capítulos previos con el operador ternario ?-:, siendo el resultado el valor devuelto por la última expresión. Así, en este ejemplo, como la última expresión es el valor booleano true ese es el valor que devuelve (siempre) las insrucciones entre los paréntesis.

Igualmente, se puede emplear el método some (que es el complemento de every):

En este caso, como some termina cuando el resultado es verdadero y se quiere iterar para todos los elementos del array, el resultado de la función debe ser siempre falso (false, 0 o cualquier valor equivalente a falso).

De forma similar se pueden emplear los métodos find, findIndex, findLast, findLastIndex y filter. Sin embargo, es necesario dejar en claro que ninguno de estos métodos es adecuado para resolver este tipo de problemas, porque su propósito es otro: Verificar si todos los elementos cumplen una determinada condición (every), si alguno de los elementos cumple una determinada condición (some), si un valor dado es uno de los elementos del array (find y findLast), encontrar la posición de un determinado elemento en el array (findIndex y findLastIndex) y obtener un array con los elementos que cumplen una determinada condición (filter).

En la asignatura se emplean estos métodos (sabienod que no son adecuados en estos casos), únicamente para comprender mejor la forma en que operan (su lógica), pero no deben ser empleados (para estos casos) en la solución de un problema real.

Finalmente, el problema puede ser resuelto también empleando una función recursiva inmediatamente invocada (una IIFE):

Object.defineProperty(SV.prototype, "promedioj", { get() { const x = this.x; const n = x.length; if (n===0) throw new Error("vector vacío"); s = 0; x.some(e=>(s+=e,false)); return (s=i=>i===0?x[i]:s(i-1)+x[i])(n-1)/n }, configurable: true });
new SV(1, 9, 12, 3, 5, 7, 12).promedioj
randseed = 1; b = [].rand(20, 1, 30).round(); print(b); new SV(...b).promedioj

Vector de números aleatorios

En este ejemplo se elabora un método que genera un vector con "n" números enteros aleatorios comprendidos entre 1 y 1000.

El problema puede ser resuelto fácilmente con el método rand: simplemente se generan "n" números aleatorios comprendidos entre 1 y 1000 y se redondea el vector resultante, tal como se muestra en la siguiente función:

function rand1000(n) { if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); return [].rand(n, 1, 1000).round(); };

Llamando al método con algunos valores de prueba, se obtiene:

randseed = 10;
rand1000(5)
rand1000(10)
rand1000(20)
rand1000(25)
rand1000(-10)
rand1000(5.7)

Si embargo, si no se tiene acceso al método rand, porque no se está trabajando en la calculadora o no se ha importado la librería array.js, el problema puede ser resuelto empleando la estructura for y la función estándar random.

Para transformar el número aleatorio comprendido entre 0 y 1 (que es el resultado devuelto por random) en un número comprendido entre "li" y "ls", se aplica la siguiente expresión:

\[ \begin{aligned} \text{Nú}&\text{mero aleatorio comprendido entre li y ls}=\\ &(\text{Número aleatorio comprendido entre 0 y 1})\times (\text{ls}-\text{li})+\text{li} \end{aligned} \]

Entonces, en cada repetición del ciclo for, que va desde 0 hasta "n-1", se genera y guarda un número aleatorio, en este ejemplo implementado como un método getter de la clase "RAND", probando el método con algunos valores de prueba:

RAND = class { #n; constructor(número) { this.#n = número; } get rand1000() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v= []; for (let i=0; i<n; i++) v[i] = (Math.random()*999+1).round(); return v; } get n() { return this.#n; } };
randseed = 10;
new RAND(5).rand1000
new RAND(10).rand1000
new RAND(20).rand1000
new RAND(25).rand1000

La solución, implementada empleando el método push (en lugar de una asignación directa), es:

Object.defineProperty(RAND.prototype, "rand1000_", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v = []; for (let i=0; i<n; i++) v.push((Math.random()*999+1).round()); return v; }, configurable: true });
randseed = 10;
new RAND(5).rand1000_

El problema puede ser resuelto, igualmente, con el método estático Array.from:

Object.defineProperty(RAND.prototype, "rand1000__", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); return Array.from({length:n}, ()=>(Math.random()*999+1).round()); }, configurable: true });
randseed = 10;
new RAND(5).rand1000__

El problema, resuelto con la estructura for-of, es:

Object.defineProperty(RAND.prototype, "rand1000a", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v = []; for (const _ of [].zeros(n)) v.push((Math.random()*999+1).round()); return v; }, configurable: true });
randseed = 10;
new RAND(5).rand1000a

Como el método for-of itera para cada uno de los elementos de un array y en este caso, ese array no existe, es necesario crear uno con ese número de elementos (en este ejemplo se crea con zeros). Luego se itera para cada uno de los elementos de ese array, pero no se emplea ninguno de esos elementos y por ello son recibidos en el parámetro "_" (que como se explicó, suele emplearse para identificar parámetros/variables, que son recibidos/declarados, pero que no son utilizados). Debido a que for-of, requiere la creación de un array adicional (cuyos elementos no se utilizan), no es la estructura más adecuada para resolver este problema, porque el crear ese array consume tiempo y memoria adicional.

Los "n" elementos del array pueden ser generados con cualquiera de los métodos disponibles: const, range, rand, Array.from, new Array, etc., sin que ello afecte ni la lógica ni el resultado obtenido (porque únicamente es importante que el array tenga el número correcto de elementos).

El problema puede ser resuelto también con el método forEach:

Object.defineProperty(RAND.prototype, "rand1000b", { get() { const n = this.n; if (n%1!==0 || n<0) throw "El número debe ser entero positivo"; const v = [].zeros(n); v.forEach((_,i,v)=>v[i]=(Math.random()*999+1).round()); return v; }, configurable: true });
randseed = 10;
new RAND(5).rand1000b

Al igual que en la solución anterior se genera un array con "n" elementos (que tampoco son utilizados). Luego, con forEach, se itera para cada uno de esos elementos, pero ahora se recibe el índice "i", de cada elemento, para reemplazar ese elemento (v[i]) con el número aleatorio generado. Esta solución es un poco más eficiente que la anterior, porque solo se genera un array, pero, al igual que en el ejemplo anterior, los elementos del array generado no son empleados.

El problema resuelto con el método reduce (y zeros para generar el array), es:

Object.defineProperty(RAND.prototype, "rand1000c", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); return [].zeros(n).reduce(v=>(v.push((Math.random()*999+1).round()),v),[]); }, configurable: true });
randseed = 10;
new RAND(5).rand1000c

Como no se emplean los elementos del array, en la función solo se recibe el primer parámetro (el acumulador "v") que se convertirá en el array con los números aleatorios. Por esa razón, el acumulador "v" inicia como un vector vacío ([]), luego, en cada iteración, se le añada un número aleatorio y se devuelve el array con el nuevo elemento añadido. De esa manera, en la última iteración, reduce devuelve el array con todos los números aleatorios generados.

El problema puede ser resuelto, también, con el método some, aunque, como se explicó previamente, no es la opción correcta para este tipo de problemas:

Object.defineProperty(RAND.prototype, "rand1000d", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v = []; [].zeros(n).some(()=>(v.push((Math.random()*999+1).round()),false)); return v; }, configurable: true });
randseed = 10;
new RAND(5).rand1000d

Como ocurre con for=of, no se emplean los elementos del array generado (zeros), por lo que la función no recibe nada y para garantizar que el método itere para todos los elementos, la función devuelve siempre un valor false.

De la misma forma, se puede emplear el método every:

Object.defineProperty(RAND.prototype, "rand1000e", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v = []; [].range(n).every(()=>(v.push((Math.random()*999+1).round()),true)); return v; }, configurable: true });
randseed = 10;
new RAND(5).rand1000e

Que es prácticamente la misma solución que con some, sólo que ahora la función devuelve siempre true y el array se genera con range.

El problema puede ser resuelto, igualmente, con el método map.

Object.defineProperty(RAND.prototype, "rand1000f", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); return Array.from({length:n}).map(()=>(Math.random()*999+1).round()); }, configurable: true });
randseed = 10;
new RAND(5).rand1000f

Esta solución es similar a la lograda con reduce, en el sentido de que si bien no se emplean los elementos del array generado (el array de ceros), sí se emplea el array para reemplazar esos ceros con los valores aleatorios generados en la función.

Como en casi todos los casos, el problema puede ser resuelto también con find, findIndex, findLast, findLastIndex y filter, aunque, como también se dijo, no son los métodos más adecuados para resolver este tipo de problemas.

El problema puede ser resuelto también con una función recursiva inmediatamente invocada (una IIFE):

Object.defineProperty(RAND.prototype, "rand1000g", { get() { const n = this.n; if (!(Number.isInteger(n) && n>0)) throw new Error("El número debe ser entero positivo"); const v = []; return (a=n=>n===0?v:(v.push((Math.random()*999+1).round()),a(n-1)))(n); }, configurable: true });
randseed = 10;
new RAND(5).rand1000g

Que tampoco es la herramienta más adecuada para resolver el problema, pues es empleada únicamente para lograr el número de repeteciones requeridas. El resultado de la última llamada recursiva (cuando se cumple la condición base) simplemente se devuelve hasta llegar a la primera llamada recursiva.