JS.now

Cómo aprovechar ES2015 hoy

ES¿qué?

ES2015 o ES6 es la sexta revision (la quinta publicada) de ECMAScript, el lenguaje en el que se basa JavaScript, entre otros.

Oficialmente llamado ECMAScript 2015, se publicó en su versión definitiva en Junio de 2016.

La versión anterior, ES5, se presentó en Diciembre de 2009 y se revisó en Junio de 2011.

ES¿qué?

ES2015 ofrece muchas mejoras necesarias para la magnitud que cada vez más está ganando JavaScript, como por ejemplo:

  • Clases y módulos
  • Iteradores y generadores
  • Funciones "Flecha" (Arrow functions)
  • Nuevos tipos de dato (octal, binario,...) y funciones matemáticas
  • Colecciones (maps, sets, weak maps)
  • Mejoras en procesamiento asíncrono (promise)
  • Otros agregados y mejoras de sintaxis...

¿Puedo usarlo ya?

ES2015 tiene un soporte bastante extendido, aunque no completo, en motores JS actuales, todo dependerá de quién tenga que ejecutar tu código.

Existen características de ES2015 muy extendidas que soportan navegadores como Internet Explorer, y otras que ni siquiera Google Chrome las implementa aún.

Soporte muy muy simplificado:

  • Internet Explorer (v8 - v11):
  • Microsoft Edge (v13+):
  • Google Chome (v52+) / Opera (v39+):
  • Mozilla Firefox (v50+):
  • Apple Safari (v10):
  • iOS browser (v9):
  • Android browser (v4.4-): /
  • Android (v5+ Chrome webview):
  • NodeJS (v6+):
  • KinomaXS (v6+):

"Legacy browsers or ES2015?, that's not the question", diría Shakespeare

Si tu proyecto tiene que soportar navegadores no tan actuales puedes apoyarte en librerías "traductoras de ES2015" (transpilers) o en Polyfills.

Librerías traductoras: permiten desarrollar sobre ES2015, compilando el código original en código compatible con ES5 o anteriores. Como por ejemplo: Babel.

Polyfill: Ofrece una implementación de una característica de ES2015 sobre ES5 o anteriores compatible a nivel API, sólo en caso de que el entorno de ejecución no la soporte. Algunos ejemplos: ES6-Promise (sólo para promise), Core-JS.

Polyfills

El mecanismo para crear un polyfill es muy sencillo, sólo necesitas comprobar si la función está soportada en el entorno de ejecución actual y si no es así, declarar tu función compatible:


// String.prototype.trimStart
if(String.prototype.trimStart === undefined){
  String.prototype.trimStart = function(){
    return this.replace(/^\s*/,'');
  };
}
// versión corta
''.trimStart || (String.prototype.trimStart = function(){
    return this.replace(/^\s*/,'');
});
        

Sintaxis

Algunas novedades

Ámbito de bloque

La técnica más común para limitar el ámbito de una variable hasta ahora era el uso de las funciones anónimas inmediatamente invocadas también llamadas IIFE (Inmediatly Invoked Function Expressions)


var a = 'Exterior';
(function(){
  var a = 'Interior';
  console.log(a);
})();
console.log(a);
        

Ámbito de bloque

Ahora podemos limitar el ámbito usando bloques mediante llaves { ... } y la declaración de variables mediante let.


var a = 'Exterior';
{
  let a = 'Interior';
  console.log(a);
}
console.log(a);
        

Pecualiaridades de let

TDZ o Temporal Dead Zone


console.log(a); // Reference Error: a is not defined
        

console.log(a); // Undefined (se eleva su declaración, no su asignación)
var a = 'Exterior';
        

{
  console.log(a); // Reference Error
  let a = 'Interior';
}
        

Bucles for con let

La declaración de la variable a iterar con let permite disponer de una variable redeclarada para cada iteración dentro del bloque.


var funcs = [];
for (let i = 0; i < 5; i++) {
  funcs.push( function(){
    console.log( i );
  } );
}
        

Constantes

Una constante no es más que una variable a la que sólo se puede establecer su valor en su declaración. En caso de querer modificarse lanzará una excepción.


const a = 10;
a = 11;
        

Pero si es de un tipo "complejo", sí es posible modificar su contenido (aunque no esté recomendado).


const a = [1,2,3];
a.push(4);
        

Funciones dentro bloques

Como con let, podemos declarar funciones accesibles sólo desde dentro del bloque.


'use strict';
{
  saludo();

  function saludo(){
    console.log('hola!');
  }
}

saludo();
        

Funciones dentro bloques

Hay que tener cuidado si estamos acostumbrados a escribir funciones de esta forma:


'use strict';
let foo = 'nope';
if(foo === 'bar'){
  function test(){
    console.log('Test para bar');
  }
}else{
  function test(){
    console.log('Test para otra cosa');
  }
}

test();
        

Plantillas de texto

La creación de textos con variables insertadas es una tarea que ya tenemos solucionada con la fea pero efectiva concatenación de variables y textos estáticos. Pero ahora podemos solucionarlo con un código más rápido de escribir y elegante mediante plantillas.


let username = "Pepe";

// Pre-ES2015
console.log("¡Hola " + username + "!");

// ES2015
console.log(`¡Hola ${username}!`);
        

Operador ...

Normalmente llamado operador de propagación (Spread) u operador Rest según dónde se encuentre.


// Spread
var a = [1,2,3];
console.log(a);
console.log(...a);

// Rest
function logAll(...z){
  console.log(z);
}
logAll(1,2,3,4,5,6,7,8,9,10);
        

Operador ...

En realidad responde a la estandarización del objeto arguments que se mantenía por retrocompatibilidad pero no se recomendaba su uso intensivo.

La principal diferencia entre los parametros rest y el objeto arguments es que el primero sí es un array; mientras que el segundo es un objeto con algunas propiedades de array, como length.


function logAllArguments(){
  console.log(arguments);
}
logAll(1,2,3,4,5,6,7,8,9,10);
logAllArguments(1,2,3,4,5,6,7,8,9,10);
        

Valores por defecto en funciones

Casi siempre hemos usado la fórmula


variable = variable || 10;
          
o las variantes

// Para evitar que 0 se ignore
variable = (variable !== undefined) ? variable : 10;
          
o

// Sólo detectar si se ha pasado parametro o no
variable = (0 in arguments) ? variable : 10;
          

Valores por defecto en funciones

Ahora por fin podemos indicar un valor por defecto usando la fórmula


function suma(a = 10, b = 20){
  return a + b;
}
suma(2,3);
suma(8);
suma();
suma(undefined, 6);
suma(null, 6);
suma(,6);
        

Valores por defecto en funciones

Además de valores estáticos, podemos usar expresiones para asignaciones por defecto


function cuadrado(x){
  return x * x;
}
function sumaCuadrado(a = 5, b = cuadrado(a)){
  return a + b;
}
sumaCuadrado(2);
        

Peculiaridades en valores por defecto

  • Los parámetros tipo rest no pueden tener un valor por defecto

    
    function test(...z = [1,2,3]){
      console.log(z); // Unexpected token
    }
    test();
                
  • Tienen un ámbito propio reservando los nombres de los parámetros

    
    var z = 9;
    function test(z = z + 1){
      console.log(z);
    }
    test(); // Reference error
                

Desestructuración

Permite asignar valores a distintas variables desde las posiciones de un array o las propiedades de un objeto. Muy útil para permitir devolver varios valores separados en variables desde la misma función.


function foo() {
  return [1,2,3];
}
// Pre-ES2015
var tmp = foo(),
    a = tmp[0], b = tmp[1], c = tmp[2];
console.log(a, b, c); // 1 2 3

// ES2015
var [x,y,z] = foo();
console.log(x, y, z); // 1 2 3
        

Desestructuración. Objetos.

La función es muy similar usando objetos, pero ojo, la asignación está invertida.


function foo() {
  return {a: 1, b: 2, c: 3};
}
var {a:x, b:y, c:z} = foo(); // propiedad del objeto: nombre de la variable
console.log(x, y, z); // 1 2 3
        

También es válido para variables que ya existan.


