Introducción
Cuando tratamos de comprobar si un determinado elemento existe dentro de un array en Javascript, tradicionalmente hemos tenido que utilizar métodos que no estaban inicialmente pensados para esto.
Con la nueva propuesta ECMAScript 2016 (ES7), tenemos al fin un método nativo para ello: includes(). Vamos a verlo en detalle:
Sintaxis
El método includes() determina si un arreglo incluye un elemento determinado, regresa true o false según corresponda.
Su sintaxis es la siguiente (la especificación completa aquí):
Array.prototype.includes( searchElement[, fromIndex ] ) |
Donde:
- searchElement es el elemento a buscar.
- fromIndex es un parámetro opcional que marca la posición en la matriz a partir de la cual se comienza a buscar el elemento dado.
Ejemplo básico
El ejemplo más simple es aquel donde buscamos un elemento en un array:
var myArr = [ 'la', 'donna', 'e', 'mobile', 'cual', 'piuma', 'al', 'vento' ]; console.info( myArr.includes( 'donna' ) ); // true console.info( myArr.includes( 'pensiero' ) ); // false |
Nótese que este método devuelve un Boolean, algo que lo hace diferente al tradicional indexOf() que hemos venido utilizando desde los noventa:
console.info( myArr.indexOf( 'donna' ) ); // 1 console.info( myArr.indexOf( 'pensiero' ) ); // -1 |
Afortunadamente, ahora ganamos legibilidad al poder incluir nuestras comprobaciones dentro de un flujo lógico sin necesidad de convertir el resultado:
// Old/ugly fashion: if ( myArr.indexOf( '' ) !== -1 ) { /* OK, value exists! */ } // New/nice ES7 if ( myArr.includes( '' ) ) { /* OK, value exists! */ } |
Ejemplos con índice
Si necesitamos hacer uso del segundo argumento, los ejemplos quedarían como sigue:
console.info( myArr.includes( 'cual', 5 ) ); // false console.info( myArr.includes( 'donna', 1 ) ); // true console.info( myArr.includes( 'vento', -1 ) ); // true |
Como curiosidad, podemos ver en el último ejemplo que es posible utilizar un índice negativo para que el conteo comience desde la derecha.
NOTA: Como es habitual, los resultados durante el trabajo con arrays se ajustan siempre en Índice Cero.
Valores especiales y expresiones
Al igual que con valores simples, podemos realizar comprobaciones sobre valores especiales y expresiones:
// Helper function var str = () => 'donna'; var myArr = [ 10, 100, 200, 'donna', NaN, Infinity ]; console.info( myArr.includes( NaN ) ); // true console.info( myArr.includes( Infinity ) ); // true console.info( myArr.includes( str() ) ); // true console.info( myArr.includes( 'don' ) ); // false |
La última comprobación ejemplifica cómo includes() requiere del elemento completo (en este caso ‘donna’) para reportar coincidencia.
NOTA: La identificación de NaN dentro de un conjunto es una de las novedades de includes() frente a indexOf().
Actuando sobre cadenas
Además de sobre el objeto Array, includes() también se ha añadido como un método para el objeto String, lo que nos permite realizar las comprobaciones sobre cadenas de texto. En este caso, es posible buscar tanto palabras y oraciones como secuencias de caracteres:
var str = 'En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor.'; console.info( str.includes( 'lugar' ) ); // true console.info( str.includes( 'de cuyo' ) ); // true console.info( str.includes( 'ero aco' ) ); // true -> quiERO ACOrdarme console.info( str.includes( 'Mancha', 18 ) ); // true console.info( str.includes( 'Mancha', 19 ) ); // false |
NOTA: El texto del Quijote lo he sacado de aquí. Cito la fuente porque parece que está el tema calentito últimamente con este texto en particular 😉
Un tema importante aquí es advertir como el parámetro fromIndex se aplica sobre cada letra, no cada palabra. De ahí que ‘Mancha’ sí esté entre los 18 + 1 primeros caracteres, pero no sobre los 19 + 1. El ‘+ 1’ en cada término es porque el conteo empieza (como en Array) también en 0.
Arrays tipados
El nuevo método también puede aplicarse sobre arrays tipados:
var typedArr = Uint8Array.of( 3, 14, 15 ); console.info( typedArr.includes( 15 ) ); // true |
Es interesante resaltar aquí su uso con Uint8ClampedArray, resultado del trabajo con imágenes y canvas a través del método CanvasRenderingContext2D.getImageData().
Rendimiento
Es hora de comprobar cómo rinde este método frente al tradicional indexOf().
Para medir los tiempos, utilizaremos la propia consola del navegador y su método time().
Como fuentes de datos, utilizaremos 3 arrays de diferente longitud (100, 5000 y 20000), lo cual nos permite ver cómo escalan los tiempos en función del número de elementos sobre el que deben realizar la búsqueda.
El código preparatorio sería el siguiente:
var prefix = 'item-'; var arr100 = Array.apply( null, { length: 100 } ).map( ( x, y ) => prefix + y ), arr5000 = Array.apply( null, { length: 5000 } ).map( ( x, y ) => prefix + y ), arr20000 = Array.apply( null, { length: 20000 } ).map( ( x, y ) => prefix + y ); var key = ( arr ) => prefix + arr.length / 2; // Small size console.time( 'searchingItem' ); console.info( arr100.length, arr100.includes( key( arr100 ) ) ); console.timeEnd( 'searchingItem' ); console.time( 'searchingItem' ); console.info( arr100.indexOf( key( arr100 ) ) !== -1 ); console.timeEnd( 'searchingItem' ); // Medium size console.time( 'searchingItem' ); console.info( arr5000.includes( key( arr5000 ) ) ); console.timeEnd( 'searchingItem' ); console.time( 'searchingItem' ); console.info( arr5000.indexOf( key( arr5000 ) ) !== -1 ); console.timeEnd( 'searchingItem' ); // Large size console.time( 'searchingItem' ); console.info( arr20000.includes( key( arr20000 )) ); console.timeEnd( 'searchingItem' ); console.time( 'searchingItem' ); console.info( arr20000.indexOf( key( arr20000 ) ) !== -1 ); console.timeEnd( 'searchingItem' ); |
Comprobamos cada array con los dos métodos que estamos comparando (includes e indexOf). El código resulta algo repetitivo, pero a efectos didácticos resulta muy claro.
Los arrays los creamos de forma programática, obteniendo arreglos con esta pinta:
[ 'item-0', 'item-1', 'item-2', ... ] |
La función ‘key‘ compone el elemento que vamos a buscar en nuestro array. Para que los resultados sean más precisos, esta snippet escoge el valor central de cada matriz, lo que obliga al intérprete a recorrer un número proporcional de elementos en cada test.
Con todo preparado, lanzamos nuestro script y obtenemos los siguientes resultados:
100 arreglos | 5000 arreglos | 20000 arreglos | |
---|---|---|---|
includes() | 1.52ms | 1.24ms | 1.29ms |
indexOf() | 1.38ms | 1.22ms | 1.21ms |
NOTA: Los resultados obtenidos más arriba son los tiempos medios tras 100 ejecuciones.
A la vista de los datos, el número de elementos inicial no penaliza, resultando incluso más rápido cuanto mayor es el array.
Tampoco se aprecia una diferencia de rendimiento entre los métodos indexOf() e includes() algo, por otro lado, habitual en funcionalidades recientemente incorporadas en los distintos navegadores.
Soporte
Como con todas las funcionalidades emergentes de Javascript, hay que echar un vistazo al soporte de este nuevo método a día de hoy entre los diferentes navegadores:
Chrome | Firefox | Internet Explorer | Edge | Opera | Safari | Android | Opera Mobile | Safari Mobile | Chrome for Android |
---|---|---|---|---|---|---|---|---|---|
47 | 43 | No soportado | No soportado | 34 | 9 | No soportado | 34 | 9 | 47 |
Salvando el caso de los navegadores de Microsoft, el soporte a día de hoy (noviembre, 2016) es muy completo.
Pollyfill
Para trabajar precisamente con los navegadores que no soportan por el momento este método (Internet Explorer y Edge), podemos implementar este ‘pollyfill‘. Como es habitual, lo recomendable es echar mano a este código únicamente cuando no esté presente el método de forma nativa.
if ( ! Array.prototype.includes ) { Array.prototype.includes = function( searchElement /*, fromIndex*/ ) { 'use strict'; var O = Object( this ), len = parseInt( O.length ) || 0; // Quick exit if missing data if ( len === 0 ) { return false; } var n = parseInt( arguments[ 1 ], 10 ) || 0, k, currentElement; if ( n >= 0 ) { k = n; } else { k = len + n; if ( k < 0 ) { k = 0; } } while ( k < len ) { currentElement = O[ k ]; if ( searchElement === currentElement || ( searchElement !== searchElement && currentElement !== currentElement ) ) { return true; } k++; } return false; }; } |
NOTA: El código original ha sido extraído y re acondicionado de aquí.
Conclusión
Cuando programamos, resulta trivial determinar si un elemento dado está presente en un array. Hasta el momento, requeríamos del uso de indexOf() y una posterior conversión de su retorno en un valor booleano.
Con includes(), el problema se soluciona al contar con un método nativo que funciona correctamente y que resulta gramaticalmente mucho más preciso. Si bien su propia sintaxis puede resultar inapropiada (estamos más habituados a instrucciones como ‘has()’ o ‘contains()’), su elección se debe a salvaguardar la retrocompatibilidad con estos métodos ya definidos en otros frameworks de uso frecuente (MooTools) o en el propio lenguaje Javascript (Set.prototype.has()).
Como siempre, toda mejora es bienvenida.
Aún no tenemos debug!
Ningún marciano se ha comunicado.