Volver al Centro de conocimiento
Huella digital

Orden de array y comparadores: una función fácil de errar

Ordena arrays fijos: el orden de cadena por defecto, la mutación en sitio, el orden estable, dónde caen undefined y NaN, un comparador malo y la resta que desborda.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Huella digital?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

BotBrowser soporta repetir un fixture autorizado del mismo origen bajo un perfil declarado y conservar observaciones acotadas sobre el orden en que una página ordenó un array, pero no puede cambiar cómo un entorno implementa el algoritmo de orden, no puede hacer consistente un comparador que no lo es y no convierte un resultado de orden en una señal de identidad. Esta revisión ordena arrays fijos con el comparador por defecto y con comparadores explícitos, y luego registra por qué el orden por defecto es orden de cadena, por qué el orden muta en sitio, cómo un orden estable conserva las claves iguales en orden de inserción, dónde caen undefined y NaN, por qué un comparador inconsistente da un orden erróneo y silencioso, y por qué un comparador de resta desborda con números grandes.

Resumen

  • El orden por defecto convierte cada elemento en cadena y ordena por unidades de código UTF-16, así que [10,9,1,100].sort() da [1,10,100,9], no orden numérico.
  • sort muta el array en sitio y devuelve la misma referencia, así que quien espera una copia cambia la fuente en silencio.
  • Un comparador numérico (a, b) => a - b da [1,9,10,100], el orden que un lector espera, pero un comparador de resta desborda cuando la diferencia supera el rango de doble precisión.
  • El orden es estable, así que las filas que comparan igual mantienen su orden de inserción, que es bdac para dos claves repartidas 2,1,2,1.
  • undefined y los huecos se ordenan al final; un comparador que devuelve NaN se trata como 0 para ese par, así que el elemento conserva su posición relativa.
  • Un comparador que no es consistente (no transitivo) puede producir un orden erróneo sin lanzar, porque el algoritmo asume que el comparador es un orden total.
  • toSorted devuelve una copia y deja la fuente intacta, que es la forma de ordenar sin mutar.

Contenido

Un array de 10, 9, 1, 100 entra al orden por defecto y sale como 1, 10, 100, 9 porque cada elemento se vuelve cadena, mientras una columna de comparador numérico sale como 1, 9, 10, 100; un panel STABLE conserva dos grupos de claves iguales en orden de inserción, y una fila de notas marca undefined al final, NaN en su sitio y un comparador de resta que desborda pasado el rango 2 a 53

Por qué el orden por defecto es orden de cadena

Array.prototype.sort sin comparador no ordena números como números. Convierte cada elemento en cadena y ordena las cadenas por unidad de código UTF-16. La sonda ordena los mismos cuatro números de ambas formas:

Array.sort=function node=v26.9.0
[10,9,1,100].sort() -> [1,10,100,9]
[10,9,1,100].sort((a,b)=>a-b) -> [1,9,10,100]

El orden por defecto devuelve 1, 10, 100, 9, porque como cadenas "1" va antes que "10" va antes que "100" va antes que "9" por unidad de código: el primer carácter de cada una decide, y "9" es mayor que "1" así que los cuatro números que terminan en algo distinto de 9 van primero. La forma con comparador devuelve 1, 9, 10, 100, el orden numérico que un lector espera. Las dos llamadas ordenan el mismo array y discrepan por completo, y solo una coincide con la intuición de "ordenado".

Ese hueco es el defecto de orden más común: un desarrollador llama a sort() sobre un array de números, ve un resultado que parece plausible y nunca nota que el orden es lexicográfico. Se ve bien para un dígito y cadenas de un dígito, y se rompe en el momento en que aparece un valor de dos dígitos. La solución es siempre pasar un comparador para números, nunca confiar en el por defecto, porque el por defecto está definido para cadenas y solo casualmente se ve correcto en una entrada pequeña.

El orden de cadena también explica por qué el por defecto trata los tipos mixtos por su forma de cadena. La sonda ordena un array mixto de un booleano, un número y una cadena juntos, y el orden sigue la forma String() de cada elemento y no ninguna regla por tipo. Quien asume que el orden por defecto es "orden natural" obtiene orden de unidad de código, que para letras ASCII pone mayúscula antes que minúscula y para letras acentuadas las coloca después de toda letra ASCII.

