Seleccionar página

El Array es una de las estructuras más utilizadas en Javascript y no siempre bien comprendida. Os invitamos a analizar el comportamiento interno de los objeto Array, es decir, cómo
Javascript implementa las diferente acciones con Array y que operaciones internas se realizan en cada caso.

En otro momento ya analizamos los patrones y confusiones en el uso de los Array y vimos sus diferentes modelos de funcionamiento. En esta ocasión nos vamos a centrar en su comportamiento interno, descubriendo algunos de sus secretos. Esto nos va a permitir conocer mucho mejor cómo trabajan por dentro las matrices y cómo podemos adaptar su funcionamiento a nuestros intereses.

Para poder descubrir el comportamiento interno de los objetos hemos desarrollado esta pequeña función que captura de todas las acciones que se realizan en el objeto por medio de un Proxy. El objeto Proxy nos permite inspeccionar en profundidad el comportamiento de los objetos, ya que dispone de mecanismos de captura bastante completos. Como consecuencia, nos ofrece una interesante oportunidad para comprender el funcionamiento interno de algunos de los elementos de Javascript que están basados en objetos.

Vamos a dar un repaso en profundidad a todas las operaciones y métodos de los Array. Para ello vamos a aplicar esta función de inspección sobre la matriz y podremos ver el comportamiento interno de cada una de las acciones. Nota: la función inspect() imprime por consola una representación conceptual de las operaciones realizadas, no el código que se ha ejecutado internamente.

Este artículo es bastante extenso, aunque es un contenido bastante esquemático. Si tienes especial interés en conocer el comportamiento de un método u operación concreta, utiliza la tabla de contenido para ir directamente a esta sección.

