Métodos para crear un nuevo Array

Figura
Figura. El método concat() crea un nuevo Array.

Los métodos concat(), map(), slice() y filter() devuelven un nuevo Array, no modificando el Array sobre el que estén actuando. Con map() iteramos por el Array devolviendo otro Array con el mismo número de elementos, donde sus valores se establecen en una función callback. Los otros métodos, o map() cuando devuelve los valores, realizan una copia superficial del valor.

Los argumentos callback y thisArg se usan en muchos de los métodos de Array. En este tema los veremos en map() y filter(). Pero también aparecerán en otros métodos expuestos en temas siguientes. El callback es una función que, generalmente, espera los argumentos valor e índice actual y una referencia al Array. Los suelo abreviar como v,i,a. Cuando se ejecuta uno de estos métodos se pasa una copia del Array, quedando entonces congelado en la referencia a. Si dentro del callback modificamos el Array, estos cambios no afectarán a la referencia interna.

Por otro lado el argumento thisArg es opcional, pues dentro de un callback de un método de Array la única forma de obtener una referencia this es pasándola en este argumento. En los ejemplos veremos como se usa.

arr.concat(elem0, elem1, ...)

ES3

Este método concatena los argumentos como elementos del Array sobre el que se aplica el método. Si un elemento es un Array, se concatenarán los elementos de ese Array. En el siguiente ejemplo tenemos dos argumentos, uno es el número que es agregado. El otro argumento es un Array, agregándose los elementos que también son números en ese Array. Los Arrays que itervienen en la concatenación no son modificados, pues se creará el nuevo Array que será el resultado devuelto por el método.

let arr1 = [1, 2], arr2 = [4, 5];
let arr = arr1.concat(3, arr2);
console.log(arr); // [1, 2, 3, 4, 5]
//Los Arrays que intervienen no son modificados
console.log(arr1); // [1, 2]
console.log(arr2); // [4, 5]
    

El comportamiento por defecto de extender la concatenación a los elementos de un argumento Array es evitable con el símbolo bien conocido isConcatSpreadable:

let arr1 = [1, 2], arr2 = [4, 5];
arr2[Symbol.isConcatSpreadable] = false;
let arr = arr1.concat(3, arr2);
console.log(arr); // [1, 2, 3, [4, 5]]
    

El método concat() crea una copia superficial (shallow copy) de los elementos que concatena. Esto quiere decir que de los objetos se copia la referencia, mientras que para los tipos primitivos se copia el valor. Para hacer un copia de un Array podemos hacer let copia = [].concat(original).

arr.map(callback[, thisArg]))

ES5

El método map() crea un nuevo Array a partir de la devolución de una función callback que itera sobre cada elemento. Esta acción la podemos denominar como mapear un Array. En este ejemplo lo usamos para obtener el codigo ASCII de cada carácter:

function callback(valor){
    return valor.charCodeAt(0);
}
let arr = ["a", "b", "c"];
let nuevoArr = arr.map(callback);
//Nuevo Array
console.log(nuevoArr); // [97, 98, 99]
//El anterior no se modifica
console.log(arr); // ["a", "b", "c"]
    

No necesariamente el valor tiene que intervenir en la devolución. Podemos devolver cualquier cosa:

let arr = [1, 2, 3];
console.log(arr.map(v => "a")); //["a", "a", "a"]
    

Pero si devolvemos los mismos valores hay que tener en cuenta que con arr.map(v => v) obtenemos una copia superficial (shallow copy) del Array arr. Si son valores primitivos como string, number, boolean, null o undefined hace una verdadera copia del valor. En otro caso lo que copia es la referencia al objeto. En este ejemplo mapeamos un Array compuesto de dos valores primitivos, un objeto y un Array. En el nuevo Array vemos que al modificar los primitivos en el nuevo Array no afecta al anterior, pero si lo hace si modificamos el objeto {a: 88} o el Array [4, 77]:

let arr = ["a", true, 2, {a: 3}, [4]];
let nuevoArr = arr.map(v => v);
nuevoArr[0] = "b";
nuevoArr[1] = false;
nuevoArr[2] = 99;
nuevoArr[3].a = 88;
nuevoArr[4].push(77);
console.log(arr); // ["a", true, 2, {a: 88}, [4, 77]];
console.log(nuevoArr); // ["b", false, 99, {a: 88}, [4, 77]];
    