var d,e,f;
( {a:d, b:e, c:f} = foo() );
        

Desestructuración. Objetos.

Cuando las variables y las propiedades a asignar tienen el mismo nombre, podemos abreviar la asignación:


function foo() {
  return {a: 1, b: 2, c: 3};
}
var {a, b, c} = foo();
console.log(a, b, c);
        

Esto puede aprovecharse de forma inversa para asignar propiedades a un objeto cuando estas se llamen igual que las variables a asignar.


var a = 1, b = 2, c = 3;
var x = {a ,b, c};
console.log(x);
        

Bucles for ... of

Uno de los bucles más deseados en JS, los que iteran sobre un array y devuelven directamente el elemento de cada posición.

Importante no confundirlo con el bucle for ... in, que devuelve el índice de cada posición.


var myArr = ['a','b','c','d','x','y','z'];
for (let el of myArr) {
  console.log(el);
}
for (let idx in myArr) {
  console.log(idx);
}
        

Mejoras en Objetos. Métodos abreviados.

En la definición de un método para un objeto solemos crear funciones anónimas que asignamos a una clave que le da nombre, pero ahora podemos usar una sintaxis abreviada para definirlas y dejar un código más limpio.


// Pre-ES2015
var oldObj = {
  myMethod: function() { console.log('Hola!'); }
}

// ES2015
let newObj = {
  myMethod() { console.log('Hola!'); }
}
        

Mejoras en Objetos. Generadores.

Son funciones especiales que se usan para controlar iteraciones (de hecho los generadores son iteradores). Son similares a un array, pero estos pueden ser ejecutados con parámetros y producen (yield) un valor con cada llamada.


function *fibonacci(){
  let [prev, curr] = [0,1];
  while (curr < 200) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}
        

Mejoras en Objetos. Get y Set.

Un paso hacia conseguir objetos de uso similar al resto de lenguajes son los getters y los setters.

Disponible desde ES5 pero bastante desconocido para la mayoría de los desarrolladores.


var myObj = {
  __val: 10,
  get val() { return this.__val++; },
  set val(v) { this.__val = v; }
}
myObj.val;    // 10
myObj.val;    // 11
myObj.val = 20;
myObj.val;    // 20
        

Mejoras en Objetos. Funciones flecha.

Una importante mejora a las funciones anónimas dentro de objetos.

No sólo reducen el tamaño del código, sino que mantienen automáticamente el ámbito de variables donde fueron definidas.


var btnController = {
  getUrl(id) {
    return `${this.baseURL}/${id}`;
  },
  init() {
    var self = this; // <-- El hack de siempre
    this.btnEl.addEventListener('click', function(event) {
      utils.makeRequest( self.getUrl( self.btnEl.id ) );
    });
  }
}
        

Mejoras en Objetos. Funciones flecha.

Aplicando funciones flecha (arrow functions) conseguimos prescindir de hacks o bindings de funciones facilitando el desarrollo.

Aún así, hay casos en los que no se puede aplicar, como en recursividad.


var btnController = {
  getUrl(id) {
    return `${this.baseURL}/${id}`;
  },
  init() {
    this.btnEl.addEventListener('click', (event) => {
      utils.makeRequest( this.getUrl( this.btnEl.id ) );
    });
  }
}
        

Mejoras en Objetos. Valores por defecto.

Otra tediosa tarea al tratar con funciones/métodos de objetos es controlar los valores por defecto cuando usamos objetos de configuración como parámetros, desvirtuando nuestro código a cosas como esta:


function superConfigurableFunc(param, options = {}) {
  let opt1 = options.opt1 || "defaultValue";
  let opt2 = options.opt2 || 10;
  let opt3 = options.opt3 || "defaultValue2";
  let opt4 = options.opt4 || "defaultValue3";
  let opt5 = options.opt5 || 200;

  // ... Por fin, aquí nuestro código
}
        

Mejoras en Objetos. Valores por defecto.

Lo primero a mejorar sería mejorar el aspecto y mantenibilidad de nuestros valores por defecto con un objeto:


