Novedades ES6

Novedades en ES6 - Definición de getters y setters

Desde ES6, tenemos la posibilidad de usar getters y setters para definir propiedades en nuestros objetos. En este post entenderemos como funcionan.

En JavaScript disponemos ahora de la posibilidad de definir setters que son métodos que establecen un valor a una propiedad y getters que son métodos que rescatan un valor. Para la definición de setters y getters disponemos de las palabras claves set y get.
La definición de propiedades con set y get nos permiten un mejor encapsulamiento de nuestra clase.

El comportamiento y atributos de las propiedades de los objetos en JavaScript tienen tres tipos de propiedades:

  • Data properties: Las propiedades normales, que contienen datos.
  • Accessor properties: Propiedades que cambian el comportamiento estándar de [[get]] y [[put]]
  • _Internal properties: _propiedades internas del lenguaje, como [[prototype]], [[get]] o [[put]] entre otros.

Qué son los getters y setters

Una función que obtiene un valor de una propiedad se llama getter y una que establece el valor de una propiedad se llama setter.

Esta característica a sido implementada en ES2015, pudiendo modificar el funcionamiento normal de establecer u obtener el valor de una propiedad, a estas se les conoce como accessor properties.

Funcionamiento

En ocasiones queremos valores basados en otros valores, para esto los data accessors son bastante útiles.

Para crearlos usamos los keywords get y set

const obj = {
  get prop() {
    return this.__prop__;
  },
  set prop(value) {
    this.__prop__ = value * 2;
  },
};

obj.prop = 12;

console.log(obj.prop); //24

Creamos un objeto, con una única propiedad, que tiene un getter y un setter. de esta manera cada vez que establezcamos un valor para prop se multiplicará por dos.

Nota: Utilice prop por convención, pero no implica que es un valor especial, este es un valor normal.

Otra manera de crear un accessor properties es de manera explícita usando Object.defineProperty

const obj = {};

Object.defineProperty(obj, //objeto target
  'prop', //nombre propiedad
  {
    enumerable: true,
    configurable: true,
    get prop() { //getter
      return this.__prop__;
    },
    set prop(value) { //setter
      this.__prop__ = value * 2;
    },
  });
obj.prop = 12;

var atr = Object.getOwnPropertyDescriptor(obj, 'prop')
console.log(atr); 

La ventaja que tenemos de esta manera, es que podemos establecer los atributos que queremos tenga la propiedad.

Características

Una accessor property, solo tiene los atributos configurable y _enumerable, _si vemos sus atributos veremos esto.

[object Object] {
    configurable: true,
    enumerable: true,
    get: function get() {
      return this.__prop__;
    },
    set: function set(value) {
      this.__prop__ = value * 2;
    }
}

Esto nos lleva a que el valor no puede ser sobreescrito si no se usa el setter de la función (se recomienda definir ambos setter y getter).

Si no se usa strict mode y se intenta modificar el valor va a ser un error silencioso.

Otra característica importante, es que, si se establece una propiedad con el mismo nombre en un ámbito superior de la cadena de prototipos, el accessor property, va a ser la propiedad que predomine.

Veamos un ejemplo

let persona = {
  nombre: 'Yeison',
  apellido: 'Daza',
  get nombreCompleto() {
    return `${nombre} ${apellido}`
  },
  set nombreCompleto(nom) {
    const palabras = nom.split(' ');
    this.nombre = palabras[0] || '';
    this.apellido = palabras[1] || '';
  }
}

persona.nombreCompleto = 'Camilo Sanchez'

console.log(persona.nombre); //camilo
console.log(persona.apellido); //sanchez

Veremos con un ejemplo sencillo la sintaxis para definir una propiedad definiendo su setter y getter.

Problema

Declarar una clase llamada Dado y definir un atributo llamado '_valor' que almacene el valor actual del dado. Luego definir los métodos set y get para poder fijar un nuevo valor al dado y conocer el valor actual.

<!DOCTYPE html>
<html>
<head>
  <title>Ejemplo de JavaScript</title>
  <meta charset="UTF-8">
</head>
<body>

<script>
  class Dado {
    constructor() {
      this._valor=1;
    }
    
    get valor() {
      return this._valor;
    }
    
    set valor(v) {
      this._valor=v;
    }
    
    imprimir() {
      document.write(this.valor+'<br>');
    }
  }
  
  const dado1=new Dado();
  dado1.imprimir();
  dado1.valor=6;
  dado1.imprimir();
  
</script>

</body>
</html>

En el constructor definimos un atributo llamado _valor que almacena el valor 1:

    constructor() {
      this._valor=1;
    }

Definimos la propiedad 'valor' con sus respectivos 'set' y 'get':

    get valor() {
      return this._valor;
    }
    
    set valor(v) {
      this._valor=v;
    }

El método get retorna el valor almacenado en el atributo '_valor' y el método set actualiza el valor almacenado en el atributo '_valor'.

Donde definimos un objeto de la clase Dado accedemos a la propiedad dado para cambiar su valor por medio de una asignación:

  dado1.valor=6;

Es decir que en dicha asignación se está ejecutando el método:

    set valor(v) {
      this._valor=v;
    }

En principio podría pensarse que lo más fácil es acceder directamente al atributo _valor y olvidarnos del método set:

  dado1._valor=6;

Pero si queremos luego mejorar la clase 'Dado' y hacerla más robusta, por ejemplo no dejar cargar valores inválidos para un dado podemos mejorar el método 'set' con la siguiente sintaxis:

    set valor(v) {
      if (v>=1 && v<=6 && v%1===0)
        this._valor=v;
      else
        throw "Error en asignación de valor del dado"; 
    }

Con ésta nueva implementación del método set solo almacenaremos valores válidos en el atributo _valor si llega un número entero comprendido entre 1 y 6. En el caso que llegue un valor con coma 1.4 que está comprendido entre 1 y 6 luego no cumple la tercer condición que el resto de dividirlo sea cero.

Cualquiera de estas asignaciones detiene el programa con un error:

dado1.valor=7;
dado1.valor=0;
dado1.valor=3.4;
dado1.valor='Hola';

Ahora podemos comprobar la ventaja de definir propiedades de acceso a una clase en lugar de acceder directamente a sus atributos.

Si accedemos directamente al atributos '_valor' luego no se genera un error si le asignamos:

dado1._valor=3.4;

Esto hace que nuestro programa sea más difícil de mantener y propenso a errores.

Vimos que llamamos al método set mediante una asignación, al método get lo llamamos directamente por su nombre:

  const dado1=new Dado();
  dado1.valor=5;
  document.write(dado1.valor);

Cuando llamamos al método write del objeto document estamos accediendo al método get del objeto dado1.