ES6: el remozado JavaScript. Parte II: funciones, objetos y arrays

2
10961

Índice de contenidos

 

Este post es la continuación de: ES6: el remozado JavaScript. Parte I: variables y constantess

4. Funciones

Las funciones es uno de los apartados dentro de la especificación donde se han hecho más cambios. Veámoslos

4.1. Valores por defecto

Una cosa que siempre ha tenido JavaScript, y que ha sido fruto de controversia entre los programadores de otros lenguajes, es que se puede definir una función con más parámetros que con los que se la llama.

function ejemplo(nombre, apellidos) {
  if (apellidos != undefined){
    console.log("Hola " + nombre + " " + apellidos);
  } else {
    console.log("Hola " + nombre);
  }
}

ejemplo("Manuel");
ejemplo("Pepito", "Grillo");

Pero ahora, como pasaba en otros lenguajes EcmaScript como Flex, se pueden declarar valores por defecto para esos parámetros.

function ejemplo(nombre, apellidos = "García") {
  console.log("Hola " + nombre + " " + apellidos);
}

ejemplo("Manuel");                //Hola Manuel García
ejemplo("Pepito", "Grillo");      //Hola Pepito Grillo

A las funciones en JavaScript que pueden recibir un número variable de parámetros se les llama funciones variádicas. Y es uno de los puntos que aporta potencia al lenguaje, pero que lo convierte en incómodo a aquellos desarrolladores acostumbrados a otros lenguajes de programación, que para nada esperan este comportamiento.

4.2. Parámetro REST y operador SPREAD

Ahora en una función se permite que el último argumento que se le pasa, sea especial y se denota con tres puntos suspensivos. Eso nos indica que este último argumento, recibirá todos los parámetros de más de la función en forma de array:

function ejemplo (param1, param2, ...restParams) { 
  console.log(restParams);
}
ejemplo('a','b','c','d','e','f');  
//el resultado es el array ["c", "d", "e", "f"]

¿Y la operación contraria? Imaginemos que lo que tenemos es una función variádica que espera un número variable de parámetros en lugar de un array. Podemos convertir fácilmente el array a una lista de parámetros con el operador SPREAD, que también se denota con tres puntos suspensivos.

Esto puede parecer que da lugar a equívocos, pero fijémonos que uno se utiliza para definir una función, y el otro para invocarla.

let aVocales = ["a", "e", "i", "o", "u"];

function bar(){
  let txt = '';
  for(let i in arguments){
    txt += arguments[i];
  }
  console.log(txt); // la salida por consola es aeiou
}

function foo(a){
  console.log(a.length);  //a es un array
  bar(...a);    //esto es como llamar a bar('a','e','i','o','u')
}

foo(aVocales);

4.3. Funciones Arrow

Se denominan funciones arrow, se denotan por el símbolo => y se utilizan de la siguiente manera. Las siguientes expresiones son equivalentes

let multiplicar = function (a,b) {
                    return a * b;
                  }

let multiplicar = (a,b) => a * b;

console.log(multiplicar(3,5)); //15

Se utiliza mucho en las funciones anónimas que se definen como callbacks de otras funciones porque la función arrow sí preserva el contexto del this, al contrario de lo que pasa con las funciones estándar.

Por ejemplo, muchas veces habremos visto código de terceros que hacen cosas como var that = this y luego se refieren a ese that. Eso lo hacen porque la palabra this hace referencia al contexto del callback y no de la función externa. Veamos un ejemplo.

let ImageHandler = {
  id: "",

  init: function (id) {
    this.id = id;
    let that = this;
    document.addEventListener("click", function (event) {
      that.show(event);   // se usa that aquí, porque this haría referencia al contexto de la función anónima
    }, false);
  },

  show: function (event) {
    console.log("Se ha disparado el evento de typo " + event.type);
  }
};

Sin embargo, con una función Arrow quedaría:

let ImageHandler = {
  id: "",

  init: function (id) {
    this.id = id;
    document.addEventListener("click", (event) => this.show(event), false);
  },

  show: function (event) {
    console.log(event.type + " has been fired for " + this.id);
  }
};

Cuando la función arrow sólo recibe un argumento, no es necesario escribir paréntesis:

event => this.show()

Pero sí cuando no hay argumentos o son más de uno.