function superConfigurableFunc(param, options = {}) {
  let defaults = {
    opt1: "defaultValue",
    opt2: 10,
    opt3: "defaultValue2",
    opt4: "defaultValue3",
    opt5: 200
  }

  // ...
}
        

Mejoras en Objetos. Valores por defecto.

Posteriormente, aplicaríamos los valores por defecto a nuestro objeto de opciones mediante el método Object.assign.

Este método permite tantos parámetros como necesitemos, aunque necesita al menos dos objetos; aplicando los valores del objeto más a la derecha al que tiene a su izquierda, hasta llegar al primero por la izquierda; y devolviendo el resultado.


function superConfigurableFunc(param, options = {}) {
  let defaults = { ... }

  let settings = Object.assign({}, defaults, options);

  // ...
}
        

Mejoras en Objetos. Clases.

Hasta ahora la creacion de clases era cuanto menos, poco ortodoxa


// Pre-ES2015 (una de las formas posibles)
function MyClass(x, y) {
  this.x = x;
  this.y = y;
}
MyClass.prototype.doSomething = function() {
  console.log("I did something");
}

let myObj = new MyClass(1,2);
myObj.doSomething();
        

Mejoras en Objetos. Clases.

Pero ahora podemos declararlas, por fin, como clases de una forma más parecida a otros lenguajes


// ES2015
class MyNewClass {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  doSomething() {
    console.log("I did something");
  }
}

let myNewObj = new MyNewClass(1,2);
myNewObj.doSomething();
        

Mejoras en Objetos. Herencia.

La herencia de clases se vuelve también más sencilla gracias a extends y super.


class ParentClass {
  constructor(x) { this.x = x; }
  operateX() { return this.x * this.x; }
}

class ChildClass extends ParentClass {
  constructor(x,y) {
    super(x);
    this.y = y;
  }
  operateX() {
    let tempX = super.operateX();
    return tempX + this.y;
  }
}
        

El Infierno de los Callbacks

La paradoja más conocida en JavaScript, los callbacks, es quizás la característica más potente y diferenciadora de este lenguaje; pero también es el Infierno de los Callbacks cuando necesitan anidarse en exceso.

Aquí es donde las promesas (promises) vienen a mejorar un poco nuestra vida, convirtiendo esto:


superWidgetConCallbacks(param, function(error, result) {
  if(error) { ... }
  hazUnaPeticionDeRed(result, function(error, result) {
    if(error) { ... }
    hazAlgoPesadoParaRenderizar(result, function(error, result) {
      if(error) { ... }
      creoQueNecesitoOtroCallbackAnidado(result, function(error, result) {
        if(error) { ... }
        // ...
      });
    });
  });
});

Promesas

... en esto:


superWidgetConCallbacks(param)
  .then(hazUnaPeticionDeRed)
  .then(hazAlgoPesadoParaRenderizar)
  .then(creoQueNecesitoOtroCallbackAnidado)
  .catch(function(error) { ... });
        

Pero, ¿qué es realmente una promesa?

Promesas

¿Qué son las promesas?

  • No son una sustitución incondicional de los callbacks.
  • Sí son una especie de contenedor que albergará el valor resultante de distintas operaciones asíncronas, una vez estas hayan terminado.
  • También son un elemento de control del flujo de ejecución de esas tareas.

Este control lo tenemos gracias a las funciones resolve y reject que permiten controlar el estado de una promesa de Pendiente a Resuelta o Rechazada.

Promesas

Un ejemplo de creación de una promesa:


function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response);
      }
      else {
        reject(Error(req.statusText));
      }
    };

    req.onerror = function() {
      reject(Error("Network Error"));
    };

    req.send();
  });
}
        

Promesas

Promises.all

Permiten lanzar juntas varias promesas y ejecutar el contenido de then cuando todas ellas han sido resueltas.


Promise.all([promise1, promise2]).then(function(results) { ... })
.catch(function(error) { ... });
        

Promises.race

Similar a la anterior, pero ejecuta el contenido de then cuando la primera de ellas ha sido resuelta.


Promise.race([promise1, promise2]).then(function(results) { ... })
.catch(function(error) { ... });
        

¿Preguntas?

Gracias