19. Representar vectores con objetos y arreglos en JavaScript

Elegir una estructura de datos clara permite conservar el significado físico, implementar operaciones confiables y adaptar el rendimiento a cada aplicación.

19.1 Del concepto al dato

La matemática define qué es un vector y cómo se opera. El programa necesita además decidir cómo almacenarlo. En JavaScript podemos usar objetos, arreglos, arreglos tipados o instancias de una clase.

Ninguna representación es universalmente superior. La decisión depende de la claridad requerida, la cantidad de datos, las bibliotecas utilizadas y el costo de las operaciones.

Una estructura no aporta significado por sí sola: debemos documentar unidad, referencia, dimensión y si las funciones modifican o crean valores.

19.2 Objetos con propiedades nombradas

La forma más legible para comenzar es un objeto:

const desplazamiento = { x: 3, y: -2 };
const modulo = Math.hypot(desplazamiento.x, desplazamiento.y);
console.log(desplazamiento, modulo);

x e y expresan la intención y reducen errores de índice. Podemos agregar metadatos, aunque conviene no mezclar indiscriminadamente valores matemáticos y estado de la aplicación.

19.3 Fábricas y valores iniciales

Una función fábrica centraliza la creación y evita compartir accidentalmente el mismo objeto:

const vector2 = (x = 0, y = 0) => ({ x, y });

const posicion = vector2(4, 1);
const velocidad = vector2();
console.log(posicion, velocidad, posicion !== velocidad);

No debe reutilizarse un único objeto cero para estados que luego serán mutados.

19.4 Funciones puras e inmutabilidad

Una función pura devuelve un objeto nuevo y no altera los argumentos:

function sumar(a, b) {
  return { x: a.x + b.x, y: a.y + b.y };
}
function escalar(v, k) {
  return { x: v.x * k, y: v.y * k };
}

const a = { x: 2, y: 1 }, b = { x: -1, y: 4 };
console.log(sumar(a, b), escalar(a, 3), a);

Este enfoque facilita pruebas y razonamiento. Su costo es crear objetos nuevos, algo normalmente aceptable hasta medir que constituya un problema real.

19.5 Mutación controlada

En bucles intensivos puede actualizarse un objeto de salida para reducir asignaciones:

function sumarEn(out, a, b) {
  out.x = a.x + b.x;
  out.y = a.y + b.y;
  return out;
}

const resultado = { x: 0, y: 0 };
console.log(sumarEn(resultado, {x:2,y:3}, {x:4,y:-1}));

El nombre y la documentación deben advertir la mutación. No conviene optimizar de esta manera sin un perfil de rendimiento.

19.6 Arreglos numéricos

Un vector puede almacenarse como [x, y] o [x, y, z]. Esta forma facilita algoritmos genéricos, iteraciones y compatibilidad con APIs.

function sumarArreglos(a, b) {
  if (a.length !== b.length) throw new Error("Dimensiones diferentes");
  return a.map((valor, i) => valor + b[i]);
}

console.log(sumarArreglos([1, 2, 3], [4, -1, 2]));

Los índices son menos expresivos: debemos acordar que 0 es x, 1 es y y 2 es z.

19.7 Arreglos tipados

Float32Array y Float64Array almacenan números en memoria contigua con precisión definida. Son habituales al transferir datos a WebGL, audio, procesamiento numérico o grandes colecciones.

const vector = new Float32Array([1.25, -3.5, 2]);
vector[0] *= 2;
console.log(Array.from(vector), vector.byteLength);

Float32Array redondea a precisión simple. No posee todos los métodos mutables de un arreglo normal y su tamaño queda fijo.

19.8 Comparación de representaciones

Ventajas y compromisos habituales.
RepresentaciónVentajaCompromiso
{x, y}Legible y flexibleMás objetos y propiedades
[x, y]Compacta y genéricaÍndices menos claros
Float32ArrayInteroperabilidad y memoria predeciblePrecisión simple y tamaño fijo
Clase Vector2API agrupada y validableSemántica de mutación debe quedar clara

19.9 Una clase Vector2

Una clase reúne datos y operaciones. Aquí usamos métodos que devuelven instancias nuevas:

class Vector2 {
  constructor(x = 0, y = 0) { this.x = x; this.y = y; }
  sumar(v) { return new Vector2(this.x + v.x, this.y + v.y); }
  escalar(k) { return new Vector2(this.x * k, this.y * k); }
  modulo() { return Math.hypot(this.x, this.y); }
  normalizado() {
    const m = this.modulo();
    return m === 0 ? null : this.escalar(1 / m);
  }
}

const v = new Vector2(3, 4);
console.log(v.modulo(), v.normalizado());

19.10 Métodos estáticos y de instancia

Un método de instancia como v.modulo() opera sobre un vector. Un método estático como Vector2.distancia(a, b) combina valores sin pertenecer naturalmente a uno solo.