La sonda hace explícito el orden de unidad de código con una entrada mixta de cadenas y booleanos:

["b","a","C","á"].sort() -> ["C","a","b","á"]
[true,false,2,1].sort() -> [1,2,false,true]

Las cuatro cadenas ordenan a "C", "a", "b", "á", porque la C mayúscula tiene una unidad de código menor que la a minúscula, y la á acentuada tiene una unidad de código mayor que cualquier letra ASCII simple, así que va al final. Quien espera orden alfabético pondría "á" junto a "a" y quizá no esperaría "C" antes que "a", pero el orden por defecto no es alfabético y nunca lo pretende. El array mixto de booleanos y números ordena a 1, 2, false, true, porque "1" y "2" como cadenas van antes que "false" y "true", cuyas primeras letras f y t tienen unidades de código mayores que los dígitos.

Ese comportamiento es por lo que un orden sobre etiquetas que vienen de entrada del usuario no puede usar el por defecto. Dos etiquetas que difieren solo en mayúsculas ordenan en un orden sensible a mayúsculas que un lector no espera, y una etiqueta que empieza con un dígito ordena antes que toda etiqueta que empieza con una letra. Las alternativas que conocen la locale, String.prototype.localeCompare e Intl.Collator, ordenan por reglas de idioma y no por unidades de código, así que una lista que un lector percibe como alfabética necesita una de ellas y no el por defecto. Un registro que guarda una lista ordenada por defecto de etiquetas ha guardado orden de unidad de código, que es reproducible pero no el orden que un lector llamaría ordenado.

El costo de equivocarse aquí es una lista que es estable y reproducible pero se lee como desordenada para toda persona que la mira. Un panel que ordena nombres de cuenta con el comparador por defecto pone toda cuenta con mayúscula inicial antes de toda cuenta con minúscula inicial, y pone los nombres acentuados después de todo el bloque ASCII, que no es el orden que el operador que escanea la lista espera. El orden nunca lanza, nunca cambia entre ejecuciones en el mismo motor, y falla en silencio contra la intención, que es la peor combinación porque nada en un test que solo comprueba "está ordenado" lo detectaría salvo que ese test codifique la misma regla errónea. Un test escrito contra el comparador por defecto pasa; un test escrito contra la expectativa del lector falla, y los dos discrepan en toda lista que contenga mayúsculas mezcladas o un nombre acentuado.

También hay una razón aritmética por la que el orden por defecto sorprende con números de valor cercano. Como el por defecto compara las formas de cadena carácter a carácter, "2" y "20" comparan en su primer carácter y empatan ahí antes de que el segundo decida, así que una lista de ids 2, 20, 3 ordena a 2, 20, 3 como cadenas pero un lector espera 2, 3, 20 como números. Cuanto más largo el prefijo compartido, más tarde la decisión y más diverge el orden de texto del orden numérico, por lo que el defecto se hace más visible a medida que los identificadores ganan dígitos en vez de achicarse.

El comparador por defecto también decide el orden de objetos y arrays dentro del array, que es un lugar al que un llamador rara vez mira. Todo objeto se convierte en cadena a "[object Object]", así que un array de objetos simples ordenado con el comparador por defecto se queda en su orden de inserción porque todo elemento compara igual. Un array de arrays convierte cada array interno a sus elementos unidos por coma, así que el orden externo ordena por el texto del contenido interno y no por longitud ni por ningún campo. Quien ordena una lista de registros con el comparador por defecto y espera un orden estable y significativo obtiene orden de inserción para objetos y un orden de texto para arrays anidados, y ninguno es el orden que tenía en mente. La lección es que el comparador por defecto solo tiene sentido para un array de primitivos cuyas formas de cadena ordenan como el llamador quiere, que es un caso estrecho que un array mixto o estructurado nunca satisface.