const inspect = (function () {

  const MARGIN = '  ';
  let stack    = 0;
  const indent = {
    toString : () => MARGIN.repeat (stack)
  };

  function internals (obj, log = console.log) {
    return new Proxy (obj, {
      apply: (target, thisArgument, argumentsList) => {
        log (indent + 'apply()');
        return call (() => Reflect.apply (target, thisArgument, argumentsList));
      },
      construct: (target, argumentsList, newTarget) => {
        log (indent + 'construct:', argumentsList);
        return call (() => Reflect.construct (target, argumentsList, newTarget));
      },
      defineProperty: (target, prop, attributes) => {
        log (indent + `defineProperty (${ format (prop) }, ${ format (attributes) })`);
        return call (() => Reflect.defineProperty (target, prop, attributes));
      },
      deleteProperty: (target, prop) => {
        log (indent + `deleteProperty (${ format (prop) })`);
        return call (() => Reflect.deleteProperty (target, prop));
      },
      get: (target, prop, receiver) => {
        const result = call (() => Reflect.get (target, prop, receiver));
        log (indent + `get (${ format (prop) }) => ${ format (result) }`);
        if (typeof result === 'function') {
          return new Proxy (result, {
            apply : (target, thisArgument, argumentsList) => {
              log (indent + `apply (${ format (prop) }, ${ format (argumentsList) })`);
              return call (() => Reflect.apply (target, thisArgument, argumentsList));
            }
          });
        }
        return result;
      },
      getOwnPropertyDescriptor: (target, prop) => {
        const result = call (() => Reflect.getOwnPropertyDescriptor (target, prop));
        log (indent + `getOwnPropertyDescriptor (${ format (prop) }) => ${ format (result) }`);
        return result;
      },
      getPrototypeOf: (target) => {
        log (indent + 'getPrototypeOf ()');
        return call (() => Reflect.getPrototypeOf (target));
      },
      has: (target, prop) => {
        const result = call (() => Reflect.has (target, prop));
        log (indent + `has (${ format (prop) }) => ${ result }`);
        return result;
      },
      isExtensible: (target) => {
        log (indent + 'isExtensible:');
        return call (() => Reflect.isExtensible (target));
      },
      ownKeys: (target) => {
        log (indent + 'ownKeys ()');
        return call (() => Reflect.ownKeys (target));
      },
      preventExtensions: (target) => {
        log (indent + 'preventExtensions:');
        return call (() => Reflect.preventExtensions (target));
      },
      set: function (target, prop, value, receiver) {
        log (indent + `set (${ format (prop) },${ format (value) })`);
        return call (() => Reflect.set (target, prop, value, receiver));
      },
      setPrototypeOf: (target, prototype) => {
        log (indent + 'setPrototypeOf:');
        return call (() => Reflect.setPrototypeOf (target, prototype));
      }
    });

  }

  function call (callback) {
    stack++;
    const result = callback ();
    stack--;
    return result;
  }

  function format (v) {
    switch (typeof v) {
      case 'string':
        return `"${ v }"`;
      case 'symbol':
        return v.description ? v.description : 'Symbol';
      case 'undefined':
        return 'undefined';
      case 'object':
        const replacer = (key, value) => {
          return typeof value === 'function' ? '#' + value.toString () + '#' : value;
        };
        let result     = JSON.stringify (v, replacer);
        if (result.length > 42) {
          result         = JSON.stringify (v, replacer, indent + MARGIN);
          const lastLine = result.lastIndexOf ('\n') + 1;
          result         = result.substring (0, lastLine) + MARGIN + result.substring (lastLine);
        }
        return result.replace ('"#', '').replace ('#"', '').replace (/\\"/g, '"');
      default:
        return v;
    }
  }

  return internals;
}) ();

Hemos ejecutado cada uno de los ejemplos en Chrome, Firefox, Edge y Node. No hay diferencias significativas entre los diferentes entornos, ya que -en general- las operaciones que realizan internamente están normalizadas en el estándar EcmaScript.

Operaciones básicas con Array

Lectura

Por medio de [indice] podemos obtener un valor de un elemento del Array, es algo que hacemos prácticamente sin pensar, ya que es una de las operaciones más básicas del lenguaje.

Ejemplo
const a = inspect (['a','b','c','d','e']);
let x = a[0];
Inspección
get ("0") => "a"

Como se puede observar internamente se llama a get para realizar esta operación de acceso a las propiedades, que devuelve el valor del elemento dentro del Array.

Asignación

Con [indice] y el operador = se puede cambiar un valor de un elemento del Array. Igual que en el caso anterior, es algo que hacemos prácticamente sin pensar, ya que es una de las operaciones más básicas del lenguaje.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a[0] = 'x';

como consecuencia de esta ejecución hemos modificado nuestro Array, que ahora tendrá estos valores:

a => ["x","b","c","d","e"]
a.length => 5
Inspección
set ("0","x")
  getOwnPropertyDescriptor ("0") => {
    "value": "a",
    "writable": true,
    "enumerable": true,
    "configurable": true
  }
  defineProperty ("0", {"value":"x"})

Esta operación internamente ha llamado a set para realizar la asignación, y esta operación interna a su vez ha llamado a getOwnPropertyDescriptor y a defineProperty para hacer efectivo el cambio de valor. Cuando se produce esta anidación de operaciones internas nuestra función de inspección añade una tabulación para dejar clara que son llamadas subordinadas unas a otras.

.length

La propiedad .length corresponde al tamaño del Array, y puede utilizarse tanto para consultarlo, como también para cambiarlo. Vamos a ver las diferentes operaciones:

Ejemplo

Para consultar el tamaño del Array utilizaremos algo como:

const a = inspect (['a','b','c','d','e']);
console.log (a.length);
Inspección
get ("length") => 5

Al consultar el valor de la propiedad .length estamos internamente llamado a get, como cuando consultábamos los valores del Array. Esto es así porque .length es una propiedad más y los elementos también.

Ejemplo

Al aumentar el valor de length aumenta el tamaño del Array con valores no definidos (undefined). Aunque no es un método muy habitual, se puede utilizar sin demasiados problemas:

const a = inspect (['a','b','c','d','e']);
a.length = 7;

Como resultado de esta operación nuestro Array queda de esta forma:

a => ["a","b","c","d","e",undefined,undefined]
a.length => 7
Inspección
set ("length",7)
  getOwnPropertyDescriptor ("length") => {
    "value": 5,
    "writable": true,
    "enumerable": false,
    "configurable": false
  }
  defineProperty ("length", {"value":7})

Como se puede comprobar, internamente se ha llamado a set, que a su vez llama a getOwnPropertyDescriptor y a defineProperty para hacer efectivo el cambio de valor.

Ejemplo

La operación contraría, es decir, reducir el valor de length, reduce el tamaño del Array eliminando los valores por encima del valor indicado.

const a = inspect (['a','b','c','d','e']);
a.length = 4;

Como resultado obtendremos:

a => ["a","b","c","d"]
a.length => 4
Inspección
set ("length",4)
  getOwnPropertyDescriptor ("length") => {
    "value": 5,
    "writable": true,
    "enumerable": false,
    "configurable": false
  }
  defineProperty ("length", {"value":4})

Internamente la operación es prácticamente igual que la anterior, se ha llamado a set, que a su vez llama a getOwnPropertyDescriptor y a defineProperty para hacer efectivo el cambio de valor de la propiedad.

delete

La instrucción delete permite borrar un elemento del Array sin cambiar su tamaño. El elemento borrado queda como undefined.

Ejemplo

const a = inspect (['a','b','c','d','e']);
delete a[0];

resultado el array tendrá estos valores

a => [undefined,"b","c","d","e"]
a.length => 5
Inspección
deleteProperty ("0")

Como podemos comprobar lo que se ha realizado internamente es el borrado de una propiedad. Como los Array tienen un comportamiento singular, su tamaño no se ha reducido por esta operación, quedando el hueco, en lo que se denomina un Sparse Array o matriz dispersa.

instanceof

La instrucción variable instanceof Array se utiliza para comprobar si una variable es un instancia del objeto Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a instanceof Array;
Inspección
getPrototypeOf ()

Como podemos comprobar se hace internamente una llamada al getPropotypeOf() para obtener el prototipo del objeto y de esta forma comprobar si es una instancia del objeto Array.

Métodos de manejo del contenido del Array

.pop()

El método .pop() devuelve el último elemento del Array y lo elimina, reduciendo el tamaño de la matriz.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.pop();

el resultado de esta ejecución es

a => ["a","b","c","d"]
a.length => 4
Inspección
get ("pop") => function pop() { [native code] }
apply ("pop", [])
  get ("length") => 5
  get ("4") => "e"
  deleteProperty ("4")
  set ("length",4)
    getOwnPropertyDescriptor ("length") => {
      "value": 5,
      "writable": true,
      "enumerable": false,
      "configurable": false
    }
    defineProperty ("length", {"value":4})

Como se puede observar, las operaciones que se han realizado internamente son bastantes:

  • se ha obtenido el valor de pop, que en este caso es una función.
  • se ejecuta la función:
    • comprueba el tamaño del Array
    • obtiene el valor de su último elemento (lo tiene que devolver)
    • borra el último elemento
    • cambiar el valor de length

Esto no quiere decir que sea una operación muy costosa, pero sí que tiene varios pasos internos que deben ejecutarse para completar la operación.

.push()

El método .push(element1, [element2], […]) añade nuevos elementos al final del Array. Puede recibir varios valores como parámetros.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.push('f');

el resultado de esta ejecución es:

a => ["a","b","c","d","e","f"]
a.length => 6
Inspección
get ("push") => function push() { [native code] }
apply ("push", ["f"])
  get ("length") => 5
  set ("5","f")
    getOwnPropertyDescriptor ("5") => undefined
    defineProperty ("5", {
      "value": "f",
      "writable": true,
      "enumerable": true,
      "configurable": true
    })
  set ("length",6)
    getOwnPropertyDescriptor ("length") => {
      "value": 6,
      "writable": true,
      "enumerable": false,
      "configurable": false
    }
    defineProperty ("length", {"value":6})

Como se puede observar, las operaciones que se han realizado internamente son bastantes.

  • se ha obtenido el valor de push, que en este caso es una función.
  • se ejecuta la función:
    • se obtiene el valor de length
    • se asigna un valor por encima del tamaño del Array
    • se cambia el valor de length

Igual que en el caso anterior, esto no quiere decir que sea una operación muy costosa, pero sí que tiene varios pasos internos que deben ejecutarse para completar la operación.

.shift()

El método .shift() obtiene el primer elemento del Array y lo elimina, reasignando todos los elementos a su nueva posición, cambiando el tamaño del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.shift();

como resultado el Array queda de esta forma

a => ["b","c","d","e"]
a.length => 4
Inspección
get ("shift") => function shift() { [native code] }
apply ("shift", [])
  get ("length") => 5
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  set ("0","b")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"b"})
  has ("2") => true
  get ("2") => "c"
  set ("1","c")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"c"})
  has ("3") => true
  get ("3") => "d"
  set ("2","d")
    getOwnPropertyDescriptor ("2") => {
      "value": "c",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("2", {"value":"d"})
  has ("4") => true
  get ("4") => "e"
  set ("3","e")
    getOwnPropertyDescriptor ("3") => {
      "value": "d",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("3", {"value":"e"})
  deleteProperty ("4")
  set ("length",4)
    getOwnPropertyDescriptor ("length") => {
      "value": 5,
      "writable": true,
      "enumerable": false,
      "configurable": false
    }
    defineProperty ("length", {"value":4})

Como se puede observar, las operaciones que se han realizado internamente son muchas, básicamente lo que ha ocurrido es:

  • se ha obtenido el valor de shift, que en este caso es una función.
  • se ejecuta la función:
    • comprueba el tamaño del Array
    • obtiene el valor del primer elemento (lo tiene que devolver)
    • lee el valor del siguiente elemento y lo asigna a la posición anterior y así hasta el final
    • borra el último elemento
    • cambiar el valor de length

Esta es una operación bastante costosa, que depende del número de elementos. Aunque se resuelve a una gran velocidad, las operaciones internas son muchas y esto, en general, se nota.

.unshift()

El método .unshift(element1, [element2], […]) añade nuevos elementos al principio del Array, modificando su tamaño.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.unshift('x');

el resultado es

a => ["x","a","b","c","d","e"]
a.length => 6
Inspección
get ("unshift") => function unshift() { [native code] }
apply ("unshift", ["x"])
  get ("length") => 5
  has ("4") => true
  get ("4") => "e"
  set ("5","e")
    getOwnPropertyDescriptor ("5") => undefined
    defineProperty ("5", {
      "value": "e",
      "writable": true,
      "enumerable": true,
      "configurable": true
    })
  has ("3") => true
  get ("3") => "d"
  set ("4","d")
    getOwnPropertyDescriptor ("4") => {
      "value": "e",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("4", {"value":"d"})
  has ("2") => true
  get ("2") => "c"
  set ("3","c")
    getOwnPropertyDescriptor ("3") => {
      "value": "d",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("3", {"value":"c"})
  has ("1") => true
  get ("1") => "b"
  set ("2","b")
    getOwnPropertyDescriptor ("2") => {
      "value": "c",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("2", {"value":"b"})
  has ("0") => true
  get ("0") => "a"
  set ("1","a")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"a"})
  set ("0","x")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"x"})
  set ("length",6)
    getOwnPropertyDescriptor ("length") => {
      "value": 6,
      "writable": true,
      "enumerable": false,
      "configurable": false
    }
    defineProperty ("length", {"value":6})

De forma similar a la anterior, las operaciones que se han realizado internamente son muchas, básicamente lo que ha ocurrido es:

  • se ha obtenido el valor de unshift, que en este caso es una función.
  • se ejecuta la función:
    • comprueba el tamaño del Array
    • obtiene el valor del ultimo elemento y se añade en la siguiente posición, y así hasta el inicio
    • cambiar el valor de length

Nuevamente, esta es una operación bastante costosa, que depende del número de elementos. Tanto shift como unshift deben considerarse operaciones complejas.

.concat()

El método .concat(value1, [value N]) crea una nuevo Array con sus valores y aquellos que se pasan como parámetro. La matriz original no se ve modificada.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.concat('f','g');

obtendremos un nuevo Array con este contenido:

["a","b","c","d","e","f","g"]
Inspección
get ("concat") => function concat() { [native code] }
apply ("concat", ["f","g"])
  get ("constructor") => function Array() { [native code] }
  get (Symbol.isConcatSpreadable) => undefined
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

Debemos recordar que estamos inspeccionando el Array original, no las operaciones necesarias para crear la nueva matriz. En este caso básicamente lo que ocurre es que:

  • se obtiene el método
  • se ejecuta:
    • se obtiene el constructor para crear un nuevo objeto del mismo tipo, en este caso, un Array.
    • se comprueba si el objeto dispone de Symbol.isConcatSpreadable: si este valor fuera false se evitaría el aplanamiento del valor pasado por parámetro y se consideraría todo él como un único valor.
    • se leen todos los valores para poderlos asignar en el Array de salida.

.slice()

El método .slice([start], [end]) devuelve una parte del Array en una nueva matriz, sin modificar el contenido original.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.slice(1, 3);

obtendremos como resultado una nueva matriz con los valores

["b","c"]
Inspección
get ("slice") => function slice() { [native code] }
apply ("slice", [1,3])
  get ("length") => 5
  get ("constructor") => function Array() { [native code] }
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"

Las operaciones en el Array original son muy sencillas, simplemente se obtiene el método slice y se leer los elementos que se van a copiar.

.splice()

El método .splice([start], [count], [element1], [element2], […]) cambia el contenido del Array, elimina elementos y, opcionalmente, añade otros nuevos.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.splice(1, 3);

el Array original queda de esta forma:

a => ["a","e"]
a.length => 2
Inspección
get ("splice") => function splice() { [native code] }
apply ("splice", [1,3])
  get ("length") => 5
  get ("constructor") => function Array() { [native code] }
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"
  set ("1","e")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"e"})
  deleteProperty ("4")
  deleteProperty ("3")
  deleteProperty ("2")
  set ("length",2)
    getOwnPropertyDescriptor ("length") => {
      "value": 5,
      "writable": true,
      "enumerable": false,
      "configurable": false
    }
    defineProperty ("length", {"value":2})