Una API consistente debe decidir si sumar muta, devuelve un valor nuevo o escribe en un parámetro de salida. Mezclar estilos sin nombres claros genera errores difíciles de detectar.

19.11 Validación

JavaScript permite construir objetos con valores faltantes, textos, infinitos o NaN. Una frontera de entrada debe validar:

function esVector2(v) {
  return v !== null
    && Number.isFinite(v.x)
    && Number.isFinite(v.y);
}

console.log(esVector2({x:3,y:4}), esVector2({x:"3",y:4}));

No es necesario validar en cada operación interna si la arquitectura garantiza valores correctos desde su entrada.

19.12 Unidades y referencias

Un objeto puede incluir unidad y referencia, pero las operaciones deben comprobar compatibilidad. Otra estrategia es encapsular esos metadatos en el estado físico que contiene los vectores.

const estado = {
  posicionM: { x: 2, y: 3 },
  velocidadMps: { x: 1, y: 0 },
  referencia: "mundo"
};

Los nombres con unidades reducen ambigüedad y evitan sumar, por ejemplo, metros con píxeles.

19.13 Conversión entre formatos

const objetoAArray = v => [v.x, v.y, ...(v.z === undefined ? [] : [v.z])];
const arrayAObjeto2 = a => ({ x: a[0], y: a[1] });

const objeto = { x: 2, y: -5 };
const arreglo = objetoAArray(objeto);
console.log(arreglo, arrayAObjeto2(arreglo));

Conviene convertir en los límites de la aplicación y mantener una representación interna principal.

19.14 Copias, referencias y JSON

Los objetos y arreglos se asignan por referencia. const b = a no copia el vector; ambos nombres apuntan al mismo objeto. Para un objeto plano usamos {...a} y para un arreglo [...a].

Los objetos y arreglos normales se serializan con JSON. Un Float32Array requiere convertirlo, por ejemplo con Array.from, si queremos un arreglo JSON convencional. NaN e infinitos no se conservan correctamente como números JSON.

19.15 Laboratorio de representaciones

Modificá el vector y elegí una precisión tipada. El laboratorio muestra simultáneamente objeto, arreglo, arreglo tipado y JSON.

Objeto
Arreglo
Arreglo tipado
JSON
Módulo

Todas las estructuras representan el mismo vector.

19.16 Rendimiento con criterio

La legibilidad suele ser prioritaria. Si una medición demuestra que las asignaciones o el diseño de memoria son un cuello de botella, podemos usar parámetros de salida, almacenamiento plano o arreglos tipados.

El rendimiento depende del motor, el volumen de datos y el patrón de acceso. Una microprueba aislada no reemplaza medir la aplicación real.

19.17 Errores frecuentes

  • Asignar un objeto creyendo que se creó una copia.
  • Mezclar métodos mutables e inmutables sin indicarlo.
  • Confundir índices de un arreglo.
  • Usar arreglos de distinta dimensión en la misma operación.
  • Perder precisión al convertir a Float32Array sin evaluarlo.
  • Omitir unidades y referencias.
  • Optimizar estructuras antes de medir.
  • Confiar en JSON para conservar NaN o infinitos.

19.18 Ejercicio integrador

Implementá una clase inmutable Vector3 con suma, escalado, producto escalar, módulo y normalización. Debe rechazar componentes no finitas y tratar el vector nulo.

Ver solución y código
class Vector3 {
  constructor(x=0,y=0,z=0) {
    if (![x,y,z].every(Number.isFinite)) throw new TypeError("Componentes inválidas");
    this.x=x; this.y=y; this.z=z;
    Object.freeze(this);
  }
  sumar(v) { return new Vector3(this.x+v.x,this.y+v.y,this.z+v.z); }
  escalar(k) { return new Vector3(this.x*k,this.y*k,this.z*k); }
  punto(v) { return this.x*v.x+this.y*v.y+this.z*v.z; }
  modulo() { return Math.hypot(this.x,this.y,this.z); }
  normalizado() { const m=this.modulo(); return m===0?null:this.escalar(1/m); }
}
const a=new Vector3(2,-1,2), b=new Vector3(1,3,0);
console.log(a.sumar(b),a.punto(b),a.modulo(),a.normalizado());

19.19 Ideas para recordar

  • Los objetos priorizan legibilidad; los arreglos facilitan algoritmos genéricos.
  • Los arreglos tipados ofrecen memoria y precisión explícitas.
  • Una clase puede reunir invariantes y operaciones.
  • La API debe declarar claramente si muta o crea valores.
  • Objetos y arreglos se asignan por referencia.
  • Unidades, dimensiones y sistemas de referencia forman parte del diseño.
  • Las optimizaciones deben responder a mediciones.

En el próximo tema construiremos una biblioteca reutilizable de operaciones vectoriales.