El comparador por defecto también tiene una forma de rendimiento que un llamador puede no esperar. Convertir cada elemento a cadena cuesta una asignación por elemento por comparación en el peor caso, y un orden que compara el mismo par más de una vez repite ese trabajo, así que un orden por defecto sobre un array numérico grande es más lento que un comparador que resta. La lentitud no es el defecto, pero es síntoma de la misma raíz: el comparador por defecto se escribió para comparar cadenas, y un array numérico empujado por él paga el costo de conversión a cadena en cada comparación por un orden que el llamador no quería. Un código que cambia a un comparador numérico obtiene tanto el orden correcto como menos trabajo, que es un caso raro donde la versión correcta también es la más rápida.

El orden de cadena del por defecto también explica por qué ordenar una lista de números escritos como cadenas parece correcto un rato. Una lista de cadenas de un dígito "1" a "9" ordena al orden numérico, porque cada cadena es un carácter y el orden de unidad de código coincide con el orden numérico para un dígito. El defecto solo aparece cuando un valor tiene dos dígitos, porque entonces "10" ordena antes que "2" en el primer carácter. Un fixture construido con valores de un dígito por eso pasa con el orden por defecto, y el mismo código falla en datos de producción que contienen un valor de diez o más. El hueco entre un fixture que pasa y un caso de producción que falla es por completo sobre cuántos dígitos tienen los valores, algo que el orden nunca reporta.

Por qué el orden muta en sitio

sort no es una función pura de su entrada: reordena el array sobre el que se llama y devuelve ese mismo array. La sonda ordena un array de tres elementos y comprueba si el valor devuelto es el mismo objeto que la fuente:

same reference -> true; array now [1,2,3]

El array devuelto es la misma referencia que la entrada (result === arr es true), y el array fuente es ahora 1, 2, 3. Quien escribe const sorted = arr.sort() y luego usa arr en otro lugar ha reordenado arr en silencio, porque sorted y arr son el mismo objeto. Es un defecto real en un código que mantiene el orden original para mostrar y también quiere una copia ordenada, porque el orden destruyó el orden original en sitio.

La forma de ordenar sin mutar es toSorted, que devuelve un array nuevo y deja la fuente intacta. La sonda confirma la diferencia: tras toSorted la fuente sigue siendo 3, 1, 2 y el array devuelto es 1, 2, 3, y los dos son referencias distintas. Quien quiere tanto el orden original como una vista ordenada necesita toSorted o un slice() explícito antes de sort, no sort sobre el original.

Esa mutación es también por lo que un orden dentro de un render es peligroso. Si un componente ordena un array de estado compartido para mostrarlo, el orden muta el estado en cada render, y una lectura posterior del estado ve el orden ordenado en vez del orden en que se cargó. La solución es la misma: ordena una copia, y registra qué array tocó el orden para que un lector sepa si el orden observado es el orden guardado o un orden transitorio.

El ejemplo de toSorted de arriba ya muestra la alternativa segura, y se paga sola siempre que el orden original importa. Quien necesita tanto el orden cargado como una vista ordenada usa toSorted, o un slice() seguido de sort, y la fuente queda como se cargó. El costo es una asignación, insignificante frente al costo de un array de estado que muta bajo un componente que asumía que era dueño del orden.

Hay una trampa relacionada en un comparador que ordena por un campo que puede faltar. Un comparador que lee a.key - b.key sobre un elemento cuya key es undefined da NaN, que el orden trata como igual, así que el elemento conserva su posición relativa en vez de moverse a una ranura definida. El resultado es un orden donde las filas con campo faltante quedan intercaladas con las filas contra las que se compararon, que se ve ordenado de un vistazo y no lo está. La solución es decidir dónde pertenece un campo faltante, ordenarlo al final con una rama explícita, y registrar esa decisión, porque el orden por sí mismo no lo moverá.

Esa mutación en sitio es también lo que hace poco fiable un orden compuesto con otra mutación. Si un llamador ordena un array y luego hace un splice de un segundo, y los dos comparten elementos por referencia, el splice lee el orden que el orden dejó, que puede no ser el orden que el llamador asumía que el array compartido todavía tenía. Ordenar en sitio es una escritura en memoria compartida siempre que el objeto array se comparte, y un registro que no dice si el array se compartió, se clonó o se construyó nuevo no puede saber si una lectura posterior vio el orden ordenado o el orden antes de que otro código lo reiniciara.

