Métodos que devuelven un valor transformado desde un Array

Figura
Figura. El método join(separador) transforma un Array en un String encadenando sus elementos con el separador.

Los métodos de este tema transforman un Array en un valor simple devolviéndolo y no modificando el Array sobre el que actuan. Incluye los métodos antiguos join(), toString() y toLocaleString() que devuelven un String con los elementos del Array.

Más interesante son los métodos reduce() y reduceRight(). Ambos funcionan igual, sólo que el primero itera de izquierda a derecha en los índices del Array y el segundo lo hace al revés. Usa un callback para ir acumulando el resultado, por lo que es un método que tiene un gran número de aplicaciones.

  • Métodos para modificar el propio Array
  • Métodos para iterar por un Array
    • keys para obtener un iterador de Array para iterar por los índices.
    • values para obtener un iterador de Array para iterar por los valores.
    • entries para obtener un iterador de Array para iterar por parejas índice-valor.
    • forEach para iterar por un Array con una función callback.
  • Métodos para crear nuevos Array
  • Métodos para buscar en un Array
  • Métodos que devuelven un valor transformado desde un Array
    • join transforma un Array devolviendo un String concatenando sus elementos con el String del argumento.
    • reduce transforma un Array en un valor final simple mediante un callback. Exponenciación rápida con el método reduce().
    • reduceRight transforma un Array en un valor final simple mediante un callback, iterando desde el final del Array hacia el principio.
    • toString transforma un Array en un String concatenando sus elementos con una coma.
    • toLocaleString transforma un Array en un String concatenando sus elementos en formato local con una coma.

arr.join([separador])

ES1

El método join() transforma el Array en un String concatenando sus elementos con el argumento separador. Si no se especifica se usa una coma como separador.

let arr = [1, 2, 3, 4]; 
//join() y toString() producen el mismo String
console.log(arr.join()); // "1,2,3,4"
console.log(arr.toString()); // "1,2,3,4"
//Poniendo un espacio después de la coma
console.log(arr.join(", ")); // "1, 2, 3, 4"
//Un Array de String representado como código
arr = ["a", "b", "c"];
console.log(`["${arr.join('", "')}"]`); // '["a", "b", "c"]'
    

Los elementos que sean objetos se presentan como [object Object]. Realmente join() aplica toString() a cada elemento antes de encadenarlos con el separador. Observe como el Array interior resulta separado por efecto del método toString(), no por el join() exterior:

let arr = [1, "a", false, [2, 3], {a: 4}];
console.log(arr.join()); // "1,a,false,2,3,[object Object]"
let arrStr = arr.map(v => v.toString());
console.log(arrStr); // ["1", "a", "false", "2,3", "[object Object]"]
console.log(arrStr.join("#")); // "1#a#false#2,3#[object Object]"
    

Los valores null y undefined así como los huecos del Array se quedan como cadenas vacías:

arr = [1, , 3, null, 4, undefined, 5];
console.log(arr.join()); // "1,,3,,4,,5" 
    

Este método join() se suele utilizar para encadenar texto (Ver más sobre esto en Literales de plantilla para texto multilínea):

let a = ["Lorem ipsum ad his scripta blandit ",
        "partiendo, eum fastidii accumsan ",
        "euripidis in, eum liber hendrerit an."].join("\n"); 
    

El método join() solía usarse para repetir un String un número de veces. Ahora también puede hacerse con el método repeat() de String:

console.log(Array(5+1).join("x")); // "xxxxx"
console.log("x".repeat(5)); // "xxxxx"
    

Podemos aplicar join() sobre un array-like usando call():

let obj = {0: "a", 1: "b", length: 2};
console.log([].join.call(obj, "_")); "a_b"
    

arr.reduce(callback[, valorInicial])

ES5

El método reduce() transforma un Array en un valor final simple, tomando un callback con los argumentos valor previo, y valor actual para operar con ellos y poner el resultado en el valor previo, que viene a ser como un acumulador. En este ejemplo lo usamos para calcular el producto de los elementos del Array.

function callback(previo, valor){
    return previo*valor;
}
let arr = [1, 2, 3, 4, 5, 6];
console.log(arr.reduce(callback)); // 720 
    

Los argumentos del callback son el valor previo, el valor actual, el índice actual y una referencia al Array. Lo podemos recordar por p,v,i,a. Además reduce() tiene el segundo argumento opcional valorInicial. Es el valor que tomará el argumento valor precio del callback en la primera iteración. Si no se suministra se le dará el valor del primer elemento.

En este ejemplo usamos todo eso para presentar un Array como si fuera texto de código. Si un elemento es a su vez un Array llamamos recursivamente al callback.

//Para presentar un Array como código, pero sólo con tipos 
//primitivos y en su caso Array también con estos tipos
function arrayToString(p, v, i, a){
    let cad = "";
    let tipo = v.constructor.name;
    if (tipo==="String"){
        cad = `"${v}"`;
    } else if (tipo==="Array"){
        cad = `${v.reduce(arrayToString, "[")}`;
    } else {
        cad = v;
    }
    cad = `${p}${cad}`;
    if (i < a.length-1) {
        cad += ", ";
    } else {
        cad += "]";
    }
    return cad;
}
let arr = [1, "A", true, [2, "B", [3, 4], false]];
//El toString() o el join() no presentan el Array como código
console.log(arr.toString()); // 1,A,true,2,B,3,4,false
console.log(arr.join()); // 1,A,true,2,B,3,4,false
//Usando esta función si lo hace
console.log(arr.reduce(arrayToString, "[")); 
// [1, "A", true, [2, "B", [3, 4], false]]

    

El algoritmo de ordenamiento de la burbuja es el más simple y a la vez el menos eficiente, con un coste de O(n2). Con objeto de usar otro ejemplo para el método reduce(), vamos a aplicarlo para implementar ese algoritmo. Se trata de comparar un elemento con el anterior y si es menor los intercambiamos. Para ello usaremos el destructuring para intercambiar variables.

El método reduce() va operando el valor inicial false con la condición verdadera cuando hay intercambio. Entonces ejecutamos reduce() hasta que nos devuelva falso, en cuyo momento estará el Array ordenado.

function ordenarBurbuja(arr){
    while (arr.reduce((p,v,i,a) => {
        if (i>0) {
            let menor = v<a[i-1];
            if (menor) {
                //Usando destructuring para 
                //intercambiar variables
                [arr[i-1], arr[i]] = [v, a[i-1]];
            }
            return p || menor;
        }        
    }, false)){}
    return arr;
}
let arr = [5, 3, 8, 1];
console.log(ordenarBurbuja(arr)); // [1, 3, 5, 8]
    

Observe que en la variable a tenemos el Array inicial congelado en el momento de la llamada a reduce(). Mientras que arr alcanza al Array externo, donde realizaremos los cambios.

Exponenciación rápida con el método reduce()

El método reduce() puede hacer tantas cosas como posibilidades de cálculo permitan sus elementos. En este apartado expondré un caso de uso de reduce(), que aunque poco práctico, puede ser interesante para entender que muchas veces podemos mejorar el coste de cómputo aplicando algunos cambios a nuestros programas.

Este año ya se publicó la versión 7 de EcmaScript, ES7. Sólo tiene un par de novedades, una de ella es el operador exponenciación. Su sintaxis es b**n que produce el mismo resultado que Math.pow(b, n). Por otro lado en estos días he visto un artículo de John D.Cook sobre exponenciación rápida. Me pareció interesante intentar implementarlo con JavaScript, viendo que puede hacerse con sólo una línea de código usando el método reduce:

function exp(b, n){
    return [...n.toString(2)].reduce((p,v) => (v==1) ? p*p*b : p*p, 1);
}
console.log(exp(7, 0)); // 1
console.log(exp(7, 1)); // 7
console.log(exp(7, 2)); // 49
console.log(exp(7, 13)); // 96889010407
    

Ese algoritmo tiene un coste logarítmico. Si utilizáramos un bucle for el coste sería lineal, pues necesitaríamos hacer n iteraciones, contemplando también el caso de exponente cero.

function exp(b, n){
    let rFor = 1;
    for (let i=0; i<n; i++){
        rFor = rFor*b;
    }
    return rFor;
}
console.log(exp(7, 0)); // 1
console.log(exp(7, 1)); // 7
console.log(exp(7, 2)); // 49
console.log(exp(7, 13)); // 96889010407
    
Figura
Figura. Gráfica de y = 2 log2n e y = n.

El problema planteado es que la exponenciación tiene un coste de computación importante, especialmente cuando el exponente es grande. Para calcular bn mediante un bucle for necesitamos hacer n multiplicaciones. Si el exponente es grande la solución para reducir el coste sería convertirlo en un número binario haciendo como mucho O(2 log2 n) multiplicaciones. Veáse en la gráfica que esa función (rojo) está significativamente por debajo de y = n (azul) para valores grandes.

Para obtener el exponente en binario usaremos el método exponente.toString(2). El número de dígitos binarios de un entero no negativo n es el resultado de la aproximación entera floor(1 + log2 n), pero podemos usar el valor real log2 n a efectos de comparar el coste.

El String con los dígitos binarios del exponente lo propagamos a un Array y le aplicamos reduce(). Ese Array tiene de longitud el número de dígitos binarios, por lo que se deduce que el coste será de orden logarítmico. Para 2 no obtenemos ninguna ventaja, es más, realizaremos más cálculos que si hacemos directamente 7×7. Pero para el exponente 13 su binario es 1101. En lugar de multiplicar 13 veces solo realizaremos 5 multiplicaciones, tal como se observa en el siguiente esquema y despreciando el coste de las multiplicaciones por uno en la primera iteración.

Base: b = 7, 
Dígito binario: v del exponente 13 (1101)
Calculamos 7^13
Inicializamos p=1 y acumulamos
----------------------------------------
p        v       (v==1) ? p*p*b : p*p
----------------------------------------
1        1       1*1*7 = 7
7        1       7*7*7 = 343
343      0       343*343 = 117649
117649   1       117649*117649*7 = 96889010407
    

Considerando que una multiplicación tiene un coste unitario a efectos de obtener un orden del coste, el mejor caso es para un binario con un uno y resto con ceros, con un coste O(log2 n) al realizar una única multiplicación en cada iteración (despreciando la primera iteración). Y el peor cuando todos los dígitos son unos, coste O(2 log2 n), pues tendríamos que hacer dos multiplicaciones en cada iteración.

Usar ese algoritmo en JavaScript no es más eficiente que utilizar Math.pow(b, n) o el nuevo operador b**n, pues esas operaciones se realizan con lenguaje nativo. Pero si lo comparamos con un bucle en JavaScript que realice las multiplicaciones, la mejora tiene que ser significativa. Hagámos un test de eficiencia para comprobarlo.

En primer lugar debemos tener en cuenta que el uso de reduce() incrementará el coste. Por eso haremos también las pruebas con la versión del algoritmo binario usando un bucle:

let binario = [...n.toString(2)].map(v => (v==="1"));
let rBin = 1;
for (let i=0; i<binario.length; i++){
    rBin = (binario[i]) ? rBin*rBin*b : rBin*rBin;
}        
    

Para no tener que convertir el exponente en binario en cada repetición de la prueba lo salvamos previamente. Además usando el método map() convertimos los dígitos binarios que aparecen como caracteres "0" o "1" a valores booleanos false y true. Así también reducimos el coste al realizar una comparación directa con un booleano. Por otro lado la versión con reduce() quedaría así:

let binario = [...n.toString(2)].map(v => (v==="1"));
let rBin = binario.reduce((p, v) => (v) ? p*p*b : p*p, 1);
    

A título comparativo en las condiciones que estoy haciendo esta prueba se obtienen estos resultados, donde se observa claramente que el algoritmo binario en un bucle consigue mejores resultados, siempre realizando las comparaciones con el algoritmo de coste lineal. Usando reduce() es en torno a un exponente 35 cuando el coste comparado con el lineal se igualan, apreciándose a partir de ahí una significativa mejora del binario.

Cálculo% mejora binario sobre lineal
Binario con bucleBinario con reduce()
1312%-19%
3550%0%
70120%40%
100180%80%

Ejemplo: Eficiencia del algoritmo binario de exponenciación rápida

Durante ese tiempo ejecutaremos un algoritmo lineal con un bucle for con las n multiplicaciones:

let rFor = b;
for (let i=0; i<n; i++){
    rFor = rFor * b;
}
        

Y un algoritmo de exponente binario usando

let binario = [...n.toString(2)].map(v => (v==="1"));
let rBin = binario.reduce((p, v) => (v) ? p*p*b : p*p, 1);
        
let binario = [...n.toString(2)].map(v => (v==="1"));
let rBin = 1;
for (let i=0; i<binario.length; i++){
    rBin = (binario[i]) ? rBin*rBin*b : rBin*rBin;
}        
        

Cuantas más iteraciones hagamos más eficiente será el proceso.

7 13
Lineal:
Binario:
Iteraciones (Cuanto mayor mejor)% mejora Binario
s/ Bucle
Binario con
reduce()
Lineal con
bucle for
Diferencia
 
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

arr.reduceRight(callback[, valorInicial])

ES6

El método reduceRight() transforma un Array en un valor final simple, tomando un callback con los argumentos valor previo, y valor actual para operar con ellos en la dirección derecha a izquierda del Array, poniendo el resultado en el valor previo, que viene a ser como un acumulador. Funciona igual que el método reduce(), donde la dirección de iteración era izquierda a derecha. En el siguiente código se observa que no hay diferencia en el resultado usando ambos métodos al sumar todos los elementos de un Array:

function sumar (p, v){
    return p+v;
}
let arr = [8, 24, -3, 69, 5];
console.log(arr.reduce(sumar)); // 103
console.log(arr.reduceRight(sumar)); // 103
    

El método reduceRight() nos podría devolver el Array en orden inverso, como hacemos con reverse(). En este ejemplo hacemos uso del argumento opcional valorInicial con un Array vacío. En cada iteración le agregamos un elemento y devolvemos ese Array:

function callback(p, v){
    p.push(v);
    return p;
}
let arr = [1, 2, 3, 4, 5];
console.log(arr.reduce(callback, [])); // [1, 2, 3, 4, 5]
console.log(arr.reduceRight(callback, [])); // [5, 4, 3, 2, 1]
console.log(arr.reverse()); // [5, 4, 3, 2, 1]
    

arr.toString()

ES1

El método toString() transforma el Array en un String encadenando los elementos con una coma. El resultado es el mismo que usando el método join() con una coma como separador. Se observa que los elementos que son a su vez Array también se les aplica el método. Los objetos se presentan como [object Object].

let arr = [123, "abc", true, [4, [5, 6]], {a: 6}];
console.log(arr.toString()); // 123,abc,true,4,5,6,[object Object]
console.log(arr.join(",")); // 123,abc,true,4,5,6,[object Object]
    

El método toString() está en el prototipo de Array. Podemos modificarlo para que saque otra cosa:

console.log(Array.prototype.toString); // function toString()
Array.prototype.toString = function() {
    return `Array [${this.join(", ")}]`;
}
let arr = [1, 2, 3];
console.log(arr.toString()); // Array [1, 2, 3]
    

El método toString() se establece originalmente en Object. El resto de objetos heredan los métodos de Object y, en algunos casos como en Array, los sobrescriben. Si llamamos al método original sobre un Array nos devolverá [object Array].

let arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3
//También con Object.prototype.toString.call(arr)
console.log({}.toString.call(arr)); // [object Array]
    

Lo anterior ha venido sirviendo para saber si un objeto es un Array. Sin embargo con ES6 se introduce el símbolo bien conocido Symbol.toStringTag que nos permite cambiar ese comportamiento. En el siguiente código cambiamos Array por MI ARRAY. Esto no afectará al método sobre la instancia, pues sobrescribe el de Object.

Array.prototype[Symbol.toStringTag] = "MI ARRAY";
let arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3
console.log({}.toString.call(arr)); // [object MI ARRAY]
    

arr.toLocaleString()

ES3

El método toLocaleString() transforma el Array en un String encadenando los elementos con un coma y aplicándoles el formato local para números y fechas. Utiliza los métodos toLocaleString() de Number y Date. Convierte el número 1335.99 en la cadena "1.335,99" si lo estoy usando con un sistema configurado con punto para separar miles y coma para separar decimales. Mientras que las fechas se presentan con el formato d/m/aaaa si el sistema tiene esa configuración local.

//Conversión en formato local de números y fechas
let num = 1335.99;
console.log(num.toString()); // 1335.99
console.log(num.toLocaleString()); // 1.335,99
let fecha = new Date(2015, 11, 31); // argumentos: año, mes-1, día
console.log(fecha.toString()); // Thu Dec 31 2015 00:00:00 GMT+0000 (Hora estándar GMT)
console.log(fecha.toLocaleString()); // 31/12/2015 0:00:00
    

En un Array hace lo mismo. Sólo hay que tener en cuenta que Chrome 51 necesita el elemento numérico como un objeto con new Number(1335.99) para aplicar el formato local de número. Firefox 47 sin embargo lo hace sobre un literal de número.

//En Chrome 51 los números deben ser objetos instanciados con new(Number) 
//para una correcta conversión a formato local. En Firefox 47 no es necesario.
let arr = [1335.99, new Number(1335.99), new Date(2015, 11, 31)];
console.log(arr.toString()); // 1335.99,1335.99,Thu Dec 31 2015 00:00:00 GMT+0000 (Hora estándar GMT)
console.log(arr.toLocaleString()); // 1335.99,1.335,99,31/12/2015 0:00:00