() => "Soy Íñigo Montoya";
(a,b) => a * b;

5. Objetos y Arrays

5.1. Forma abreviada para quitar repeticiones

En numerosas ocasiones nos hemos encontrado devolviendo un objeto con nombres y valores repetidos, donde el primero hace referencia a la propiedad, y el segundo es la variable con su valor

return {width: width, height: height};

Pero ahora es posible devolver una versión abreviada, donde la propiedad será como el nombre de la variable que contiene el valor. En el siguiente ejemplo, ambas funciones son equivalentes:

function getSizes(){
  let width = window.innerWidth;
  let height = window.innerHeight - 200; 
  return {width: width, height: height};  
}

function getSizes(){
  let width = window.innerWidth;
  let height = window.innerHeight - 200; 
  return {width, height}; 
}

Y también funciona en la asignación de variables:

  let sizes = {width, height};
  console.log(sizes.width);

En lugar de

  let sizes = {width: width, height: height};

5.2. Asignación por Destructuring

Se pueden asignar variables recorriendo las propiedades de un objeto de una en una, o bien se puede usar la asignación por destructuring, donde se asigna a cada variable la propiedad correspondiente.

let sizes = getSizes();
let width = sizes.width;
let height = sizes.height;

let {width, height} = getSizes();
console.log(width);
console.log(height);

No es obligatorio usar todas las propiedades del objeto. Se puede utilizar sólo una parte para hacer la asignación por destructuring.

let {width} = getSizes();

La asignación por destructuring también funciona para arrays.

let aVocales = ["a", "e", "i", "o", "u"];

let [z, y, x, w, v] = aVocales;
console.log(z);    //a
console.log(y);    //e
console.log(x);    //i
console.log(w);    //o
console.log(v);    //u

¿y funcionará con el operador Spread?

let aVocales = ["a", "e", "i", "o", "u"];

let [z, y, ...x] = aVocales;
console.log(z);    //a
console.log(y);    //e
console.log(x);    //["i", "o", "u"]

Pues sí que funciona, al menos en Google Chrome, aunque según la especificación, eso es de la próxima versión de EcmaScript ES7.

5.3. Iteraciones: for… in vs for… of

En las versiones previas de JavaScript ya se podía recorrer un array con bucles de tipo for… in

Realmente, recorre todas las propiedades enumerables de un objeto en un orden indeterminado.

let jugador = {
  name: "Sergio Callejas",
  posicion: "4",
  esTitular: true,
  dorsal: 33
}

for (propiedad in jugador){
  console.log(propiedad, eval('jugador.' + propiedad));
}

for (propiedad in jugador){
  console.log(propiedad, jugador[propiedad]);
}

De los dos bucles for, el primero nos obliga a acceder a la propiedad mediante la triquiñuela del eval(), porque si accedemos directamente mediante jugador.propiedad, no evalúa el valor de “propiedad” en cada vuelta del bucle, y busca literalmente el atributo llamado “propiedad”, y nos devolvería undefined.

Pero todos los objetos pueden recorrerse como arrays asociativos. Es decir, podemos acceder al nombre de ambas maneras.

console.log(propiedad.name)
console.log(propiedad["name"])

Y esto es lo que usamos en el segundo bucle.

Pero ¿y qué pasa si lo que tenemos es un array de objetos?

let equipo = [
  {name: "David Gómez", posicion: "5"},  
  {name: "Sergio Callejas", posicion: "4"},
  {name: "Javier Rodríguez", posicion: "3"},
  {name: "Laura Fernández", posicion: "2"},
  {name: "Manuel Fernández", posicion: "1"}
]

//recorremos por posición que ocupa en el array
for (i in equipo){
  console.log(equipo[i]);
}

Pero en ES6 existe una nueva forma de recorrer con for…of, que en lugar de por índice, recorre directamente los objetos que contiene:

//recorremos directamente los objetos del array
for (jugador of equipo){
  console.log(jugador.name);
}

Esto de for…of lo podemos hacer sobre los arrays porque son objetos iterables. Pero no es lo habitual en JavaScript. Si definimos un objeto, e intentamos recorrer sus propiedades, nos dará un error similar al siguiente: “TypeError: jugador[Symbol.iterator] is not a function”.

Pero siempre podemos crearlo. Veamos cómo.

