El primero será el último con matrices JavaScript

Así será el último [0], y el primero [longitud - 1] .– Adaptado de Mateo 20:16

Saltaré la catástrofe de Malthus y llegaré a ella: las matrices son una de las estructuras de datos más simples e importantes. Si bien se accede con frecuencia a los elementos terminales (primero y último), Javascript no proporciona una propiedad o método conveniente para hacerlo y el uso de índices puede ser redundante y propenso a efectos secundarios y errores uno por uno.

Una propuesta reciente de JavaScript TC39, menos conocida, ofrece consuelo en forma de dos “nuevas” propiedades: Array.lastItem& Array.lastIndex.

Matrices de JavaScript

En muchos lenguajes de programación, incluido Javascript, las matrices tienen un índice cero. Se accede a los elementos terminales, primero y último, mediante los índices [0]y [length — 1], respectivamente. Debemos este placer a un precedente establecido por C, donde un índice representa un desplazamiento desde el principio de una matriz. Eso hace que cero sea el primer índice porque es el cabezal de la matriz. También Dijkstra proclamó "cero como el número más natural". Así que se escriba. Así que deja que se haga.

Sospecho que si promediara el acceso por índice, encontrará que los elementos terminales son referenciados con mayor frecuencia. Después de todo, las matrices se usan comúnmente para almacenar una colección ordenada y, al hacerlo, colocan elementos superlativos (más alto, más bajo, más antiguo, más nuevo, etc.) en los extremos.

A diferencia de otros lenguajes de programación (por ejemplo, PHP o Elixir), Javascript no proporciona un acceso conveniente a los elementos de la matriz de terminales. Considere un ejemplo trivial de intercambio de los últimos elementos en dos matrices:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals[animals.length - 1];animals[animals.length - 1] = faces[faces.length - 1];faces[faces.length - 1] = lastAnimal;

¡La lógica de intercambio requiere 2 matrices referenciadas 8 veces en 3 líneas! En el código del mundo real, esto puede volverse muy repetitivo y difícil de analizar para un humano (aunque es perfectamente legible para una máquina).

Además, utilizando únicamente índices, no puede definir una matriz y obtener el último elemento en la misma expresión. Puede que eso no parezca importante, pero considere otro ejemplo en el que la función getLogins(),, realiza una llamada a la API asincrónica y devuelve una matriz ordenada. Suponiendo que queremos el evento de inicio de sesión más reciente al final de la matriz:

let lastLogin = async () => { let logins = await getLogins(); return logins[logins.length - 1];};

A menos que la longitud es fija y conocida de antemano, que tenemos para asignar la matriz a una variable local para acceder al último elemento. Una forma común de abordar esto en lenguajes como Python y Ruby es usar índices negativos. Luego [length - 1]se puede acortar a [-1], eliminando la necesidad de una referencia local.

Encuentro -1solo un poco más legible que length — 1, y aunque es posible aproximar índices de matriz negativos en Javascript con ES6 Proxy o Array.slice(-1)[0], ambos tienen implicaciones de rendimiento significativas para lo que de otra manera debería constituir un simple acceso aleatorio.

Subrayado y Lodash

Uno de los principios más conocidos en el desarrollo de software es Don't Repeat Yourself (DRY). Dado que acceder a elementos terminales es tan común, ¿por qué no escribir una función auxiliar para hacerlo? Afortunadamente, muchas bibliotecas como Underscore y Lodash ya ofrecen utilidades para _.first& _.last.

Esto ofrece una gran mejora en el lastLogin()ejemplo anterior:

let lastLogin = async () => _.last(await getLogins());

Pero cuando se trata del ejemplo de intercambio de últimos elementos, la mejora es menos significativa:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = _.last(animals);animals[animals.length - 1] = _.last(faces);faces[faces.length - 1] = lastAnimal;

Estas funciones de utilidad eliminaron 2 de las 8 referencias, solo que ahora introdujimos una dependencia externa que, curiosamente, no incluye una función para configurar elementos terminales.

Lo más probable es que dicha función se excluya deliberadamente porque su API sería confusa y difícil de leer. Las primeras versiones de Lodash proporcionaron un método _.last(array, n)en el que n era el número de elementos del final, pero finalmente se eliminó a favor de _.take(array, n).

Suponiendo que numses una matriz de números, ¿cuál sería el comportamiento esperado de _.last(nums, n)? Podría devolver los dos últimos elementos como _.take, o podría establecer el valor del último elemento igual an .

Si tuviéramos que escribir una función para establecer el último elemento en una matriz, solo hay algunos enfoques a considerar usando funciones puras, encadenamiento de métodos o usando prototipos:

let nums = ['d', 'e', 'v', 'e', 'l']; // set first = last
_.first(faces, _.last(faces)); // Lodash style
$(faces).first($(faces).last()); // jQuery style
faces.first(faces.last()); // prototype