La identidad de referencia es también por lo que una comprobación de igualdad tras un orden puede inducir a error. Quien compara un array ordenado consigo mismo para probar "nada cambió" los encuentra iguales, porque el orden devolvió el mismo objeto, aunque el contenido cambió. Un test que quiere probar que el orden dejó la fuente intacta tiene que comparar contra una copia guardada antes del orden, no contra la misma referencia, porque la igualdad de referencia vale haya o no mutado el orden. La variante toSorted quita la ambigüedad al devolver un objeto distinto, así que una comprobación de referencia distingue "volvió una copia" de "se reescribió la fuente".

La mutación en sitio también cambia el significado de un nombre de variable que promete orden. Una constante llamada insertionOrder que se pasa a sort y luego se lee ya no guarda orden de inserción, porque el orden reescribió el array detrás del nombre. El nombre miente sobre el contenido, y un lector que confía en el nombre lee un orden ordenado donde el código pretendía un orden de llegada. Como la mutación es invisible en el sitio de llamada cuando se ignora el valor devuelto, nada en el código que lee el nombre revela que el orden cambió, y un registro que guarda el array bajo ese nombre guarda el orden ordenado bajo una etiqueta que afirma lo contrario.

El patrón más seguro es ordenar solo dentro de la función que consume el resultado ordenado y pasar un clon a esa función cuando la fuente importa en otro lugar. Una función que toma un array y devuelve su orden ordenado debería documentar que ordena en sitio o devolver toSorted, y un llamador debería decidir una vez si el array fuente puede cambiar. Un código que mezcla ambos, ordenando en sitio en un lugar y dependiendo del orden original en otro, produce un defecto que depende del orden de llamadas, que es el tipo más difícil de reproducir porque solo falla cuando los dos llamadores corren en una secuencia particular.

La mutación también interactúa con una copia superficial de una forma que sorprende a quien cree que una copia basta. Un slice() copia el array de referencias pero no los elementos, así que ordenar la copia por un campo reordena las referencias sin mutar los elementos, y el array original mantiene su orden de las mismas referencias. Eso es seguro para el orden del array, pero si el orden luego escribe en los elementos para registrar la posición ordenada, la escritura alcanza los elementos compartidos y los elementos del array original cambian aunque su orden no. La distinción entre "el orden del array cambió" y "los elementos cambiaron" es exactamente la distinción que un registro necesita, y ni un sort ni un slice solo declaran cuál pasó.

Un array congelado es el único caso donde la mutación en sitio falla ruidosamente. Ordenar un array con Object.freeze lanza en modo estricto en vez de reordenar en silencio, que es la única forma en que la mutación se anuncia como escritura. Un código que quiere garantizar que nunca muta un array compartido puede congelarlo en desarrollo, así un sort accidental lanza en vez de dejar un reordenamiento sutil, y luego usar toSorted donde de verdad se necesita un orden. La congelación es una forma barata de convertir la mutación silenciosa en ruidosa, el mismo trato que un registro hace cuando guarda si la fuente podía cambiar.

Dónde un orden estable conserva las claves iguales

El orden es estable: los elementos que comparan igual mantienen su orden de inserción relativo. La sonda ordena cuatro filas con dos claves distintas y comprueba el orden de las filas de igual clave:

stable sort -> bdac

Las filas de entrada son a (clave 2), b (clave 1), c (clave 2) y d (clave 1). Tras ordenar por clave ascendente, el orden es b, d, a, c: las dos filas de clave 1 mantienen b antes que d, y las dos de clave 2 mantienen a antes que c. Cada grupo de igual clave preservó el orden en que se insertó, que es lo que significa "estable" y lo que permite a un llamador ordenar por clave primaria, luego por clave secundaria, y obtener el orden compuesto esperado.

La estabilidad no siempre estuvo garantizada; motores viejos usaban algoritmos inestables y un orden secundario podía desordenar el primero. La especificación ahora exige un orden estable, así que un código que depende de ello es correcto en todo motor actual. La consecuencia práctica es que un flujo de "ordenar por columna, luego por otra columna" es seguro solo porque el orden es estable, y un registro que guarda el resultado de un solo orden no puede distinguir un resultado estable de uno inestable sin guardar también el orden previo de las filas de igual clave.

El orden estable también es lo que hace innecesario un comparador multi-clave en muchos casos. En vez de un comparador que compara clave A y luego clave B, un llamador puede ordenar por B primero y A segundo, confiando en la estabilidad para mantener el orden B dentro de valores A iguales. Ambos enfoques dan el mismo orden cuando el comparador es consistente, y la versión de dos pasadas es más fácil de leer pero solo correcta porque el orden es estable.

La garantía de estabilidad también decide qué puede prometer una tabla de "ordenar por columna" tras un segundo clic. Una tabla que primero ordena por una columna y luego por otra depende de la estabilidad para mantener el primer orden dentro del segundo, y si el motor no fuera estable el segundo clic dispersaría el primer orden y la tabla mostraría un orden sin sentido que ningún encabezado de columna describe. Como la especificación ahora exige estabilidad, el orden compuesto vale en todo motor actual, pero un registro que guarda solo el orden final y no la secuencia de órdenes no puede probar que el orden compuesto se derivó como la tabla pretende.

Hay una consecuencia más sutil para un comparador que es estable pero no consistente. Un orden estable preserva el orden de los elementos que el comparador llama iguales, así que un comparador que llama erróneamente iguales a dos elementos desiguales los mantiene en el orden en que llegaron, que puede verse plausible y estar mal. La estabilidad hace que el orden erróneo parezca intencional, porque un orden estable nunca desordena elementos iguales, así que el defecto se esconde detrás misma garantía que hace funcionar los órdenes multi-clave. Un registro que captura el orden previo de los elementos que el comparador podría empatar es la única forma de distinguir un orden compuesto correcto de un orden estable sobre un comparador roto.

La estabilidad también decide si un orden sobre datos ya ordenados es un no-op. Un orden de dos pasadas por A luego B, corrido sobre datos que ya están en orden A, deja el orden A intacto dentro de valores B iguales, así que repetir las mismas dos pasadas no cambia nada. Un único comparador que lee solo B, corrido sobre los mismos datos, reordena las filas por B y arrastra los empates de A al orden B, que es un resultado distinto del de dos pasadas aunque ambos están "ordenados por B". Cuál es correcto depende de si el llamador quería una clave secundaria o no, y el orden estable es lo que hace que la versión de dos pasadas signifique lo que parece.

Para un registro, el orden previo de claves iguales es el campo que convierte un resultado estable en uno reproducible. El array final ordenado solo no dice cuál de dos elementos de igual clave vino primero, porque ambos órdenes son resultados válidos de un orden estable sobre la misma entrada según el orden en que llegaron. Solo el orden previo más el comparador más la garantía de estabilidad fijan el resultado, y un registro que guarda el array final sin el orden previo ha guardado uno de varios resultados válidos y lo llamó el resultado.

La estabilidad también importa para una deduplicación que sigue a un orden. Un código que ordena un array y luego remueve duplicados adyacentes para quedarse con el "primero" de cada corrida depende de la estabilidad para mantener al frente de cada corrida el elemento que llegó primero. En un motor estable el primer elemento que llegó gana la deduplicación, y en un motor inestable podría ganar otro, lo que cambia qué valor sobrevive. Como el orden ahora es estable, el primer elemento que llegó siempre sobrevive, y un registro que guarda el resultado deduplicado sin el orden previo no puede decir qué elemento se quedó ni por qué.

El orden compuesto de dos pasadas tiene un costo que un único comparador esconde. Ordenar dos veces corre el algoritmo dos veces, y la segunda pasada reordena todo el array por la clave primaria mientras la estabilidad preserva el orden secundario dentro de los empates. Un único comparador que compara primaria y luego secundaria hace el trabajo en una pasada y da el mismo resultado, pero tiene que manejar la comparación secundaria él mismo y mantenerse consistente en los tres campos que ahora compara. Cualquiera que sea el enfoque que un código elija, el registro tiene que decir cuál usó, porque un resultado de dos pasadas y uno de una pasada sobre los mismos datos coinciden solo cuando el comparador es un orden total consistente sobre la clave compuesta.

La estabilidad es también lo que hace seguro un orden paginado. Un servidor que devuelve la página dos de una lista ordenada depende de un orden estable para que las filas de la página dos no repitan una fila de la página uno cuando dos filas empatan en la clave de orden. Sin estabilidad el orden de empate es arbitrario por página, y una fila puede aparecer en ambas páginas o en ninguna, que es un bug clásico de paginación que ningún error reporta. El orden estable hace determinista el orden de empate dentro de una pasada, pero entre dos peticiones separadas el orden de empate depende del orden en que el servidor leyó las filas, por lo que un orden estable del cliente no puede rescatar una página que el servidor construyó con un orden inestable. El registro tiene que capturar qué lado ordenó y si ese lado fue estable.

Dónde caen undefined y NaN

undefined, los huecos de array y NaN no se comportan como números ordinarios bajo un orden, y la sonda muestra a dónde va cada uno:

[3,undefined,1].sort() -> [1,3,null]
[3,undefined,1].sort((a,b)=>a-b) -> [1,3,null]
[1,null,3].sort((a,b)=>a-b) -> [null,1,3]
[3,NaN,1].sort((a,b)=>a-b) -> [3,null,1]

El null impreso en las dos primeras filas es la vista del serializador: JSON.stringify convierte una ranura de array que guarda undefined (tras un orden que la movió al final) en null, porque un array no puede guardar undefined en JSON. El valor guardado es undefined al final, y el orden lo movió ahí porque el orden por defecto trata undefined como mayor que todo elemento definido y siempre lo ordena al final. La forma con comparador hace lo mismo, ya que la especificación manda undefined al final sea cual sea el comparador.

La fila de null es un valor genuino: null compara como 0 bajo a - b, así que [1, null, 3] ordena a null, 1, 3, con null al frente como si fuera cero. La fila de NaN es la más aguda: NaN comparado con cualquier otro valor devuelve NaN, que el orden trata como 0, así que NaN no se mueve respecto a los elementos con los que se comparó y el array mantiene su orden 3, NaN, 1. Un comparador que devuelve NaN para todo un orden se trata como "igual" para todo par, así que el orden se conserva.

Esos dos comportamientos son por lo que un registro ordenado debe nombrar si guarda undefined, huecos o NaN. Una lista ordenada de números que en silencio contiene un NaN no ordena al orden numérico que un lector espera, porque el NaN se queda donde estaba y el resto ordena a su alrededor. Un registro que guarda solo el array impreso, con el NaN ya convertido a null por el serializador, no se puede volver a correr al mismo orden sin saber que el NaN estaba ahí.

La fila de NaN también cambia cómo un orden se compone con un filtro. Un código que ordena, luego filtra el elemento del frente, depende de que el orden ponga un elemento conocido al frente; si el array contiene un NaN que se quedó en su sitio, el elemento del frente no es el mínimo, y el filtro remueve el elemento equivocado. El NaN se queda donde se insertó y el orden lo deja ahí, así que el frente del array ordenado es el menor de los elementos que el orden de verdad movió, que puede ser un elemento distinto del mínimo del array entero. Un registro que anota la presencia de un NaN es lo que permite a un lector saber que el elemento del frente no es necesariamente el mínimo, y un código que filtra antes de ordenar evita la trampa por completo.

Los casos de undefined, hueco y NaN comparten una lección para un fixture: prueba el array como llega de verdad, no como se ve tras un paso de limpieza. Un fixture que pre-ordena un array limpio pasa, y el mismo código sobre un array con un undefined al final o un NaN suelto se comporta distinto sin lanzar. El paso de limpieza esconde justo la entrada que el orden maneja en especial, y un registro que guarda el array limpio ha guardado la versión que el orden trata como ordinaria, no la versión que la ruta de producción le da.

La interacción entre undefined y el comparador merece un paso más, porque decide si un comparador sobre valores mixtos se comporta como el por defecto o no. Un comparador que suma o resta un valor definido y un undefined produce NaN, y la especificación manda undefined al final antes incluso de que se consulte el comparador, así que la aritmética nunca corre para ese par. Eso significa que un comparador que fallaría con undefined en un bucle simple es seguro dentro de un orden, porque el orden quita los elementos undefined primero. Quien pre-filtra los undefined del array antes de ordenar, entonces, ha hecho trabajo que el orden habría hecho igual, y quien los deja dentro obtiene un resultado definido y reproducible en vez de una excepción.

El valor null es el caso que no se comporta ni como undefined ni como un número que un lector esperaría. null se convierte a 0 bajo resta, así que un comparador lo ordena al frente numérico, y quien trata null como "faltante" y espera que vaya al final lo encuentra al principio. La distinción entre "faltante como undefined" y "faltante como null" por eso cambia el resultado del orden, y un registro que guarda solo el array impreso con ambos renderizados como null no puede decir qué representación usó la fuente. Quien vuelve a correr el orden sobre la forma guardada obtiene un orden distinto del original siempre que las dos representaciones se mezclaron, porque una ordenó al final y la otra al frente.

Por qué un comparador inconsistente da un orden erróneo

El algoritmo de orden asume que el comparador describe un orden total: si dice que a no es mayor que b y b no es mayor que c, entonces a no es mayor que c, y a compara igual consigo mismo. Un comparador que rompe esto puede producir un orden erróneo sin lanzar. La sonda ordena con un comparador que solo compara paridad y muestra tanto un caso benigno como uno roto:

sort(()=>0) keeps order -> [3,1,2,5,4]
non-transitive comparator -> [2,4,6,8,10,1,3,5,7,9,11]

sort(() => 0) dice que todo par es igual, así que el algoritmo deja el array como estaba: 3, 1, 2, 5, 4. Eso es inofensivo y predecible. El comparador no transitivo (a, b) => (a % 2) - (b % 2) dice que todo número par es menor que todo impar, pero no es consistente: un par, un impar y otro par pueden comparar en círculo. El resultado 2, 4, 6, 8, 10, 1, 3, 5, 7, 9, 11 pone todos los pares primero y los impares después, pero dentro del grupo impar el orden 1, 3, 5, 7, 9, 11 y dentro de los pares 2, 4, 6, 8, 10 resulta ascendente solo porque la entrada lo era; una entrada distinta puede dejar los pares desordenados, y nada lanza.

Un comparador que devuelve un valor mayor que la brecha de doble precisión entre los dos números tiene el mismo modo de fallo por otra causa. La sonda ordena tres valores alrededor de 2^53 con un comparador de resta:

big subtract sort -> ["-9007199254740992","1","9007199254740992"]

2^53 es el mayor entero por debajo del cual todo entero es representable con exactitud como doble, y un entero uno por encima no es representable, así que (2^53) - (2^53 + 1) no puede ser -1; se redondea a 0, y el comparador reporta dos números desiguales como iguales. Un comparador de resta sobre ese rango por eso da un orden arbitrario pero estable en vez del orden numérico, y el fallo es silencioso. La solución es escribir el comparador con ramas explícitas de menor-que y mayor-que que devuelvan -1, 1 o 0, en vez de restar.

El desbordamiento es silencioso del mismo modo que el orden de cadena es silencioso, y vale decir por qué los dos comparten una raíz. Ambos defectos vienen de confiar en una comparación barata de escribir y errónea en parte de su entrada: el comparador por defecto confía en la forma de cadena de un número, y un comparador de resta confía en que la diferencia cabe en un doble. Ambos producen un orden estable y sin lanzar que se ve ordenado para las entradas que un desarrollador prueba y diverge en las entradas que importan, así que ninguno lo detecta un test que solo comprueba que el código corrió. La solución para ambos es hacer la comparación explícita y total sobre el dominio real, y registrar el dominio para que un lector posterior sepa con qué entradas se validó el comparador.