5.4. Entendiendo Iteradores, y como crearlos

Hay algunos tipos de objetos que tienen implementado el iterador. Vamos a ver qué pinta tiene este iterador

Lo primero que se puede observar, es que la variable equipo, que es un array, sí implementa Symbol.iterator. Y al preguntar por iterador.next() nos devuelve un objeto con dos propiedades: value, que tiene el primer elemento del array, y done un booleano que nos indica si ya hemos recorrido todos los elementos.

Pero si usamos un objeto en lugar de un array, esto no funcionará porque no implementa un iterador. Pero podemos definirlo nosotros. Imaginemos el siguiente objeto no iterable:

let equipo = {
  posteBajo: {name: "David Gómez", dorsal: "00"},  
  posteAlto: {name: "Sergio Callejas", dorsal: "33"},
  alaPivot: {name: "Javier Rodríguez", dorsal: "23"},
  escolta: {name: "Laura Fernández", dorsal: "05"},
  base: {name: "Manuel Fernández", dorsal: "17"}
};

Y queremos iterar sobre sus atributos. Podríamos definir el siguiente iterador:

equipo[Symbol.iterator] = function(){
  let listaPosiciones = Object.keys(this);
  let i = 0;

  let next = () => {
    let posicion = listaPosiciones[i];
    return { 
      value : this[posicion], 
      done: (i++ >= listaPosiciones.length)
    };
  };

  return {next};
};

Una vez ya definido, podríamos recorrer las propiedades del objeto con un bucle de tipo for… of:

for (miembro of equipo){
  console.log(miembro);
};

5.5. Generadores

En esta nueva versión de JavaScript se introducen los generadores, que tienen todo el aspecto de una función, pero con algunas particularidades. La primera, y que es la que lo define, es que tras la palabra reservada function hay un asterisco *. Y en lugar de return usa la palabra reservada yield. Además, todo generador implementa un iterador.

function* miGenerador(){
  yield "uno";
  yield "dos";
  yield "tres";
}

let ejemplo = miGenerador();

Cuando se invoca a un generador, como en el ejemplo anterior, no hace nada de nada.

Como los iteradores, funciona cuando se llama a su método next(), entonces devuelve el primer yield, y se guarda el estado a la espera de que en algún momento se vuelva a llamar a next(). Cuando se llame a next() por segunda vez, devolverá el segundo yield. Y así hasta que no haya más.

Esto tiene múltiples implicaciones, ligadas a la preservación del estado y la asincronía. Una que se me ocurre es para implementar un asistente, wizard o sistema de pasos:

let wizardBox = {
  step1 : function(){
    console.log("paso 1 de 3");
  },
  step2 : function(){
    console.log("paso 2 de 3");
  },
  step3 : function(){
    console.log("paso 3 de 3");
  }
}

function* wizard(){
  yield wizardBox.step1();
  yield wizardBox.step2();
  yield wizardBox.step3();
}

let w = wizard();

w.next(); 
//ejecutamos el primer paso y hacemos lo que sea

w.next(); 
//se ejecuta el segundo paso continuado donde lo dejamos

w.next();
//se ejecuta el tercer paso continuando desde el punto anterior

Y otra de las más utilizadas es para escribir el iterador que poníamos de ejemplo en el punto anterior, pero de una forma mucho más simple:

equipo[Symbol.iterator] = function *(){
  let listaPosiciones = Object.keys(this);

  for (let posicion of listaPosiciones){
    yield this[posicion];
  }
};

5.6. Los nuevos tipos Map y WeakMap

En esta versión de JavaScript se introducen cuatro tipos nuevos de objetos: Map, WeakMap, Set y WeakSet.

Los mapas son un conjunto de pares clave / valor que implementan algunas operaciones:

  • para añadir elementos al mapa
  • para obtener un elemento del mapa
  • para consultar si está un elemento en el mapa
  • para quitar un elemento del mapa
  • para vaciar el mapa
let equipacion = new Map();
equipacion.set('camiseta',{size:'XXL', dorsal:'02'});
equipacion.set('zapatillas',46);
equipacion.set('pantalones','54');

equipacion.get('zapatillas');     //46
equipacion.get('camiseta').size;  //XXL
equipacion.size;                  //3