Esta es una operación algo compleja, ya que:

  • se obtiene el método
  • se ejecuta y en esa ejecución
    • se obtiene el constructor del objeto para crear una nueva instancia.
    • se leen y se borran los elementos de nuestra matriz que van a ser extraídos y se escriben los que les sustituyen.

La obtención del constructor es especialmente importante cuando hemos heredado de Array y estamos trabajando con una clase derivada. En vez de crear un nuevo Array sin más, lo que se crea es una nueva instancia del objeto con el que estamos operando.

.find()

El método .find(callback, [thisArg]) devuelve el primer elemento para el que la función devuelve true. Se utilizar para buscar elementos en el Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.find(x => x === 'e');
Inspección
get ("find") => function find() { [native code] }
apply ("find", [x => x === 'e'])
  get ("length") => 5
  get ("0") => "a"
  get ("1") => "b"
  get ("2") => "c"
  get ("3") => "d"
  get ("4") => "e"

Además de obtener el método, lo que se puede observar es que se han leído cada uno de los elementos del Array para pasarlos a la función que comprueba si coincide o no con nuestro criterio de búsqueda.

.copyWithin()

El método .copyWithin(target, [start], [end]) copia parte del Array en otra posición del mismo Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.copyWithin(0, 1, 5)

como resultado de esta ejecución obtenemos:

a => ["b","c","d","e","e"]
a.length => 5
Inspección
get ("copyWithin") => function copyWithin() { [native code] }
apply ("copyWithin", [0,1,5])
  get ("length") => 5
  has ("1") => true
  get ("1") => "b"
  set ("0","b")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"b"})
  has ("2") => true
  get ("2") => "c"
  set ("1","c")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"c"})
  has ("3") => true
  get ("3") => "d"
  set ("2","d")
    getOwnPropertyDescriptor ("2") => {
      "value": "c",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("2", {"value":"d"})
  has ("4") => true
  get ("4") => "e"
  set ("3","e")
    getOwnPropertyDescriptor ("3") => {
      "value": "d",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("3", {"value":"e"})

Se puede observar con claridad como, además de obtener y ejecutar el método, se leen y escriben cada uno de los elementos que es copiado dentro del Array.

.fill()

El método .fill(value, [strar], [end]) rellena los elementos de un Array con un valor, pudiendo indicar un inicio y un fin. Si no se indican límites, se rellena todo el Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.fill('x', 0, 3)

el resultado es:

a => ["x","x","x","d","e"]
a.length => 5
Inspección
get ("fill") => function fill() { [native code] }
apply ("fill", ["x",0,3])
  get ("length") => 5
  set ("0","x")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"x"})
  set ("1","x")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"x"})
  set ("2","x")
    getOwnPropertyDescriptor ("2") => {
      "value": "c",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("2", {"value":"x"})

En esta operación, además de obtener y ejecutar el método, podemos ver como se van escribiendo con el nuevo valor los elementos afectados.

.sort()

El método .sort([callback]) ordena los elementos de un Array. Se puede pasar una función de comparación que recibe dos valores y debe devolver un número negativo, cero o un número positivo para indicar que es menor, igual o mayor el primer valor que el segundo.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.sort((x, y) => y.charCodeAt(0) - x.charCodeAt(0) )

como resultado de esta ejecución obtenemos un Array ordenado según el criterio de la función que se ha pasado al método:

a => ["e","d","c","b","a"]
a.length => 5
Inspección
get ("sort") => function sort() { [native code] }
apply ("sort", [
  (x, y) => y.charCodeAt(0) - x.charCodeAt(0)
])
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"
  set ("0","e")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"e"})
  set ("1","d")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"d"})
  set ("2","c")
    getOwnPropertyDescriptor ("2") => {
      "value": "c",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("2", {"value":"c"})
  set ("3","b")
    getOwnPropertyDescriptor ("3") => {
      "value": "d",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("3", {"value":"b"})
  set ("4","a")
    getOwnPropertyDescriptor ("4") => {
      "value": "e",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("4", {"value":"a"})

En este caso, además de obtener y ejecutar el método, podemos ver cómo se obtienen todas las propiedades con has y get, para posteriormente escribir con set aquellas que han cambiado de posición. Esta es siempre una operación bastante compleja y puede llegar a ser muy pesada si el número de elementos de Array es grande.

.reverse()

El método .reverse() mueve los elementos de un Array para que se sitúen en orden inverso al original.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.reverse()

como resultado tendremos

a => ["e","d","c","b","a"]
a.length => 5
Inspección
get ("reverse") => function reverse() { [native code] }
apply ("reverse", [])
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("4") => true
  get ("4") => "e"
  set ("0","e")
    getOwnPropertyDescriptor ("0") => {
      "value": "a",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("0", {"value":"e"})
  set ("4","a")
    getOwnPropertyDescriptor ("4") => {
      "value": "e",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("4", {"value":"a"})
  has ("1") => true
  get ("1") => "b"
  has ("3") => true
  get ("3") => "d"
  set ("1","d")
    getOwnPropertyDescriptor ("1") => {
      "value": "b",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("1", {"value":"d"})
  set ("3","b")
    getOwnPropertyDescriptor ("3") => {
      "value": "d",
      "writable": true,
      "enumerable": true,
      "configurable": true
    }
    defineProperty ("3", {"value":"b"})

En este caso, la operación afecta a todos os elementos del Array, tanto en lectura como en escritura. Además de obtener y ejecutar el método, podemos observar cómo se van leyendo y escribiendo los elementos para hacer el cambio.

Primero se lee el primer y último elemento, luego los siguientes más bajo y más alto, y así hasta que esté completada la operación. Si el número de elemento es impar, el que está justo en el medio no tiene ninguna operación, ya que no va cambiar de posición,.

Métodos de búsqueda en el Array

.findIndex()

El método .findIndex(callback, [thisArg]) devuelve el índice del Array si la función devuelve true. Es muy parecido a .find(), pero en vez de devolver el valor de elemento, devuelve el índice.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.findIndex(x => x === 'e')
Inspección
get ("findIndex") => function findIndex() { [native code] }
apply ("findIndex", [x => x === 'e'])
  get ("length") => 5
  get ("0") => "a"
  get ("1") => "b"
  get ("2") => "c"
  get ("3") => "d"
  get ("4") => "e"

Como en el caso anterior, además de obtener el método, lo que se puede observar es que se han leído cada uno de los elementos del Array.

.indexOf()

El método .indexOf(searchElement, [fromIndex]) devuelve índice del primer elemento que coincida con el parámetro o -1. La comparación se realiza con un operador ===,
por lo que la coincidencia debe ser exacta.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.indexOf('e')
Inspección
get ("indexOf") => function indexOf() { [native code] }
apply ("indexOf", ["e"])
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

Aunque es muy parecido al resto de operaciones de búsqueda, en este caso, además de obtener los valores, se comprueba que exista el elemento con la operación has.

.lastIndexOf()

El método .lastIndexOf(searchElement, [fromIndex]) devuelve el índice del último elemento que con el parámetro o -1 si no se encuentra.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.lastIndexOf('e')
Inspección
get ("lastIndexOf") => function lastIndexOf() { [native code] }
apply ("lastIndexOf", ["e"])
  get ("length") => 5
  has ("4") => true
  get ("4") => "e"

Aunque en este ejemplo hemos obtenido un único valor, lo que ocurre es que se recorren los elementos del Array desde el último al primer elemento. Si hubiéramos buscado «a» se hubieran obtenido todos los valores hasta encontrar ese, pero siempre desde el el valor mayor al menor índice.

.includes()

El método .includes(searchElement, [fromIndex]) devuelve true si el valor buscado coincide con alguno de los valores de los elementos del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.includes('e')
Inspección
get ("includes") => function includes() { [native code] }
apply ("includes", ["e"])
  get ("length") => 5
  get ("0") => "a"
  get ("1") => "b"
  get ("2") => "c"
  get ("3") => "d"
  get ("4") => "e"

Además de obtener el método, lo que se puede observar es que se han leído cada uno de los elementos del Array para comprobar si coincide con el criterio de búsqueda.

.hasOwnProperty()

El método .hasOwnProperty(property) es heredado de Object,  y comprueba si una propiedad está creada en el objeto. Se puede utilizar para saber si un elemento existe dentro del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.hasOwnProperty(3)
Inspección
get ("hasOwnProperty") => function hasOwnProperty() { [native code] }
apply ("hasOwnProperty", [3])
  getOwnPropertyDescriptor ("3") => {
    "value": "d",
    "writable": true,
    "enumerable": true,
    "configurable": true
  }

Es curioso comprobar que lo que se ha realizado internamente es una llamada a getOwnPropertyDescriptor que devuelve la información de la propiedad o undefined si la propiedad no existe.

in

El operador in comprueba si una propiedad está creada en el objeto. Es parecido al caso anterior, pero con una instrucción en vez de con un método.

Ejemplo

const a = inspect (['a','b','c','d','e']);
3 in a
Inspección
has ("3") => true

En este caso, internamente, la estrategia de comprobación ha cambiado y se utiliza la operación has, que comprueba la existencia de la propiedad en el objeto.

Métodos para procesar el Array

.forEach()

.forEach(callback, [thisArg]) ejecuta la función para cada elemento de la matriz

Ejemplo

const a = ['a','b','c','d','e'];
let c = '';
a.forEach(x => c += x);

inspección

get ("forEach") => function forEach() { [native code] }
apply ("forEach", [x => c += x])
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

Esta operación es bastante sencilla, se obtiene le método, se ejecuta y se recogen todos los elementos del Array para pasarlos a la función.

.filter()

El método .filter(callback, [thisArg]) crea una nueva matriz con todos los elementos para los que la función devuelva true

Ejemplo

const a = ['a','b','c','d','e'];
a.filter(x => typeof x === "string");

inspección

get ("filter") => function filter() { [native code] }
apply ("filter", [x => typeof x === "string"])
  get ("length") => 5
  get ("constructor") => function Array() { [native code] }
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

En este caso, además de obtener el método y ejecutarlo, podemos observar que se consulta el constructor del objeto. Igual que en otras ocasiones, esto es importante para crear una nueva instancia del objeto (un Array) que será el valor que se retorne por parte de la función. Luego se recorren todos los elementos para pasarlos a la función.

.map(callback, [thisArg])

.map()crea una matriz con los resultados la función para cada uno de los elementos de la matriz

Ejemplo

const a = ['a','b','c','d','e'];
a.map(x => x.toUpperCase());

inspección

get ("map") => function map() { [native code] }
apply ("map", [x => x.toUpperCase()])
  get ("length") => 5
  get ("constructor") => function Array() { [native code] }
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

De forma similar a el resto de métodos de este tipo, además de obtener el método y ejecutarlo, se consulta el constructor del objeto para crear una nueva instancia. Luego se recorren todos los elementos para pasarlos a la función.

.reduce()

El método .reduce(callback, initialValue)aplica una función a todos los elementos de la matriz y devuelve el resultado en un acumulador

Ejemplo

const a = ['a','b','c','d','e'];
let x = a.reduce((r, x) => {
  r += x.toUpperCase();
  return r;
}, '');

inspección

get ("reduce") => function reduce() { [native code] }
apply ("reduce", [
  (r, x) => {\n  r += x.toUpperCase();\n  return r;\n},
  ""
  ])
  get ("length") => 5
  has ("0") => true
  get ("0") => "a"
  has ("1") => true
  get ("1") => "b"
  has ("2") => true
  get ("2") => "c"
  has ("3") => true
  get ("3") => "d"
  has ("4") => true
  get ("4") => "e"

Este es un caso bastante evidente, se obtiene el método, se ejecuta y se recorren todos los elementos del Array para pasarlos a la función.

.reduceRight()

El método .reduceRight(callback, initialValue)aplica una función a todos los elementos de la matriz de mayor a menor índice y devuelve el resultado en un acumulador

Ejemplo

const a = ['a','b','c','d','e'];
let x = a.reduceRight((r, x) => {
  r += x.toUpperCase();
  return r;
}, '');

resultado

a => ["a","b","c","d","e"]
a.length => 5

inspección

get ("reduceRight") => function reduceRight() { [native code] }
apply ("reduceRight", [
  (r, x) => {\n  r += x.toUpperCase();\n  return r;\n},
  ""
  ])
  get ("length") => 5
  has ("4") => true
  get ("4") => "e"
  has ("3") => true
  get ("3") => "d"
  has ("2") => true
  get ("2") => "c"
  has ("1") => true
  get ("1") => "b"
  has ("0") => true
  get ("0") => "a"

Es muy parecido al anterior, se obtiene el método, se ejecuta y se recorren todos los elementos del Array para pasarlos a la función, pero en orden inverso a su índice, es decir, primero el ubicado en el índice mayor y se va descendiendo hasta llegar al primer elemento..

Bucles e iteradores sobre el Array

for

La instrucción for([initialization]; [condition]; [final-expression]) se ejecuta en bucle mientras la condición sea verdadera. Es utilizado muy habitualmente para
recorrer los valores de un Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
let c = '';
for (let i = 0; i < a.length; i++) {
  c += a[i];
}
Inspección
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

Las operaciones realizadas son bastante evidentes, se comprueba length una vez en cada bucle y se obtienen cada uno de los valores.

for in

La instrucción for(variable in array) se ejecuta en bucle para todos los elementos enumerables del Array. Hay que tener en cuenta que las propiedades enumerables incluyen, por ejemplo, length.

Ejemplo

const a = inspect (['a','b','c','d','e']);
let c = '';
for (let i in a) {
  c += a[i];
}
Inspección
ownKeys ()
getPrototypeOf ()
getOwnPropertyDescriptor ("0") => {
  "value": "a",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
get ("0") => "a"
getOwnPropertyDescriptor ("1") => {
  "value": "b",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
get ("1") => "b"
getOwnPropertyDescriptor ("2") => {
  "value": "c",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
get ("2") => "c"
getOwnPropertyDescriptor ("3") => {
  "value": "d",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
get ("3") => "d"
getOwnPropertyDescriptor ("4") => {
  "value": "e",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
get ("4") => "e"
getOwnPropertyDescriptor ("length") => {
  "value": 5,
  "writable": true,
  "enumerable": false,
  "configurable": false
}

for of

La instrucción for (variable of iterable) se ejecuta en bucle para todos los elementos del Array llamando implícitamente a a[Symbol.iterator]()

Ejemplo

const a = inspect (['a','b','c','d','e']);
let c = '';
for (let x of a) {
  c += x;
}
Inspección
get (Symbol.iterator) => function values() { [native code] }
apply (Symbol.iterator, [])
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

Symbol.iterator

El símbolo Symbol.interator obtiene un método que ejecutado de esta forma [Symbol.interator]() obtiene un objeto iterable del Array. Este objeto iterable se utiliza para recorrer los elementos del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
const i = a[Symbol.iterator]();
let o;
do {
  o = i.next();
} while(!o.done)
Inspección
get (Symbol.iterator) => function values() { [native code] }
apply (Symbol.iterator, [])
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

Aquí podemos ver dos operaciones, la primera es simplemente obtener el método que oculta Symbol.iterator y luego las operaciones que se han realizado en el bucle. En cada solicitud al iterador, se consulta el tamaño de la matriz.

spread

La sintaxis spread  ... realizar una invocación a Symbol.interator para insertar el contenido del Array en otra instrucción. Este objeto iterable
se utiliza para recorrer los elementos del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
const x = [...a, 'f']
Inspección
get (Symbol.iterator) => function values() { [native code] }
apply (Symbol.iterator, [])
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

En este caso la llamada a Symbol.iterator y las operaciones de lectura de cada uno de los elementos del Array se ejecutan directamente por medio de la sintaxis de spread.

.entries()

El método .entries() devuelve un objeto iterable que contiene los pares clave-valor de cada uno de los elementos del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
for (let x of a.entries()) {}
Inspección
get ("entries") => function entries() { [native code] }
apply ("entries", [])
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

Aquí podemos ver dos operaciones, la primera es simplemente obtener el método y luego las operaciones que se han realizado en el bucle. En cada solicitud al iterador que devuelve .entries(), se consulta el tamaño de la matriz y el valor del elemento.

.values()

El método .values() devuelve un iterador con todos los valores del Array.

Ejemplo

const a = inspect (['a','b','c','d','e']);
for (let x of a.values()) {}
Inspección
get ("values") => function values() { [native code] }
apply ("values", [])
get ("length") => 5
get ("0") => "a"
get ("length") => 5
get ("1") => "b"
get ("length") => 5
get ("2") => "c"
get ("length") => 5
get ("3") => "d"
get ("length") => 5
get ("4") => "e"
get ("length") => 5

Como en los casos anteriores, observamos cómo se obtiene el método y su ejecución, así  cómo en en cada solicitud al iterador que devuelve .values(), se consulta el tamaño de la matriz y el valor del elemento.

Conversión del Array

.join()

El método .join([separator]) une todos los elementos del Array en una cadena, pudiendo utilizar un carácter separador entre los elementos.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.join()
Inspección
get ("join") => function join() { [native code] }
apply ("join", [])
  get ("length") => 5
  get ("0") => "a"
  get ("1") => "b"
  get ("2") => "c"
  get ("3") => "d"
  get ("4") => "e"

Se obtiene el método y se ejecuta, consulta el tamaño y obtiene todos los elementos del Array.

.toString()

En método .toString() convierte el Array en cadena de texto, para ello convierte cada elemento en cadena y se concatenan todas ellas separadas por comas.

Ejemplo

const a = inspect (['a','b','c','d','e']);
a.toString()
Inspección
get ("toString") => function toString() { [native code] }
apply ("toString", [])
  get ("join") => function join() { [native code] }
  apply ("join", [])
    get ("length") => 5
    get ("0") => "a"
    get ("1") => "b"
    get ("2") => "c"
    get ("3") => "d"
    get ("4") => "e"

En este caso, además de obtener y ejecutar el método .toString() podemos ver como internamente se obtiene y se llama al método .join(), que es quien realmente realiza la conversión.

JSON.stringify(value, [replacer], [spaces] )

JSON.strinify() intenta ejecutar el método .toJSON()

Ejemplo

const a = inspect (['a','b','c','d','e']);
JSON.stringify(a)
Inspección
get ("toJSON") => undefined
get ("length") => 5
get ("0") => "a"
get ("1") => "b"
get ("2") => "c"
get ("3") => "d"
get ("4") => "e"

Es interesante comprobar cómo se comprueba si existe un método .toJSON() antes de recorrer el contenido. Este método permite personalizar la conversión a JSON. Aunque Array carece de este método, JSON.stringify() siempre comprueba si está disponible antes de recorrer los elementos de la matriz para hacer la conversión estándar de su contenido.

./

Novedades

Datos inmutables en Javascript

Datos inmutables en Javascript

En Javascript todo parece mutable, es decir, que se puede cambiar, pero lo cierto es que también nos ofrece varios mecanismos para conseguir que los datos que manejamos, especialmente los objetos, sean inmutables. Te invitamos a descubrir cómo…

Copiar objetos en Javascript

Copiar objetos en Javascript

Copiar objetos no es algo sencillo, incluso se podría decir que en si mismo no es posible, ya que el concepto «copiar» no entra dentro del paradigma de los objetos. No obstante, por medio de instrucciones como Object.assign() hemos aprendido como obtener objetos con las mismas propiedades, pero está técnica no se puede aplicar a todos los tipos de objetos disponibles en Javascript. Vamos a ver cómo podemos copiar cualquier tipo de objeto…

Web Components: pasado, presente y futuro

Web Components: pasado, presente y futuro

Los Web Components aparecieron en el panorama de desarrollo hace ya bastante tiempo. Desde su presentación se les ha prestado mucha atención, pero lo cierto es que no han sido acogidos de forma generalizada, quizás por la difusión de nuevos y potentes frameworks. Nos preguntamos qué ha pasado con este estándar y, sobre todo, que puede pasar de aquí en adelante con el uso práctico de los componentes web.

Adaptar un componente web para Microsoft Edge

Adaptar un componente web para Microsoft Edge

Si quieres que tus componentes web funcionen sin problemas en Microsoft Edge, es conveniente que conozcas algunas de sus carencias y cómo resolverlas. Edge tiene soporte nativo a algunas de las características clave de los web component, pero hay que utilizar polyfill en otras y hay que tener en cuenta algunas limitaciones. Próximamente Edge utilizará Chromium como base de su desarrollo, pero de momento, es necesario aplicar estas soluciones.