El callback tiene tres parámetros: valor, índice y una referencia al Array sobre el que se está iterando. Podemos modificarlo, pero map() ya habrá establecido una copia de los elementos antes de empezar a iterar, por lo que los cambios efectuados en el Array no serán contemplados en el nuevo Array. En este código agregamos nuevos elementos y borramos el segundo. Estos cambios no son recogidos por el nuevo Array:

let arr = [1, 2, 3];
let nuevoArr = arr.map((valor, indice, array) => {
    array.push(indice+10);
    if (indice===1) delete array[1];
    return valor;
});
console.log(`arr: [${arr}]`); // arr: [1,,3,10,11,12]
console.log(`nuevoArr: [${nuevoArr}]`); // nuevoArr: [1,2,3];
    

Podemos generalizarlo a los array-like como los String usando call() o apply(). En este ejemplo lo usamos para convertir en mayúscula la primera letra de cada palabra, ayudándonos del método join() para reponer el String. Observe el uso de los tres argumentos del callback: valor, índice y array (v,i,a):

let str = "un gran avance";
let nuevoStr = [].map.call(str, (v,i,a) => {
    if ((i===0)||(a[i-1]===" ")){
        return v.toUpperCase();
    } else {
        return v;
    }
}).join("");
console.log(nuevoStr); // "Un Gran Avance"
    

No debemos olvidar que propagando un String obtenemos un Array, así que con [...str] podemos aplicar directamente el método map() sin necesidad de usar call():

let str = "un gran avance";   
let nuevoStr = [...str].map((v,i,a) => {
    if ((i===0)||(a[i-1]===" ")){
        return v.toUpperCase();
    } else {
        return v;
    }
}).join("");
console.log(nuevoStr); // "Un Gran Avance"
    

La sintaxis completa del método es arr.map(callback[, thisArg]), portando el argumento opcional thisArg la referencia al this al que se tendrá alcance dentro del callback. Si se omite ese argumento entonces esa referencia será undefined. En este ejemplo vemos que this no apunta al Array, de tal forma que this[i] será undefined. Recordar que para usar this dentro de una función no podemos usar las funciones flecha.

let arr = [1, 2];
let nuevoArr = arr.map(function(v, i) {
    //this no apunta a ningún sitio
    return this[i];
});
console.log(nuevoArr); // [undefined, undefined]
    

Pero si lo apuntamos a otro Array entonces this[i] se referirá a ese segundo Array. Aquí obtenemos los elementos del segundo Array sumando su valor al del primer Array. Esto podría servir para devolver la suma por columnas de dos Array:

let arr1 = [1, 2];
let arr2 = [3, 4];
let nuevoArr = arr1.map(function(v, i) {
    //this apunta a arr2
    return this[i] + v;
}, arr2);
console.log(nuevoArr); // [4, 6]
    

En el siguiente ejemplo interactivo usaremos thisArg. Explicaremos lo principal para ver el uso de map(), pues el resto del código lo puede ver en el enlace al pie del ejemplo. Supongamos una simple aplicación de gestión de un almacén de artículos. Por un lado tenemos el constructor Tarifa() que almacena el descuento aplicado y otras propiedades. El método agregado al prototipo ver() nos listará una tarifa:

//Constructor de tarifas
function Tarifa(nombre, dto, conDevolucion){
    this.nombre = nombre;
    this.dto = dto;
    this.conDevolucion = conDevolucion;
}
Tarifa.prototype.ver = function(){
    return `{nombre: "${this.nombre}", dto: ${this.dto}, 
            conDevolucion: ${this.conDevolucion}}`;
};
    

Por otro lado tenemos el constructor Almacen(), donde un Array almacenará los artículos. Con métodos agregados al prototipo podemos agregar artículos, listarlos y finalmente valorar el almacén. Instanciamos un nuevo almacén con let articulos = new Almacen("Libros").

//Constructor de un almacén
function Almacen(titulo){
    this.titulo = titulo;
    this.articulos = [];
}
Almacen.prototype.agregar = function (ref, uds, pvp){
    this.articulos.push({ref: ref, uds: uds, pvp: pvp});
};
Almacen.prototype.listar = function (){
    return this.articulos.map(v => 
        `{ref: "${v.ref}", uds: ${v.uds}, pvp: ${v.pvp}}`).join(", ");
};
Almacen.prototype.valorar = function (thisArg){
    return this.articulos.map(function (v){
        let descuento = (this && 
            (this.hasOwnProperty("dto"))) ? this.dto : 0;
        return `{ref: "${v.ref}", 
            valor: ${v.uds * v.pvp * (1-descuento)}}`;
    }, thisArg).join(", ");
}
    

Observe el uso de map() para recorrer el Array de artículos y formar una cadena que podemos presentar en pantalla. En el método valorar() también hacemos uso de map(). Ahora se trata de iterar por todos los artículos obteniendo el valor de venta de las existencias. Observe que hay dos this. El único que está por fuera de map() se refiere al objeto sobre el que estamos aplicando el método valorar(). Mientras que los que están dentro de map() son los que traemos con el argumento thisArg.

En el ejemplo hacemos tres valoraciones. En la primera con la opción de tarifa ninguna hacemos articulos.valorar() sin argumento. Dentro de map() el this será undefined y, por tanto, la variable descuento será cero. Con las otras opciones pasamos en thisArg un objeto tarifa, que como vimos antes, contiene una propiedad dto. Ahora en map() el this apuntará a ese objeto desde donde obtenemos el correspondiente descuento.

Ejemplo: Ejemplo uso de map() y de su argumento thisArg

Almacén:
Tarifas:
Valorar con tarifa
Valoración del almacén:
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Detalles sobre creación de nuevo Array con map() y from()

El método map() nos permite crear un nuevo Array aplicándolo a un Array. Pero también podemos crearlo a partir de un iterable. En el siguiente código convertimos el String "abc" en un Array de sus códigos ASCII. Podemos usar el método call() o propagar el String con desestructurado. Y también hacer uso del método estático from(), con o sin el método callback. Este callback de from() nos evita tener que crear un Array intermedio para luego aplicar map().

//Todos estas ejecuciones devuelven el Array [97, 98, 99]
//Usando call() para aplicar map() al String
console.log([].map.call("abc", v => v.charCodeAt(0)));
//Propagando el String en un Array y luego aplicando map()
console.log([..."abc"].map(v => v.charCodeAt(0)));
//Usando from() y luego aplicando map()
console.log(Array.from("abc").map(v => v.charCodeAt(0)));
//Usando from() con un callback
console.log(Array.from("abc", v => v.charCodeAt(0)));
    

Para crear un Array literal de un número de posiciones vacías hay que tener cuidado de contar las comas y no los espacios. Por ejemplo el literal [ , , ] crearía un Array de dos posiciones, no tres como aparenta por los huecos entre comas. Esto se observa en Firefox que nos da en la consola el numero de ranuras vacías (o posiciones sin valores) del Array. Por lo tanto para crear un Array con 3 posiciones vacías habría que hacer [ , , , ] con tantas comas como posiciones.

console.log(Array(3)); // Array [ <3 ranuras vacías> ]
console.log([,,]); // Array [ <2 ranuras vacías> ]
console.log([,,,]); // Array [ <3 ranuras vacías> ]
console.log([,,,].toString()); // ,, 
    

Además vemos que si hacemos toString() sobre [,,,] nos dará ",," con dos comas y, por tanto, tres posiciones. Teniendo en cuenta lo anterior podemos pensar en crear un Array con un número de posiciones vacías y rellenarlo con map() con Array(3).map(callback) o [,,,].map(callback), pero no funcionará. El problema es que map() lee los huecos y los traspasa tal cual al nuevo Array, sin entrar en el callback. Una forma de solucionarlo podría ser propagar el Array con huecos.

//Los huecos en un Array se mapean a huecos y no entran en callback
console.log([88,,99].map((v,i) => i).toString()); // 0,,2
console.log(Array(3).map((v,i) => i).toString()); // ,,
console.log([,,,].map((v,i) => i).toString()); // ,,
//Para evitarlo propagamos antes el Array
console.log([...[88,,99]].map((v,i) => i).toString()); // 0,1,2
console.log([...Array(3)].map((v,i) => i).toString()); // 0,1,2
console.log([...[,,,]].map((v,i) => i).toString()); // 0,1,2
    