equipacion.has('pantalones');     //true
equipacion.delete('pantalones')   //quitamos el elemento pantalones
equipacion.has('pantalones');     //false
equipacion.clear();               //vaciamos el mapa
equipacion.size;                  //0

En el caso concreto de los mapas, son iterables, y admiten pares de objetos, tanto en las claves como en los valores:

let equipacion = new Map();
equipacion.set(equipo.posteBajo, {camiseta: 'XXXL', pie: 46});
equipacion.get(equipo.posteBajo);

Por el contrario los WeakMap son un tipo de mapas “débiles”. La primera diferencia es que las claves sólo pueden ser objetos. Nunca strings. Tampoco son iterables, y como tal no puedes consultar sus claves con Object.keys(). Además, hay que tener en cuenta que sus objetos se encuentran referenciados de forma «weak» (débil). Esto quiere decir, que el objeto que forma parte de la clave, si no es referenciado por nadie más que el WeakMap, es susceptible de que el Garbage Collector lo elimine.

Por tanto, el uso de los WeakMap tienen sus pros y sus contras.

En contra tienen que no son colecciones que se puedan recorrer con bucles de tipo for…of, hay que conocer la clave a priori para recuperar su valor. Tiene el peligro de que si nadie referencia a la clave, se pierda el valor asociado. A cambio, tiene la ventaja de no permitir memory leaks.

let equipacion = new WeakMap();

equipacion.set("posteBajo", {camiseta: 'XXXL', pie: 46});
//TypeError: la clave sólo puede ser un objeto

equipacion.set(equipo.posteBajo, {camiseta: 'XXXL', pie: 46});

console.log(equipacion.get(equipo.posteBajo));
//{camiseta: 'XXXL', pie: 46}

equipo.posteBajo = undefined;      //borramos el objeto posteBajo
console.log(equipacion.get(equipo.posteBajo));  //undefined

5.7. Los nuevos tipos Set y WeakSet

Al igual que los mapas, los conjuntos también se introducen en esta especificación de ES6. Consisten en colecciones de objetos o valores primitivos y no se pueden repetir.

let vocales = new Set();
vocales.add('a');
vocales.add('e');
vocales.add('i');
vocales.add('o');
vocales.add('u');
vocales.add('a');

console.log(vocales.size); //5

for (let vocal of vocales){ 
  console.log(vocal); 
}

Los WeakSet son como los Set pero con la misma línea argumental que los WeakMap:

  • No implementan un iterador
  • Sólo pueden contener objetos
  • Y su referencia a sus valores es débil

[alert type=»warning» close=»false»]Este post está formado de tres partes y en breve se publicará la siguiente: ES6: el remozado JavaScript. Parte III: clases y otras novedades del lenguaje[/alert]

2 COMENTARIOS

  1. Muy Buenas, estoy haciendo un código en js que copie de una pagina y he modificado para que haga lo que quiero, se trata de una animacion canvas con una pelota que cae y que luego es un link a una pagina.

    Pero ahora, tengo otro problema, que no se como encauzar, me gustaría crear con este código una especie de constructor, de tal forma que pueda llamarlo como una función, pasandole algunos parámetros, tamaño de la pelota, color de la pelota, color del texto, texto, posición en el canvas, etc. y que pudiese llamarlo varias veces con los diferentes parámetros, para que me cree nuevos objetos de la pelota, dentro del mismo canvas, cada uno con sus caracteristicas.

    pero no se como debo plantearlo, como aplico para crear ese constructor, con el código que ya tengo.

    Muchas gracias.

    saludos.
    Juan Carlos.

    • Pues depende del código que ya tengas. Pero puedes definir un costructor normal o por el contrario definir una clase y extender su definición añadiendo métodos al prototipo.

      La diferencia en POO clásica sería que en el primer caso, esos métodos tendrían una dirección de memoria distinta cada uno, mientras que en el segundo caso, tendrían la misma dirección de memoria (es como si fueran métodos estáticos).

      Si el número de pelotas puede ser muy elevado sería conveniente elegir la segunda forma por una cuestión de rendimiento del navegador (sobre todo en móviles y en equipos con poca memoria). Sin embargo, la gente que venga de Java se sentirá más cómoda con la primera manera que es más fiel al paradigma.

      Espero haberte aclarado algo. Siéntete libre de compartir tu código con nosotros.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad