Wextensible

Usando el constructor Array()

Figura
Figura. El constructor Array

El constructor de Array es una función que tiene algunos métodos genéricos (o estáticos) que será interesante exponerlos en este tema. Uno de ellos es Array.isArray(obj) para comprobar si un objeto es un Array. El método Array.of() es de ES6 y viene a suplir el problema que existe con el constructor Array(), puesto que no nos permite crear un Array de un único elemento cuyo valor sea un entero no negativo.

El método Array.from() es también de ES6 a raíz de la inclusión de los objetos iterables. Estos implementan el símbolo [Symbol.iterator]. Desde un iterable podemos construir un Array con este método Array.from(iterable). Son iterables Array, String, Set, arguments y otros. Por otro lado un objeto que tenga claves numéricas correlativas empezando en cero y una propiedad length con el total de elementos también puede ser usado en Array.from(). Generalizamos a veces denominando a todos estos objetos como array-like, pues son como Array y pueden ser usados en muchos métodos tanto genéricos como del prototipo de Array.

El resto de métodos estarán disponible en el prototipo y los veremos con detalle en los temas siguientes, exponiéndose en el último apartado de este tema una lista de ellos.

Empecemos el tema viendo el constructor Array() que podemos usar para crear un nuevo Array en lugar de usar la notación literal. Los argumentos serán los elementos del Array. Observe como el método toString() aplicado a la variable que contiene el Array nos da un String representando los elementos separados por comas.

let arr = new Array(1, 2, 3);
console.log(arr[0]); // 1
console.log(arr[1]); // 2
console.log(arr[2]); // 3
console.log(arr.length); // 3
console.log(arr.toString()); // "1,2,3""
    

Un Array creado con notación literal como con el constructor Array() responderán al operador instanceof diciéndonos si es instancia de ese constructor.

let arr = new Array(1, 2, 3); // o literal [1, 2, 3]
console.log(arr instanceof Array); // true
    

Con ES6 podemos crear una clase que herede de Array, de tal forma que las instancias de esa clase serán de Array y de la clase al mismo tiempo:

class MiArray extends Array {}
let arr = new MiArray(1,2,3);
console.log(arr); // [1, 2, 3]
console.log(arr instanceof Array); // true
console.log(arr instanceof MiArray); // true
    

No hay ninguna diferencia entre un Array creado con notación literal o mediante el constructor. Sólo debemos tener en cuenta una cosa. Si sólo hay un argumento y es un número entero no negativo, el constructor creará un Array con la propiedad length igual ese argumento. Pero realmente no hay nada en el Array, como se observa en el siguiente código donde los índices 0 hasta 2, los tres de length, nos devuelven el valor undefined. Cuando una variable no contiene un valor, éste será el valor undefined. Vea como el toString() nos devuelve dos comas seguidas que separa tres valores vacíos, indicando que no hay nada en esas posiciones.

let arr = new Array(3);
console.log(arr.length); // 3
console.log(arr[0]); // undefined
console.log(arr[1]); // undefined
console.log(arr[2]); // undefined
console.log(arr.toString()); // ",,"
    

Si usa un número que no sea entero no negativo (el cero si es posible), nos dará un error de rango:

let arr = new Array(-1); // RangeError: Invalid array length
let arr = new Array(3.5); // RangeError: Invalid array length
    

Por lo tanto no es posible crear un Array con un único elemento que sea un número usando el constructor. Pero usando un único valor de otro tipo se creará un Array con ese único elemento. De paso observe que también podemos omitir la palabra clave new con estos constructores.

let arr = Array("a"); // También es posible omitir "new"
console.log(arr); // ["a"]
    

ES6 introduce el método genérico Array.of(), que hace lo mismo que el constructor Array(), pero solventa el problema de crear un Array con un único elemento que sea un número entero.

Siempre se recomienda crear valores usando literales en lugar de los constructores. Veamos si el motivo pudiera también estar relacionado con la eficiencia de JavaScript en el proceso de creación. En el siguiente ejemplo ejecutamos un bucle durante un tiempo determinado llevando a cabo la operación de creación de Array literales y con el constructor. Hay dos opciones: con números enteros y con cadenas.

while (new Date() - ini < tiempo){
    iteraciones++;
    if (literal) {
        let x = (numerico) ? [1, 2, 3] : ["a", "b", "c"];
    } else {
        let x = (numerico) ? Array(1, 2, 3) : Array("a", "b", "c");
    }
}
    

Lo anterior es una parte del código que ejecuta el siguiente ejemplo. Puede ver el código completo en el enlace al pie del ejemplo.

Ejemplo: Eficiencia notación literal vs constructor de Array

Array con elementos

Durante ese tiempo ejecutaremos un código para crear valores de Array literal

let x = ["a","b","c"]

y con el constructor

let x = Array("a","b","c")

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

Iteraciones (Cuanto mayor mejor)% mejora literal
s/ constructor
LiteralConstructorDiferencia
 
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

En los navegadores actuales Chrome 51 y Firefox 46 no se observan diferencias importantes entre crear un Array literal o usando el constructor.

Propiedades genéricas de Array

En el siguiente ejemplo podrá consultar las propiedades genéricas de Array() en el navegador que está usando.

Ejemplo: Propiedades genéricas de Array

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

El ejemplo anterior en Chrome 51 nos devuelve lo siguiente:

  1. arguments: object
  2. caller: object
  3. from: function
  4. isArray: function
  5. length: number
  6. name: string
  7. of: function
  8. prototype: object

El constructor Array() es una función. Las propiedades del constructor se les denomina genéricas o estáticas, para diferenciarlas de las que encontraremos en el prototipo. En las genéricas encontramos las propiedades típicas de una función: arguments, caller, length y name (puedes ver más en propiedades de las funciones).

En prototype encontraremos los métodos del prototipo, que veremos en el siguiente apartado. También estará en el prototipo otra propiedad length que no debemos confundir con la genérica. Ésta devuelve el número de parámetros de la función. El length del prototipo nos dará el total de elementos de un Array.

//La propiedad length de una función nos devuelve
//el número de parámetros que se declararon
function fun(a, b){};
console.log(fun.length); // 2
//Para los constructores built-in nos devuelve 1
console.log(String.length); // 1
console.log(Number.length); // 1
console.log(Array.length); // 1
//Para una instancia de un Array nos devuelve
//el número de elementos del Array
let arr = [1, 2, 3];
console.log(arr.length); // 3
    

Array.isArray(obj)

ES5

Comprueba sin un objeto es un Array. En el siguiente código se detecta el Array pero no el objeto que parece un Array.

console.log(Array.isArray(["a", "b"])); // true
console.log(Array.isArray({0: "a", 1: "b", length: 2})); // false
    

Saber el tipo de datos en JavaScript no siempre es fácil. Cuando son estructuras de datos como un Array, el valor de typeof siempre nos devuelve "object". Podríamos usar el nombre del constructor, lo que devuelve el método toString() o el operador instanceof.

let arr = [1, 2, 3];
console.log(typeof arr); // "object"
console.log(arr.constructor.name); // "Array"
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(arr instanceof Array); // true
    

Llevarse por el nombre del constructor no siempre tendrá que funcionar. En este ejemplo usamos las nuevas clases con extends que hace que un objeto herede de otro. Ahora se observa que el nombre del constructor no es "Array" sino "MiArray":

class MiArray extends Array{}
let arr = new MiArray(1,2,3);
console.log(arr.constructor.name); // "MiArray"
    

Utilizar Object.prototype.toString.call() no es del todo seguro, pues con los nuevos símbolos de ES6 ahora es posible modificar la cadena "[object Array]". En este ejemplo lo hacemos para que con cualquier Array generado obtengamos "[object ???]":

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

Por último con el operador instanceof tendremos un problema cuando consultamos Array desde distintos espacios globales. En el navegador el espacio global se identifica con la ventana por medio del objeto window. Cada espacio global tiene sus propias variables. Los objetos built-in como Array no son compartidos entre ventanas. Por lo tanto desde una ventana no podremos saber si un objeto de otra es o no un Array usando ese operador.

En el siguiente código creamos una nueva ventana w y un Array w.arr en ella usando su constructor w.Array(). Vemos que w.arr no es una instancia del constructor en la ventana actual, pero si lo es de la nueva ventana. El nuevo método isArray() viene a solventar estos problemas, pues determina claramente que w.arr es un Array.

let w = window.open("about:blank");
console.log(w === window); //false
w.arr = new w.Array(1,2,3);
//El instanceof se limita a cada ventana
console.log(w.arr instanceof Array); // false
console.log(w.arr instanceof w.Array); // true
//Pero isArray es independiente de la ventana
console.log(w.Array.isArray(w.arr)); // true
console.log(Array.isArray(w.arr)); // true
    

Array.of(elem0, elem1, ...)

ES6

El constructor Array() tiene un comportamiento extraño con los argumentos. Si hay más de un argumento creará un Array con tantos elementos como argumentos. Si hay un único argumento y no es un número entero no negativo, entonces también creará un Array con un elemento con ese valor. En otro caso tomará ese único argumento como longitud del Array que va a crear, con todos sus elementos sin valor.

Como no es posible crear un Array de un elemento con Array(N), ES6 introduce el método Array.of(N), pudiendo pasarse un único valor para crear un Array con un sólo elemento.

let arr1 = Array(3);
console.log(arr1); []
console.log(arr1.length); // 3

let arr2 = Array.of(3);
console.log(arr2); // [3]
console.log(arr2.length); //1
    

Array.from(arrayLike[, callback[, thisArg]])

ES6

Con Array.from() podemos crear un Array desde cualquier objeto iterable o que sea array-like. Los iterables implementan el símbolo [Symbol.iterator]. Los Array son obviamente iterables, pero también lo son otros objetos como String, Set y Arguments, tal como vemos en el siguiente código de ejemplo:

//Un conjunto es iterable
let set = new Set().add(1).add(2).add(3);
console.log(typeof set[Symbol.iterator]); // "function"
console.log(Array.from(set)); // [1, 2, 3]
//Un String es iterable
let str = "abc";
console.log(typeof str[Symbol.iterator]); // "function"
console.log(Array.from(str)); // ["a", "b", "c"]
//Arguments es iterable
function fun(){
    console.log(typeof arguments[Symbol.iterator]); // "function"
    return Array.from(arguments);
}
console.log(fun(1,2,3)); // [1, 2, 3]

    

Los array-like son objetos que tienen propiedades numéricas que pueden actuar como índices y una propiedad length con el número total de elementos. En el siguiente ejemplo creamos un objeto de esa forma, observándose que no dispone de [Symbol.iterator], pero aún así al ser algo parecido a un Array, el método from() es capaz de usarlo:

//Un objeto puede ser "array-like"
let obj = {0: "x", 1: "y", 2: "z", length: 3};
console.log(typeof obj[Symbol.iterator]); // "undefined"
console.log(Array.from(obj)); // ["x", "y", "z"]
    

La propiedad length en el objeto anterior es imprescindible para que el método from() devuelva el Array. Si esa propiedad no existe devolverá una Array vacío. Pero aún sin la propiedad length podemos hacer iterable cualquier objeto con índices numéricos si le agregamos una función generadora a su símbolo [Symbol.iterator]:

//Un objeto puede ser iterable con una función generadora
let obj = {0: "x", 1: "y", 2: "z",
    [Symbol.iterator]: function* () {
        let index = 0;
        while (this[index]) yield this[index++];
    }
};
console.log(typeof obj[Symbol.iterator]); // "function"    
console.log(Array.from(obj)); // ["x", "y", "z"]
    

Con destructuring podemos conseguir lo mismo que con este método from():

let str = "abc";
console.log([...str]); // ["a", "b", "c"]
    

Pero el desestructurado sólo se puede aplicar sobre objetos iterables. Al ejemplo del objeto array-like no podemos aplicar esta técnica por que no tiene un método [Symbol.iterator]:

let obj = {0: "x", 1: "y", 2: "z", length: 3};
console.log([...obj]); // obj[Symbol.iterator] is not a function
    

Este método Array.from(arrayLike[, callback[, thisArg]]) tiene los dos últimos argumentos opcionales que equivalen a aplicar el método map() tras la creación del Array. Sería como hacer Array.from(iterable).map(callback, thisArg). La ventaja de usar los argumentos opcionales es que no creamos un Array intermedio. En el siguiente código creamos el Array a partir de las letras de un String, pero se devuelven el código ASCII de cada letra:

let str = "abc";
let arr = Array.from(str, v => v.charCodeAt(0));
console.log(arr); // [97, 98, 99]
    

El callback también se podría aplicar a un array-like. Como vimos más arriba, el objeto debe tener una propiedad length. Podemos rellenar un Array dando una objeto con una determinada longitud:

console.log(Array.from({length: 3}, (v, i) => i)); // [0, 1, 2]
    

Ver más cosas sobre from() y map().

Propiedades del prototipo de Array

En el ejemplo siguiente podrá consultar las propiedades del prototipo de Array en el navegador que esté usando.

Ejemplo: El prototipo de Array

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

En Chrome 51 obtenemos 29 métodos del prototipo de Array, el constructor y length, que ya vimos que es donde se almacena la longitud de un Array. Agruparemos los métodos según su finalidad para estudiarlos mejor. Cada apartado se presenta en un página distinta.