Conocer lo anterior nos puede servir para construir sucesiones matemáticas con map(). En el siguiente código construimos siete elementos de una progresión aritmética, geométrica y de Fibonacci:

//Progresión aritmética de distancia 3 con primer término 2
console.log([...Array(7)].map((v,i) => 
    2+i*3)); // [2, 5, 8, 11, 14, 17, 20]
//Progresión geométrica de razón 3 con primer término 1
console.log([...Array(7)].map((v,i) =>
    1*Math.pow(3, i))); // [1, 3, 9, 27, 81, 243, 729]
//Sucesión de Fibonacci
console.log([...Array(7)].map((v,i,a) => 
    a[i]=(i>1)?a[i-2]+a[i-1]:1)); // [1, 1, 2, 3, 5, 8, 13]
    

Alguno de los ejemplos anteriores también es posible generarlo con Array.from(). Siempre que no se tenga que acceder a los elementos podemos usar un array-like con una propiedad length igual al número de elementos de la sucesión:

console.log(Array.from({length: 7}, (v, i) =>
    2+i*3)); // [2, 5, 8, 11, 14, 17, 20]
    

arr.slice(inicio[, fin])

ES3

El método slice() se usa para recortar un Array, devolviendo un nuevo Array con los elementos en el rango [inicio, fin) indicado en los argumentos. Vea que el rango incluye el inicio pero no el fin. Si éste no se indica se tomará la longitud length del Array.

let arr = [1, 2, 3, 4, 5];
let nuevoArr = arr.slice(1, 3);
console.log(nuevoArr); // [2, 3]
    

Como con otros métodos que aceptan rangos de elementos, inicio y fin pueden ser negativos. En ese caso y antes de aplicar el método, JavaScript resta esos valores del length del Array. Así es como si indicáramos el rango empezando desde el final.

let arr = [1, 2, 3, 4, 5];
let nuevoArr = arr.slice(-4, -2); // como 5-4=1, 5-2=3
console.log(nuevoArr); // [2, 3]
    

Como con el resto de métodos que crean nuevos Array, este slice() también crea una copia superficial (shallow copy) del Array. Esto quiere decir que los tipos primitivos se copian por valor y para los objetos se copia la referencia al objeto. En este ejemplo creamos un nuevo Array y luego lo modificamos. El objeto {a: 3} resultará modificado en el Array original y en el nuevo.

let arr = [1, 2, {a: 3}];
let nuevoArr = arr.slice(1); // [2, {a: 3}]
nuevoArr[0] = 99;
nuevoArr[1].a = 88
console.log(nuevoArr); // [99, {a: 88}]
console.log(arr); // [1, 2, {a: 88}];
    

El método slice() puede generalizarse a los array-like. Por ejemplo, podemos aplicarlo a un String con la forma conocida Array.prototype.slice.call(str, N). O abreviando con [].slice.call(str, N). O incluso dado que un String es iterable, podemos usar el desestructurado con [...str].slice(N). En cualquier caso un String tiene su propio método slice():

let str = "abcdefg";
//Aplicando el método slice() de Array
let nuevoStr = [...str].slice(3).join("");
console.log(nuevoStr); // "defg"
//Aplicando el método slice() de String
let str2 = str.slice(3);
console.log(str2); // "defg"
    

Búsqueda binaria con el método slice()

Como ejemplo de aplicación del método slice() vamos a implementar el algoritmo de búsqueda binaria. Se trata de buscar un elemento en un Array. Es, por supuesto, poco útil, pues los métodos para buscar en un Array pueden servirnos mejor y ser más eficientes. Pero lo importante es explicar el concepto de búsqueda binaria, un algoritmo que se encuadra dentro de los del tipo divide y vencerás.

En el ejemplo tenemos un Array con 142 etiquetas HTML. Si usamos un bucle for que vaya consultando si arr[i] === key es obvio que el peor caso sucederá cuando la palabra a buscar esté en el último lugar del Array. Una forma de reducir este peor coste es usando la búsqueda binaria:

function buscarBinariaRecursiva(arr, start, end, key){
    if (start === end){           
        return (arr[start]===key) ? key : null; 
    } else {
        let medio = start + Math.floor((end-start+1) / 2);
        if (key === arr[medio]){
            return key;
        } else if (key < arr[medio]){
            return buscarBinariaRecursiva(arr, start, medio-1, key);
        } else {
            return buscarBinariaRecursiva(arr, medio+1, end, key);
        }
    }
}
    
La función Math.floor(n) nos devuelve el entero menor. Podría obtenerse también con parseInt(n, 10).

El código anterior es un recursivo con argumentos un Array, su primer y último índice y la palabra a buscar. Si el rango [start, end] tiene una única posición sólo resta comprobar si ese único elemento es el que estamos buscando. Si lo es se devuelve, si no devolvemos un valor nulo. En caso de que el rango [start, end] tenga más de un elemento obtenemos la posición media y preguntamos si la palabra a buscar está ahí, en cuyo caso la devolvemos. En otro caso consultamos si la palabra es mayor que la que está en el medio. Según este resultado buscamos en un trozo u otro del Array, sin incluir la posición media pues ya la habremos consultado.

En cada iteración el problema se divide por dos, por eso lo de búsqueda binaria y divide y vencerás. Un Array de 142 posiciones podremos dividirlo por dos no más de 7 veces, pues log2142 ≅ 7.15. Pero si en el peor caso dividimos esa 7 veces aún tendremos que realizar otra iteración más con un Array de un sólo elemento. Por lo tanto el coste será 8 en el peor caso.

Un coste de 8 iteraciones en el peor caso frente a los 142 de un bucle for es significativo. El mejor caso con coste 1 es cuando la palabra está en el centro del Array, como "keygen" en el ejemplo. Un segundo mejor caso con coste 2 será cuando esté en el centro de la segunda división, como para "s" en el ejemplo. El mayor coste sucederá cuando tenga que realizar las 7 divisiones quedando finalmente una única posición, comprobándose entonces si arr[i] === key. Esto sucederá por ejemplo con la primera posición "a". O cuando no exista la palabra a buscar en el Array.

El recursivo anterior puede transformarse en un bucle while. Puede ver ese código más el resto en el enlace al pie del ejemplo. Ahora sólo vamos a exponer como adaptamos el recursivo anterior usando el método slice().

function buscarBinariaRecursiva2(arr, key){
    if (arr.length>1){
        let medio = Math.floor(arr.length / 2);
        if (key === arr[medio]){
            return key;
        } else if (key < arr[medio]){
            return buscarBinariaRecursiva2(arr.slice(0, medio), key);
        } else {
            return buscarBinariaRecursiva2(arr.slice(medio+1), key);
        }
    } else {
        return (arr[0]===key) ? key : null;
    }
}
    

En el primer recursivo el argumento arr era siempre el mismo Array. En cada iteración lo que se modificaban eran los rangos [start, end] donde debíamos actuar en ese Array. Ahora usando el método slice() lo que hacemos es ir recortando el Array en cada iteración. Empezamos preguntando si la longitud es uno para comprobar si es la palabra a buscar. En otro caso hacemos la división del Array.

Por un lado recortamos con arr.slice(0, medio) para buscar en la parte izquierda del Array. Recuerde que el segundo argumento no entra en el recorte, por lo que ahí tendríamos un rango [0, medio-1], dado que la posición media ya fue consultada y no formará parte de los nuevos rangos dónde buscar. El recorte derecho sería con arr.slice(medio+1) para buscar en la parte derecha del Array. Como no expresamos el segundo argumento, el recorte será hasta el final del Array.

Ejemplo: Ejemplo uso de slice() para Búsqueda Binaria