El requisito de consistencia y el de desbordamiento se encuentran en el mismo lugar: un comparador tiene que definir un orden total sobre los valores que de verdad verá. Un comparador escrito para números que recibe un NaN, un null, un undefined o una cadena ya no es un orden total sobre su entrada, y el orden producirá un orden que un test de comparación estricta rechaza sin lanzar nada. El remedio es normalizar las entradas antes del orden, o escribir un comparador que maneje cada tipo explícitamente, y luego registrar qué tipos se manejaron, porque el orden por sí mismo nunca se queja de un comparador en el que no puede confiar.

Registra una lectura de orden

Una lectura de orden sostiene una afirmación estrecha: este entorno, este array, con este comparador, produjeron este orden. Seis campos la mantienen verificable, y ninguno asume que el array es numérico o el comparador consistente:

  1. El comparador, registrado como texto fuente o como la opción "por defecto", porque el orden por defecto ordena por cadena mientras cualquier comparador explícito define su propio orden.
  2. Si el orden mutó el array fuente o devolvió una copia, porque sort muta en sitio y toSorted devuelve una copia, y un orden guardado sin esto no se puede atribuir al array correcto.
  3. El orden previo de cualquier grupo de igual clave, porque un orden estable lo preserva y el resultado guardado solo no muestra qué elemento igual vino primero.
  4. Si el array guardaba undefined, huecos o NaN, porque se ordenan al final o se quedan en su sitio en vez de seguir el orden numérico, y un serializador los convierte en null.
  5. Si el comparador es un orden total consistente, porque un comparador no transitivo o que desborda produce un orden erróneo sin lanzar.
  6. Los tipos de elemento, porque un array mixto ordena por la forma de cadena de cada elemento bajo el comparador por defecto, no por ningún orden numérico compartido.

Lo que tal lectura no puede mostrar es nada sobre cómo un servidor u otro entorno ordenará los mismos datos. Una página ordena su propio array, pero un servidor que devuelve una lista pre-ordenada puede haber usado un comparador distinto, y un orden estable en el cliente no puede recuperar un orden que el servidor no envió. Para el orden de claves que un serializador da a un objeto, Orden de claves muestra por qué las claves enteras ordenan al frente mientras las de cadena mantienen el orden de inserción, y para el contenedor que una copia conserva, structuredClone frente a JSON muestra qué tipos sobreviven a una copia profunda y qué colapsa un viaje por JSON.

Dos hábitos mantienen la lectura honesta con el tiempo. Primero, pasa siempre un comparador explícito para números y escríbelo con ramas de menor-que y mayor-que en vez de resta, así una brecha grande no se redondea a igual. Segundo, registra si el orden mutó la fuente y cuál era el orden de claves iguales antes del orden, así un orden guardado es atribuible y un resultado estable se distingue de uno afortunado.

Fuentes públicas

Conclusión

Trata el comparador como parte del contrato de datos: el orden por defecto ordena por cadena y convierte [10,9,1,100] en [1,10,100,9], así que pasa siempre un comparador numérico explícito, escrito con ramas de menor-que y mayor-que en vez de resta para que una brecha pasada 2^53 no se redondee a igual. Sabe que sort muta en sitio y toSorted devuelve una copia, que el orden es estable así que las claves iguales mantienen el orden de inserción, que undefined y los huecos se ordenan al final mientras un NaN se queda en su sitio, y que un comparador que no es un orden total consistente da un resultado erróneo y silencioso. Registra el comparador, si la fuente mutó, el orden previo de claves iguales y cualquier undefined, hueco o NaN, así un orden guardado es atribuible y un resultado estable se distingue de uno afortunado.

Lectura siguiente

Para el orden de claves que un serializador da a un objeto, lee Orden de claves; para qué tipos de contenedor sobreviven a una copia profunda antes de que corra cualquier orden, lee structuredClone frente a JSON.

#Array.sort#Comparador#Orden Estable#NaN#Desbordamiento De Enteros

Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?

Lleva BotBrowser de la investigación a producción

Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.