No creo que ninguno de estos enfoques sea una gran mejora. De hecho, aquí se pierde algo importante. Cada uno realiza una asignación, pero ninguno usa el operador de asignación ( =). Esto podría hacerse más evidente con convenciones de nomenclatura como getLasty setFirst, pero eso rápidamente se vuelve demasiado detallado. Sin mencionar que el quinto círculo del infierno está lleno de programadores obligados a navegar por el código heredado "autodocumentado" donde la única forma de acceder o modificar los datos es a través de captadores y definidores.

De alguna manera, parece que estamos atrapados con [0]& [length — 1]

¿O somos nosotros? ?

La propuesta

Como se mencionó, una propuesta de ECMAScript Technical Candidate (TC39) intenta abordar este problema definiendo dos nuevas propiedades en el Arrayobjeto: lastItem& lastIndex. Esta propuesta ya es compatible con core-js 3 y se puede usar hoy en Babel 7 y TypeScript. Incluso si no está utilizando un transpilador, esta propuesta incluye un polyfill.

Personalmente, no encuentro mucho valor lastIndexy prefiero el nombre más corto de Ruby para firsty last, aunque esto se descartó debido a posibles problemas de compatibilidad web. También me sorprende que esta propuesta no sugiera una firstItempropiedad de coherencia y simetría.

Mientras tanto, puedo ofrecer un enfoque sin dependencia, al estilo Ruby en ES6:

Primero último

Ahora tenemos dos nuevas propiedades Array - first& last- y una solución que:

✓ Utiliza el operador de asignación

✓ No clona la matriz

✓ Puede definir una matriz y obtener un elemento terminal en una expresión

✓ Es legible por humanos

✓ Proporciona una interfaz para obtener y configurar

Podemos reescribir lastLogin()nuevamente en una sola línea:

let lastLogin = async () => (await getLogins()).last;

Pero la verdadera victoria llega cuando intercambiamos los últimos elementos en dos matrices con la mitad del número de referencias:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals.last;animals.last = faces.last;faces.last = lastAnimal;

Todo es perfecto y hemos resuelto uno de los problemas más difíciles de CS. No hay pactos malvados escondidos en este enfoque ...

Prototipo de paranoia

Seguramente no hay nadie [programador] en la tierra tan justo como para hacer el bien sin pecar jamás. - Adaptado de Eclesiastés 7:20.

Muchos consideran que extender el prototipo de un Object nativo es un antipatrón y un crimen punible con 100 años de programación en Java. Antes de la introducción de la enumerablepropiedad, la extensión Object.prototypepodría cambiar el comportamiento de los for inbucles. También podría generar conflictos entre varias bibliotecas, marcos y dependencias de terceros.

Quizás el problema más insidioso es que, sin las herramientas de tiempo de compilación, un simple error de ortografía podría crear inadvertidamente una matriz asociativa.

let faces = ["?", "?", "?", "?", "?"];let ln = faces.length 
faces.lst = "?"; // (5) ["?", "?", "?", "?", "?", lst: "?"] 
faces.lst("?"); // Uncaught TypeError: faces.lst is not a function 
faces[ln] = "?"; // (6) ["?", "?", "?", "?", "?", "?"] 

Esta preocupación no es exclusiva de nuestro enfoque, se aplica a todos los prototipos de objetos nativos (incluidas las matrices). Sin embargo, esto ofrece seguridad en una forma diferente. Las matrices en Javascript no tienen una longitud fija y, en consecuencia, no hay IndexOutOfBoundsExceptions. El uso Array.lastasegura que no intentemos acceder [length]accidentalmente y sin querer entrar al undefinedterritorio.

Independientemente del enfoque que adopte, existen dificultades. Una vez más, el software demuestra ser un arte de hacer concesiones.

Continuando con la referencia bíblica extraña, asumiendo que no creemos que extender Array.prototypees un pecado eterno, o estamos dispuestos a tomar un bocado del fruto prohibido, ¡podemos usar esta sintaxis concisa y legible hoy!

Ultimas palabras

Los programas deben estar escritos para que las personas los lean y solo de manera incidental para que los ejecuten las máquinas. - Harold Abelson

En lenguajes de secuencias de comandos como Javascript, prefiero un código que sea funcional, conciso y legible. Cuando se trata de acceder a los elementos de la matriz de terminales, encuentro que la Array.lastpropiedad es la más elegante. En una aplicación de front-end de producción, podría preferir Lodash para minimizar los conflictos y las preocupaciones entre navegadores. Pero en los servicios de back-end de Node donde controlo el entorno, prefiero estas propiedades personalizadas.

Ciertamente no soy el primero, ni seré el último, en apreciar el valor (o la precaución sobre las implicaciones) de propiedades como Array.lastItem, que con suerte llegará pronto a una versión de ECMAScript cerca de usted.

Sígueme en LinkedIn · GitHub · Medium