Tags: a, abbr, acronym, address, applet, area, article, aside, audio, b, base, basefont, bdi, bdo, bgsound, big, blink, blockquote, body, br, button, canvas, caption, center, cite, code, col, colgroup, command, content, data, datalist, dd, del, details, dfn, dialog, dir, div, dl, dt, element, em, embed, fieldset, figcaption, figure, font, footer, form, frame, frameset, h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, i, iframe, image, img, input, ins, isindex, kbd, keygen, label, legend, li, link, listing, main, map, mark, marquee, menu, menuitem, meta, meter, multicol, nav, nobr, noembed, noframes, noscript, object, ol, optgroup, option, output, p, param, picture, plaintext, pre, progress, q, rp, rt, rtc, ruby, s, samp, script, section, select, shadow, small, source, spacer, span, strike, strong, style, sub, summary, sup, table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr, track, tt, u, ul, var, video, wbr, xmp

Longitud Array: length =

Registro 0 de 0
Método
Búsqueda binaria

Iteraciones peor caso: 142. Mejor caso: 1.

Clave encontrada:

Iteraciones:

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

arr.filter(callback[, thisArg])

ES5

El método filter() crea un nuevo Array con los elementos que superan el filtro de la función callback. En el siguiente ejemplo convertimos en Array una frase dividiéndola en palabras con str.split(" "). Filtramos las palabras con longitud dos usando un return en la función.

function callback (valor){
    return valor.length === 2;
}
let str = "En un lugar de La Mancha de cuyo nombre no quiero acordarme";
let arr = str.split(" ");
let dosLetras = arr.filter(callback);
console.log(dosLetras); // ["En", "un", "de", "La", "de", "no"]
    

Este método, como todos los de este tema, devuelve una copia superficial. Se copian los valores primitivos y las referencias a objetos. En este ejemplo filtramos sólo los números y los Array interiores. Luego modificamos el nuevo Array y observamos que los cambios en el Array interior se reflejan en el original y en el nuevo Array.

let arr = [1, "a", false, [2, 3], {k: 4}];
let nuevoArr = arr.filter(v => {
    let tipo = v.constructor.name;
    if ((tipo === "Number")||(tipo === "Array")){
        return v;
    }
});
console.log(nuevoArr); // [1, [2, 3]]
nuevoArr[0] = 88;
nuevoArr[1][0] = 77;
//El Array interior se copia por referencia, cambios 
//en uno se reflejan en el otro
console.log(nuevoArr); // [88, [77, 3]]
console.log(arr); [1, "a", false, [77, 3], {k: 4}];
    

Este método para filtrar puede ser muy útil a la hora de trabajar con el DOM. Como acepta array-like, podemos usar call(), apply() o destructuring para filtrar elementos. Ahora recuperamos todos los elementos de la página actual y filtramos sólo los encabezados h2. Observe que la expresión v.tagName.toLowerCase() === "h2" devuelve un valor booleano. Y que en una función flecha cuando sólo hay una sentencia con un return podemos obviarlo y poner sólo la expresión.

let tags = [...document.body.getElementsByTagName("*")];
let h2s = tags.filter(v => v.tagName.toLowerCase() === "h2");
console.log(h2s); // [h2.dispnone, h2#notas-creanarray.num, ...
    

El callback, como en map() y otros métodos, tiene tres argumentos: el valor e índice del elemento sobre el que se itera y una referencia al Array. Esto lo abreviamos como v, i, a. En el siguiente ejemplo filtramos los impares con v % 2 que se encuentren en la mitad derecha del Array. Vea como usamos todo los argumentos v, i, a:

let arr = [5, 22, 13, 88, 7, -3, 42, 1];
let impares = arr.filter((v,i,a) => (v % 2) && 
    (i >= parseInt(a.length/2, 10)));
console.log(impares); // [7, -3, 1]
    

El método filter además puede llevar el argumento opcional thisArg. Como vimos con map(), dentro del callback el this es undefined cuando no se usa thisArg o en otro caso será esa referencia. En el siguiente apartado haremos uso de ese argumento.

Usando filter() para filtrar elementos en el DOM

Este ejemplo va de buscar en el DOM de esta página los elementos que cumplan con un determinado tag, que dispongan de un cierto atributo y un valor dado para ese atributo. Tenemos tres campos de texto que recogen esos valores y que pasamos a un objeto filtro:

let filtro = {
    tag: document.getElementById("tag-filter").value, 
    attr: document.getElementById("attr-filter").value, 
    val: document.getElementById("val-filter").value
};
    

El buscador deberá permitir el carácter comodín * para filtrar todos por ese campo. Implementaremos este buscador usando el método filter(). En el siguiente código propagamos la lista de nodos obtenida con getElementsByTagName("*"), pues al ser un array-like podemos convertirla en un Array y así aplicarle directamente filter(callback, thisArg). El siguiente código lo implementa:

let filtrados = [...document.getElementsByTagName("*")].filter(
function(elem){
    if (this && this.hasOwnProperty("tag") && 
    this.hasOwnProperty("attr") && 
    this.hasOwnProperty("val")){
        if (this.tag === "*" || 
        this.tag === elem.tagName.toLowerCase()){
            if ((this.attr === "*") && (this.val === "*")) {
                //Cualquier atributo y cualquier valor
                return true;
            } else if ((this.attr === "*") && (this.val !== "*")){
                //Cualquier atributo y un valor dado
                return [...elem.attributes].some(function(v){
                    return v.value === this.val;   
                }, this);
            } else if (elem.hasAttribute(this.attr)){
                //Un atributo dado y cualquier valor
                if (this.val === "*" || 
                this.val === elem.getAttribute(this.attr)){
                    return true;
                }
            }
        }
    }
}, filtro);
    

Como vamos a manejar this dentro de la función, ésta debe ser una expresión de función y no una función flecha. Pasamos en el argumento thisArg el objeto filtro obtenido desde los campos de texto. Ya dentro de la función aplicamos condicionales. El primero es ver si se está pasando thisArg, pues en otro caso el this será undefined. También comprobamos que ese objeto es como filtro y contiene las tres propiedades tag, attr y val.

Recordamos que dentro de la función el this se refiere al filtro, mientras que el elemento del DOM que estamos analizando lo tenemos en el argumento elem. Si filtramos todos los tag o si el tag del actual elemento es el que le pusimos al filtro podemos agregar este elemento si, a su vez, en atributo y valor del atributo hemos puesto también *, ignorándose atributos y valores. Observe que con tres * filtramos todos los elementos de la página.

Si permitimos cualquier atributo pero estamos filtrando por sus valores (primer else if), hemos de extraer todos los atributos del elemento actual y comprobarlo. Para ello propagamos elem.attributes en un Array para aplicar el método some(callback, thisArg). Éste método ,que veremos en el tema siguiente, devuelve verdadero si algún elemento cumple la condición del callback y falso en otro caso. También tiene un callback y un thisArg. Como necesitamos el valor del atributo que tenemos en el filtro para pasar el test de some(), incorporamos el filtro que está en el this de filter() al interior del método some(). Cuando el valor de un atributo coincida con el valor del filtro el método some() devolvera verdadero, lo que a su vez devolvemos al método exterior filter().

El último condicional else if es que tengamos en el filtro un determinado atributo, filtrando sea cual sea el valor del mismo. Comprobamos si el elemento tiene ese atributo con hasAttribute(), para luego comprobar, en su caso, si el valor coincide con getAttribute().

Al finalizar tendremos en filtrados un Array de elementos del DOM. Con ellos podríamos hacer algo, como pasarlos a una cadena HTML como una lista. Componemos un literal HTML de cada elemento como <tag attributos>, sin su contenido interior ni su tag de cierre. Vea como usamos el método map() explicado en este tema para componer una cadena de atributos y valores. Por último resaltamos el código con wxRC.resaltar().

let html = `<ol>`;
for (let elemento of filtrados){
    let tag = elemento.tagName.toLowerCase();
    let attribs = [...elemento.attributes].map(v => 
        `${v.name}="${v.value}"`).join(" ").trim();
    if (attribs) attribs = " " + attribs;
    let outer = `<${tag}${attribs}>`;      
    html += `<li>${wxRC.resaltar(outer, "html", "code")}</li>`;
}
html += `</ol>`;
document.getElementById("res-filter").innerHTML = html;
    

El ejemplo interaactivo con todo lo anterior es el siguiente:

Ejemplo: Ejemplo uso de filter() para filtrar DOM

Todos los campos deben contener algo. Para filtrar todos usar asterisco "*". Un valor vacío será reemplazado por "*".

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.