Getters y Setters de Propiedad
En JavaScript, los objetos pueden tener dos tipos principales de propiedades.
El primer tipo son las propiedades de datos, que ya conocemos. Todas las propiedades que hemos utilizado hasta ahora son propiedades de datos.
El segundo tipo son las propiedades de acceso, también conocidas como accessors. Estas son funciones que se ejecutan para obtener (“get”) y establecer (“set”) un valor, pero desde fuera parecen propiedades normales.
Getters y Setters
Las propiedades de acceso se crean con métodos de obtención (getter
) y asignación (setter
). En un objeto literal, se denotan con get
y set
:
let obj = {
get propName() {
// getter, el código ejecutado para obtener obj.propName
},
set propName(value) {
// setter, el código ejecutado para asignar obj.propName = value
}
};
El getter se activa cuando se lee
obj.propName
, y el setter se activa cuando se asigna un valor aobj.propName
.Por ejemplo, imaginemos un objeto
user
con propiedadesname
ysurname
:
let user = {
name: "John",
surname: "Smith"
};
Queremos agregar una propiedad fullName
que combine name
y surname
en un solo valor, como «John Smith». En lugar de duplicar la información existente, podemos crearla como una propiedad de acceso:
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
alert(user.fullName); // John Smith
Desde fuera,
fullName
parece una propiedad normal. No llamamos auser.fullName
como si fuera una función; simplemente la leemos, y el getter se ejecuta en segundo plano.Actualmente,
fullName
solo tiene un getter. Si intentamos asignarle un valor, obtendremos un error:
let user = {
get fullName() {
return `...`;
}
};
user.fullName = "Test"; // Error (property has only a getter)
Solucionemos esto añadiendo un setter para user.fullName
:
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
// El setter fullName se ejecuta con el valor dado.
user.fullName = "Alice Cooper";
alert(user.name); // Alice
alert(user.surname); // Cooper
Como resultado, ahora tenemos una propiedad virtual fullName
que puede ser leída y escrita.
Descriptores de Acceso
Los descriptores de propiedades de acceso son diferentes a los descriptores de propiedades de datos.
Para las propiedades de acceso, no existen value
ni writable
; en su lugar, tenemos get
y set
.
Un descriptor de acceso puede tener:
get
: una función sin argumentos que se ejecuta cuando se lee la propiedad.set
: una función con un argumento que se llama cuando se establece la propiedad.enumerable
: igual que para las propiedades de datos.configurable
: igual que para las propiedades de datos.
Por ejemplo, para crear una propiedad de acceso fullName
usando defineProperty
, podemos pasar un descriptor con get
y set
:
let user = {
name: "John",
surname: "Smith"
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
alert(user.fullName); // John Smith
for (let key in user) alert(key); // name, surname
Tenga en cuenta que una propiedad puede ser un accessor (tiene métodos get/set) o una propiedad de datos (tiene un value), pero no ambas.
Si intentamos poner get
y value
en el mismo descriptor, obtendremos un error:
// Error: Descriptor de propiedad inválido.
Object.defineProperty({}, 'prop', {
get() {
return 1;
},
value: 2
});
Getters y Setters más Inteligentes
Los getters y setters pueden ser utilizados como envoltorios alrededor de valores de propiedad “reales” para obtener más control sobre ellos.
Por ejemplo, si queremos prohibir nombres demasiado cortos para user
, podemos guardar name
en una propiedad especial _name
y filtrar las asignaciones en el setter:
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert("El nombre es demasiado corto, necesita al menos 4 caracteres");
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name); // Pete
user.name = ""; // El nombre es demasiado corto...
Entonces, name
se almacena en la propiedad _name
, y el acceso se realiza a través del getter y setter.
Técnicamente, el código externo aún puede acceder directamente a _name
, pero hay una convención general de que las propiedades que comienzan con un guion bajo _
son internas y no deben ser manipuladas desde fuera del objeto.
Uso para Compatibilidad
Una de las grandes ventajas de los getters y setters es que permiten tomar el control de una propiedad de datos “normal” y reemplazarla con un getter y un setter para refinar su comportamiento.
Imaginemos que empezamos a implementar objetos user
usando las propiedades de datos name
y age
:
function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User("John", 25);
alert(john.age); // 25
Pero eventualmente, podemos decidir almacenar birthday
en lugar de age
, ya que es más preciso y conveniente:
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
Ahora, ¿qué hacemos con el viejo código que aún usa la propiedad age
?
Podríamos intentar encontrar todos esos lugares y corregirlos, pero eso lleva tiempo y puede ser difícil si ese código fue escrito por otras personas. Además, age
es una propiedad útil para tener en user
.
Podemos mantener la propiedad age
añadiendo un getter que calcule la edad a partir de birthday
:
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
Object.defineProperty(this, "age", {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User("John", new Date(1992, 6, 1));
alert(john.birthday); // El cumpleaños está disponible
alert(john.age); // ...así como la edad
Ahora, el código antiguo también funciona, y tenemos una propiedad adicional útil.