2. JavaScript orientado a objetos
2.1. Trabajando con objetos
Todo en JavaScript es un objeto. El ejemplo más básico de un objeto en JavaScript es una cadena.
var cadena = "Yo soy tu padre",
long = cadena.length;
var minus = cadena.toUpperCase();
console.log(minus);
console.log(cadena);
Podemos observar como la cadena tiene atributos (length
) y métodos (toUpperCase()
).
Los tipos de datos primitivos son tipos de datos Number
, String
o Boolean
. Realmente no son objetos, y aunque tengan métodos y propiedades, son inmutables.
En cambio, los objetos en JavaScript son colecciones de claves mutables. En JavaScript, los arrays son objetos, las funciones son objetos (Function
), las fechas son objetos (Date
), las expresiones regulares son objetos (RegExp
) y los objetos, objetos son (Object
).
Para crear un objeto, podemos usar el tipo de datos Object
.
var obj = new Object();
var str = new String();
2.1.1. Propiedades
Un objeto es un contenedor de propiedades (cada propiedad tiene un nombre y un valor), y por tanto, son útiles para coleccionar y organizar datos. Los objetos pueden contener otros objetos, lo que permite estructuras de grafo o árbol.
Para añadir propiedades a un objeto no tenemos más que asignarle un valor utilizando el operador .
para indicar que la propiedad forma parte del objeto.
var persona = new Object();
persona.nombre = "Aitor";
persona.apellido1 = "Medrano";
Para averiguar si un objeto contiene un campo, podemos usar el operador in
. Finalmente, si queremos eliminar una propiedad de un objeto, hemos de utilizar el operador delete
:
console.log('nombre' in persona); // true
delete persona.nombre;
console.log('nombre' in persona); // false
2.1.2. Métodos
Para crear un método, podemos asignar una función anónima a una propiedad, y al formar parte del objeto, la variable this
referencia al objeto en cuestión (y no a la variable global como sucede con las funciones declaración/expresión).
persona.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
}
Y para invocar la función invocaremos el método de manera similar a Java:
console.log( persona.getNombreCompleto() );
Realmente el código no se escribe así, es decir, no se crea un Object
y posteriormente se le asocian propiedades y métodos, sino que se simplifica mediante objetos literales.
2.2. Objetos literales
Los objetos literales ofrecen una notación para crear nuevos objetos valor. Un objeto literal es un par de llaves que rodean 0 o más parejas de clave:valor separados por comas, donde cada clave se considera como un propiedad/método, de la siguiente manera:
var nadie = {};
var persona = {
nombre : "Aitor", (1)
apellido1 : "Medrano",
getNombreCompleto : function() { (2)
return this.nombre + " " + this.apellido1;
}
};
1 | La propiedad y el valor se separan con dos puntos, y cada una de las propiedades con una coma, de forma similar a JSON. |
2 | un método es una propiedad de tipo function |
Todo objeto literal finaliza con un punto y coma (; )
|
Para recuperar un campo, además de la notación .
, podemos acceder a cualquier propiedad usando la notación de corchetes []
:
var nom = persona.nombre;
var ape1 = persona["apellido1"];
var nombreCompleto = persona.getNombreCompleto();
var nombreCompletoCorchete = persona["getNombreCompleto"]();
La notación de corchetes es muy útil para acceder a una propiedades cuya clave está almacenada en una variable que contiene una cadena con el nombre de la misma.
2.2.1. Objetos anidados
Los objetos anidados son muy útiles para organizar la información y representar relaciones contiene con cardinalidades 1 a 1, o 1 a muchos:
var cliente = {
nombre: "Bruce Wayne",
email: "bruce@wayne.com",
direccion: {
calle: "Mountain Drive",
num: 1007,
ciudad: "Gotham"
}
};
También podemos asignar objetos:
var cliente = {};
cliente.nombre = "Bruce Wayne";
cliente.email = "bruce@wayne.com";
cliente.direccion = {};
cliente.direccion.calle = "Mountain Drive";
cliente.direccion.num = 1007;
cliente.direccion.ciudad = "Gotham";
Si accedemos a una propiedad que no existe, obtendremos undefined
. Si queremos evitarlo, podemos usar el operador ||
(or).
var nada = cliente.formaPago; // undefined
var pagoPorDfecto = cliente.formaPago || "Efectivo";
Un fallo muy común es acceder a un campo de una propiedad que no existe. El siguiente fragmento fallará, ya que el objeto direccion
no está definido.
var cliente = {};
cliente.direccion.calle = "Mountain Drive";
Si intentamos obtener un valor de una propiedad undefined
, se lanzará un excepción TypeError
. Para evitar la excepción, podemos usar el operación &&
(and).
var cliente = {};
cliente.direccion; // undefined
cliente.direccion.calle; // lanza TypeError
cliente.direccion && cliente.direccion.calle; // undefined
En resumen, un objeto puede contener otros objetos como propiedades. Por ello, podemos ver código del tipo variable.objeto.objeto.objeto.propiedad
o variable['objeto']['objeto']['objeto']['propiedad']
. Esto se conoce como encadenado de objeto (object chaining).
2.3. Creando un tipo de datos
Si quisiéramos crear dos personas con los mismos atributos que en el ejemplo anterior, tendríamos que repetir mucho código.
2.3.1. Función factoría
Para evitarlo, y que los objetos compartan un interfaz común, podemos crear una función factoría que devuelva el objeto.
function creaPersona(nom, ape1) {
return {
nombre : nom,
apellido1 : ape1,
getNombreCompleto : function() {
return this.nombre + " " + this.apellido1;
}
};
}
var persona = creaPersona("Aitor", "Medrano"),
persona2 = creaPersona("Domingo", "Gallardo");
Al tratarse de un lenguaje débilmente tipado, si queremos usar un objeto dentro de un método, es recomendable que comprobemos si existe el método que nos interesa del objeto. Por ejemplo, si queremos añadir un método dentro de una persona, que nos permita saludar a otra, tendríamos lo siguiente:
function creaPersona(nom, ape1) {
return {
nombre : nom,
apellido1 : ape1,
getNombreCompleto : function() {
return this.nombre + " " + this.apellido1;
},
saluda: function(persona) {
if (typeof persona.getNombreCompleto !== "undefined") { (1)
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
}
};
}
var persona = creaPersona("Aitor", "Medrano"),
persona2 = creaPersona("Domingo", "Gallardo");
persona.saluda(persona2); // Hola Domingo Gallardo
persona.saluda({}); // Hola colega
persona.saluda({ getNombreCompleto: "Bruce Wayne" }); // TypeError, la propiedad getNombreCompleto no es una función
1 | Comprobamos que el objeto contiene la propiedad que vamos a usar |
2.3.2. Función constructor
Otra manera más elegante y eficiente es utilizar es una función constructor haciendo uso de la instrucción new
, del mismo modo que creamos una fecha con var fecha = new Date()
.
Para ello, dentro de la función constructor que nombraremos con la primera letra en mayúscula (convención de código), crearemos las propiedades y los métodos mediante funciones y los asignaremos (ya no usamos la notación JSON) a propiedades de la función, tal que así:
var Persona = function(nombre, apellido1) {
this.nombre = nombre;
this.apellido1 = apellido1;
this.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
};
this.saluda = function(persona) {
if (persona instanceof Persona) { (1)
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
};
1 | Al tratarse de un objeto mediante función constructor, ya podemos consultar su tipo con instanceof |
instanceof
Mediante el operador Hay que tener en cuenta que devolverá
En cambio, devolverá
En resumen, |
Una vez creada la función, la invocaremos mediante la instrucción new
, la cual crea una nueva instancia del objeto:
var persona = new Persona("Aitor", "Medrano"),
persona2 = new Persona("Domingo", "Gallardo");
persona.saluda(persona2); // Hola Domingo Gallardo
persona.saluda({}); // Hola colega
persona.saluda({ getNombreCompleto: "Bruce Wayne" }); // Hola colega
También podíamos haber comenzado con una sintaxis similar a Java, es decir, en vez de una función expresión, mediante una función declaración. Con lo que sustituiríamos la primera línea por:
function Persona(nombre, apellido1) {
this.nombre = nombre;
this.apellido1 = apellido1;
// ....
}
var batman = new Persona("Bruce", "Wayne");
No olvides
new Mucho cuidado con olvidar la palabra clave Una manera de evitar esta posibilidad, es añadir una comprobación nada más declarar la función:
|
Los desarrolladores que vienen (¿venimos?) del mundo de Java preferimos el uso de funciones constructor, aunque realmente al usar una función constructor estamos consiguiendo lo mismo que una función factoría, pero con la variable this
siempre referenciando al objeto y no con un comportamiento dinámico como veremos más adelante. Más información: http://ericleads.com/2013/01/javascript-constructor-functions-vs-factory-functions/
En ambos casos, cada vez que creamos un objeto, los métodos vuelven a crearse y ocupan memoria. Así al crear dos personas, los métodos de getNombreCompleto
y saluda
se crean dos veces cada uno. Para solucionar esto, tenemos que usar la propiedad prototype
que veremos más adelante.
2.4. Invocación indirecta
Para poder reutilizar funciones de un objeto entre diferentes objetos, podemos hacer uso del método apply
. Este tipo de invocación se conoce como invocación indirecta y permite redefinir el valor de la variable this
:
Invocación Indirecta - http://jsbin.com/necohu/1/edit?js
var heroe = {
nombre: "Superheroe",
saludar: function() {
return "Hola " + this.nombre;
}
};
var batman = { nombre: "Batman" };
var spiderman = { nombre: "Spiderman" };
console.log(heroe.saludar()); // Hola Superheroe
console.log(heroe.saludar.apply(batman)); // Hola Batman
console.log(heroe.saludar.call(spiderman)); // Hola Spiderman
Los métodos apply
y call
son similares, con la diferencia de que mientras apply
además admite un segundo parámetro con un array de argumentos que pasar a la función invocada, call
admite un número ilimitado de parámetros que se pasarán a la función.
var heroe = {
nombre: "Superheroe",
saludar: function() {
return "Hola " + this.nombre;
},
despedirse: function(enemigo1, enemigo2) {
var malos = enemigo2 ? (enemigo1 + " y " + enemigo2) : enemigo1;
return "Adios " + malos + ", firmado:" + this.nombre;
}
};
var batman = { nombre: "Batman" };
var spiderman = { nombre: "Spiderman" };
console.log(heroe.despedirse()); // Adios undefined, firmado:Superheroe
console.log(heroe.despedirse.apply(batman, ["Joker", "Dos caras"])); // Adios Joker y Dos caras, firmado:Batman
console.log(heroe.despedirse.call(spiderman, "Duende Verde", "Dr Octopus")); // Adios Duende Verde y Dr Octopus, firmado:Spiderman
Una tercera aproximación es usar bind
, que funciona de manera similar a las anteriores, pero en vez de realizar la llamada a la función, devuelve una función con el contexto modificado.
var funcionConBatman = heroe.despedirse.bind(batman);
console.log(funcionConBatman("Pingüino")); // Adios Pingüino, firmado:Batman
console.log(funcionConBatman("Mr Frio")); // Adios Mr Frio, firmado:Batman
Antes de enamorarse de bind
, conviene destacar que forma parte de ECMAScript 5, por lo que los navegadores antiguos no lo soportan.
Se emplea sobre todo cuando usamos un callback y en vez de guardar una referencia a this
en una variable auxiliar (normalmente nombrada como that
), hacemos uso de bind
para pasarle this
al callback.
Por ejemplo, cuando se trabaja con AJAX suele suceder esto:
var that = this;
function callback(datos){
that.procesar(datos);
}
ajax(callback);
Ahora con bind
nos quedaría asi:
function callback(datos){
this.procesar(datos);
}
ajax(callback.bind(this));
2.5. Descriptores de propiedades
Al definir las propiedades mediante un objeto literal, estas se pueden tanto leer como escribir, ya sea mediante la notación .
o []
.
Si queremos que nuestro objeto contenga propiedades privadas, sólo hay que declararlas como variables dentro del objeto:
function Persona(nombre, apellido1) {
var tipo = "Heroe";
this.nombre = nombre;
this.apellido1 = apellido1;
}
var batman = new Persona("Bruce", "Wayne");
console.log(batman.nombre); // Bruce
console.log(batman.tipo); // undefined
Si lo que necesitamos es restringir el estado de las propiedades, a partir de ECMAScript 5, podemos usar:
-
un descriptor de datos para las propiedades que tienen un valor, el cual puede ser de sólo lectura, mediante
Object.defineProperties
-
o haciendo uso de los descriptores de acceso que definen dos funciones, para los métodos
get
yset
.
2.5.1. Definiendo propiedades
Vamos a recuperar el ejemplo de la función factoría que creaba una persona:
function creaPersona(nom, ape1) {
return {
nombre : nom,
apellido1 : ape1,
getNombreCompleto : function() {
return this.nombre + " " + this.apellido1;
}
};
}
Para definir las propiedades, haremos uso de Object.defineProperty()
rellenando las propiedades value
con la variable de la cual tomará el valor, y writable
con un booleano que indica si se puede modificar (si no la rellenamos, por defecto se considera que el atributo es de sólo lectura, es decir, false
)
function creaPersona(nom, ape1) {
var persona = {};
Object.defineProperty(persona, "nombre", {
value: nom,
writable: true
});
Object.defineProperty(persona, "apellido1", {
value: ape1,
writable: false
});
return persona;
}
De este modo, podemos crear personas, en las cuales podremos modificar el nombre pero no el apellido:
var batman = creaPersona("Bruce", "Wayne");
console.log(batman.nombre, batman.apellido1);
batman.nombre = "Bruno";
batman.apellido1 = "Díaz"; // No se lanza ningún error, pero no modifica
console.log(batman.nombre, batman.apellido1);
En vez de tener que crear una instrucción Object.defineProperty()
por propiedad, podemos agrupar y usar Object.defineProperties()
y simplificar el código:
function creaPersona(nom, ape1) {
var persona = {};
Object.defineProperties(persona, {
nombre: {
value: nom,
writable: true
},
apellido1: {
value: ape1,
writable: false
}
});
return persona;
}
Si en vez de utilizar una función factoría, queremos hacerlo mediante una función constructor, el funcionamiento es el mismo, sólo que el objeto que le pasaremos a Object.defineProperty()
será this
:
function Persona(nom, ape1) {
this.nombre = nom;
Object.defineProperties(this, {
apellido1: {
value: ape1,
writable: false
}
});
}
Finalmente, si en algún momento queremos consultar un descriptor de una propiedad haremos uso de Object.getOwnPropertyDescriptor(objeto, propiedad)
:
var batman = creaPersona("Bruce", "Wayne");
console.log(Object.getOwnPropertyDescriptor(batman, "nombre"));
// [object Object] {
// configurable: false,
// enumerable: false,
// value: "Bruce",
// writable: true
// }
2.5.2. Get y Set
Los descriptores de acceso sustituyen a los métodos que modifican las propiedades. Para ello, vamos a crear una propiedad nombreCompleto
con sus respectivos métodos de acceso y modificación:
function creaPersona(nom, ape1) {
var persona = {};
Object.defineProperties(persona, {
nombre: {
value: nom,
writable: true
},
apellido1: {
value: ape1,
writable: false
},
nombreCompleto: {
get: function() { (1)
return this.nombre + " " + this.apellido1;
},
set: function(valor) { (2)
this.nombre = valor;
this.apellido1 = valor;
}
}
});
return persona;
}
1 | Método de acceso |
2 | Método de modificación. Destacar que como hemos definido la propiedad 'apellido1' como de sólo lectura, no va a cambiar su valor |
De este modo podemos obtener la propiedad del descriptor de acceso como propiedad en vez de como método:
var batman = creaPersona("Bruce", "Wayne");
console.log(batman.nombreCompleto); // Bruce Wayne
batman.nombreCompleto = "Bruno Díaz";
console.log(batman.nombreCompleto); // Bruno Díaz Wayne
Una propiedad no puede contener al mismo tiempo el atributo value y get o set , es decir no puede ser descriptor de datos y de acceso al mismo tiempo.
|
2.5.3. Iterando sobre las propiedades
Si necesitamos acceder a todas las propiedades que contiene un objeto, podemos recorrerlas como si fueran una enumeración mediante un bucle for in
:
for (var prop in batman) {
console.log(batman[prop]);
}
O hacer uso del método Object.keys(objeto)
, el cual nos devuelve las propiedades del objeto en forma de array:
var propiedades = Object.keys(batman);
Por defecto las propiedades definidas mediante descriptores no se visualizan al recorrerlas. Para poder visualizarlas, tenemos que configurar la propiedad enumerable
para cada propiedad que queramos obtener:
function creaPersona(nom, ape1) {
var persona = {};
Object.defineProperties(persona, {
nombre: {
value: nom,
enumerable: true (1)
},
apellido1: {
value: ape1,
enumerable: true
},
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
enumerable: false (2)
}
});
return persona;
}
var batman = creaPersona("Bruce", "Wayne");
console.log(Object.keys(batman)); (3)
1 | Marcamos la propiedad como enumerable |
2 | La marcamos para que no aparezca |
3 | Obtenemos un array con ["nombre", "apellido1"] |
2.5.4. Modificando una propiedad
Si por defecto intentamos redefinir una propiedad que ya existe, obtendremos un error. Para poder hacer esto, necesitamos configurar la propiedad configurable
a true
, ya que por defecto, si no la configuramos es false
.
Así pues, si ahora queremos que la propiedad de nombreCompleto
devuelva el apellido y luego el nombre separado por una coma, necesitaríamos lo siguiente:
function creaPersona(nom, ape1) {
var persona = {};
Object.defineProperties(persona, {
nombre: {
value: nom,
},
apellido1: {
value: ape1,
},
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
configurable: true (1)
}
});
return persona;
}
Object.defineProperty(persona, "nombreCompleto", { (2)
get: function() {
return this.apellido1 + ", " + this.nombre;
}
});
1 | Permitimos modificar la propiedad una vez definido el objeto |
2 | Redefinimos la propiedad |
2.6. Prototipos
Los prototipos son una forma adecuada de definir tipos de objetos que permiten definir propiedades y funcionalidades que se aplicarán a todas las instancias del objeto. Es decir, es un objeto que se usa como fuente secundaria de las propiedades. Así pues, cuando un objeto recibe un petición de una propiedad que no contiene, buscará la propiedad en su prototipo. Si no lo encuentra, en el prototipo del prototipo, y así sucesivamente.
A nivel de código, todos los objetos contienen una propiedad prototype
que inicialmente referencia a un objeto vacío. Esta propiedad no sirve de mucho hasta que la función se usa como un constructor.
Por defecto todos los objetos tienen como prototipo raíz Object.prototype
, el cual ofrece algunos métodos que comparten todos los métodos, como toString
. Si queremos averiguar el prototipo de un objeto podemos usar la función Object.getPrototypeOf(objeto)
.
console.log(Object.getPrototypeOf({}) == Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
2.6.1. Constructores y prototype
Al llamar a una función mediante la instrucción new
provoca que se invoque como un constructor. El constructor asocia la variable this
al objeto creado, y a menos que se indique, la llamada devolverá este objeto.
Este objeto se conoce como una instancia de su constructor. Todos los constructores (de hecho todas las funciones) automáticamente contienen la propiedad prototype
que por defecto referencia a un objeto vacío que deriva de Object.prototype
.
Cada instancia creada con este constructor tendrá este objeto como su prototipo. Con lo que para añadir nuevos métodos al constructor, hemos de añadirlos como propiedades del prototipo.
var Persona = function(nombre, apellido1) {
this.nombre = nombre;
this.apellido1 = apellido1;
}
Persona.prototype.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
};
Persona.prototype.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
var persona = new Persona("Aitor", "Medrano"),
persona2 = new Persona("Domingo", "Gallardo");
persona.saluda(persona2); // Hola Domingo Gallardo
persona.saluda({}); // Hola colega
persona.saluda({ getNombreCompleto: "Bruce Wayne" }); // Hola colega
Una vez definido el prototipo de un objeto, las propiedades del prototipo se convierten en propiedades de los objetos instanciados. Su propósito es similar al uso de clases dentro de un lenguaje clásico orientado a objeto. De hecho, el uso de prototipos en JavaScript se plantea para poder compartir código de manera similar al paradigma orientado a objetos.
Ya hemos comentado que todo lo que colocamos en la propiedad prototype
se comparte entre todas las instancias del objeto, por lo que las funciones que coloquemos dentro compartirán una única instancia entre todas ellas.
Si queremos compartir el prototipo entre diferentes objetos, podemos usar Object.create
para crear un objeto con un prototipo específico, aunque es mejor usar una función constructor.
2.6.2. prototype
y __proto__
Supongamos el siguiente objeto vacío:
var objeto = {};
console.dir(objeto);
Si inspeccionamos la consola, podemos observar la propiedad __proto__
con todas sus propiedades:
Así pues tenemos que todo objeto contiene una propiedad __proto__
que incluye todas las propiedades que hereda nuestro objeto. Es algo así como el padre del objeto. Para recuperar este prototipo podemos usar el método Object.getPrototypeOf()
o directamente navegar por la propiedad __proto__
, es decir, son intercambiables.
Es importante destacar la diferencia entre el modo que un prototipo se asocia con un constructor (a través de la propiedad prototype
) y el modo en que los objetos tienen un prototipo (el cual se puede obtener mediante Object.getPrototypeOf
). El prototipo real de un constructor es Function.prototype
ya que todos los constructores son funciones. Su propiedad prototype
será el prototipo de las instancias creadas mediante su constructor, pero no su propio prototipo.
Los prototipos en JavaScript son especiales por lo siguiente: cuando le pedimos a JavaScript que queremos invocar el método push
de un objeto, o leer la propiedad x
de otro objeto, el motor primero buscará dentro de las propiedades del propio objeto. Si el motor JS no encuentra lo que nosotros queremos, seguirá la referencia __proto__
y buscará el miembro en el prototipo del objeto.
Veamos un ejemplo mediante código:
function Heroe(){
this.malvado = false;
this.getTipo = function() {
return this.malvado ? "Malo" : "Bueno";
};
}
Heroe.prototype.atacar = function() {
return this.malvado ? "Ataque con Joker" : "Ataque con Batman";
}
var robin = new Heroe();
console.log(robin.getTipo()); // Bueno
console.log(robin.atacar()); // Ataque con Batman
var lexLuthor = new Heroe();
lexLuthor.malvado = true;
console.log(lexLuthor.getTipo()); // Malo
console.log(lexLuthor.atacar()); // Ataque con Joker
var policia = Object.create(robin);
console.log(policia.getTipo()); // Bueno
console.log(policia.__proto__.atacar()); // Ataque con Batman
Ya hemos visto que cada objeto en JavaScript tiene una propiedad prototype
. No hay que confundir esta propiedad prototype
con la propiedad __proto__
, ya que ni tienen el mismo propósito ni apuntan al mismo objeto:
-
Array.__proto__
nos da el prototipo deArray
, es decir, el objeto del que hereda la funciónArray
. -
Array.prototype
, en cambio, es el objeto prototipo de todos los arrays, que contiene los métodos que heredarán todos los arrays.
Para finalizar, relacionado con estos conceptos tenemos la instrucción new
en JavaScript, la cual realiza tres pasos:
-
Primero crea un objeto vacío.
-
A continuación, asigna la propiedad
__proto__
del nuevo objeto a la propiedadprototype
de la función invocada -
Finalmente, el operador invoca la función y pasa el nuevo objeto como la referencia
this
.
Un artículo muy gráfico que explica la diferencia entre estas dos propiedades es Prototypes and Inheritance in JavaScript de Scott Allen : http://msdn.microsoft.com/en-us/magazine/ff852808.aspx |
2.7. Herencia
JavaScript es un lenguaje de herencia prototipada, lo que significa que un objeto puede heredar directamente propiedades de otro objeto a partir de su prototipo, sin necesidad de crear clases.
Ya hemos visto que mediante la propiedad prototype
podemos asociar atributos y métodos al prototipo de nuestras funciones constructor.
Retomemos el ejemplo del objeto Persona
mediante la función constructor:
var Persona = function(nombre, apellido1) {
this.nombre = nombre;
this.apellido1 = apellido1;
this.nombreCompleto = function() {
return this.nombre + " " + this.apellido1;
};
this.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
};
Para mejorar el uso de la memoria y reducir la duplicidad de los métodos, hemos de llevar los métodos al prototipo. Para ello, crearemos descriptores de acceso para las propiedades y llevaremos los métodos al prototipo de la función constructor:
var Persona = function(nombre, apellido1) {
this.nombre = nombre;
this.apellido1 = apellido1;
};
Object.defineProperties(Persona.prototype, { (1)
nombreCompleto: {
get: function() { (2)
return this.nombre + " " + this.apellido1;
},
enumerable: true
}
});
Persona.prototype.saluda = function(persona) { (3)
if (persona instanceof Persona) {
return "Hola " + persona.nombreCompleto;
} else {
return "Hola colega";
}
};
var batman = new Persona("Bruce", "Wayne");
console.log(batman.nombreCompleto); // Bruce Wayne
console.log(batman.saluda()); // Hola colega
var superman = new Persona("Clark", "Kent");
console.log(batman.saluda(superman)); // Hola Clark Kent
1 | Fijamos la definición de propiedades en el prototipo de la función constructor |
2 | Añadimos una propiedad de acceso por cada propiedad |
3 | Añadimos los métodos al prototipo |
Si queremos incluir los métodos como dentro de los descriptores de propiedades, podemos añadiros como valores:
Object.defineProperties(Persona.prototype, {
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
enumerable: true
},
saluda: {
value: function(persona) { (1)
if (persona instanceof Persona) {
return "Hola " + persona.nombreCompleto;
} else {
return "Hola colega";
}
},
enumerable: true
}
});
1 | El método saluda ahora es una propiedad cuyo valor es una función |
Para crear un objeto que utilice el prototipo de otro hemos de hacer uso del método Object.create(objetoPrototipo)
. De este modo, el objetoPrototipo se convierte en el prototipo del objeto devuelto, y así podemos acceder al objeto padre mediante Object.getPrototypeOf(objetoPrototipo)
o la propiedad __proto__
(deprecated):
var Empleado = Object.create(Persona);
console.log(Empleado.hasOwnProperty('nombreCompleto')); // false
console.log(Empleado.__proto__ === Persona); // true
console.log(Object.getPrototypeOf(Empleado) === Persona); // true
Si queremos realizar herencia entre objetos, el proceso se realiza en dos pasos:
-
Heredar el constructor
-
Heredar el prototipo
2.7.1. Herencia de constructor
Si usamos funciones constructor, podemos realizar herencia de constructor para que el hijo comparta las mismas propiedades que el padre. Para ello, el hijo debe realizar una llamada al padre y definir sus propios atributos.
Por ejemplo, supongamos que queremos crear un objeto Empleado
que se base en Persona
, pero añadiendo el campo cargo
con el puesto laboral del empleado:
var Empleado = function(nombre, apellido1, cargo) {
Persona.call(this, nombre, apellido1); (1)
this.cargo = cargo;
};
1 | Llamamos al constructor del padre mediante call para que this tome el valor del hijo. |
2.7.2. Herencia de prototipo
Una vez heredado el constructor, necesitamos heredar el prototipo para compartir los métodos y si fuese el caso, sobrescribirlos.
Para ello, mediante Object.create(prototipo, propiedades)
vamos a definir los métodos del hijo y si quisiéramos sobrescribir los métodos que queramos del padre:
Empleado.prototype = Object.create(Persona.prototype, { (1)
saluda: { // sobreescribimos los métodos que queremos
value: function(persona) {
if (persona instanceof Persona) {
return Persona.prototype.saluda.call(this) + " (desde un empleado)"; (2)
} else {
return "Hola trabajador";
}
},
writable: false, (3)
enumerable: true
},
nombreCompleto: {
get: function() { (4)
var desc = Object.getOwnPropertyDescriptor(Persona.prototype, "nombreCompleto"); (5)
return desc.get.call(this) + ", " + this.cargo; (6)
},
enumerable: true
}
});
1 | Redefinimos el prototipo de Empleado con el de Persona |
2 | Dentro del método que sobreescribimos, podemos realizar una llamada al mismo método pero del padre, haciendo uso de call para redefinir el objeto this |
3 | Podemos marcar el método como no modificable para evitar que se pueda sobreescribir |
4 | Si queremos, también podemos sobreescribir los descritores de propiedades |
5 | Para acceder a una propiedad del padre hemos de hacerlo mediante Object.getOwnPropertyDescriptor |
6 | Igual que en punto 2, accedemos al descriptor pero asociándole el objeto this del hijo al padre. |
Un artículo interesante sobre la OO y la herencia en JavaScript es OOP in JavaScript: What you NEED to know: http://javascriptissexy.com/oop-in-javascript-what-you-need-to-know/ |
2.8. this
y el patrón invocación
Una de las cosas que diferencia JavaScript de otros lenguajes de programación es que la variable this
toma diferentes valores dependiendo de cómo se invoque la función o fragmento donde se encuentra.
A la hora de invocar una función lo podemos hacer de cuatro maneras, las cuales se conocen como el patrón invocación:
-
El patrón de invocación como método
-
El patrón de invocación como función
-
El patrón de invocación como constructor
-
El patrón de invocación con apply y call
Este patrón define como se inicializa this
en cada caso.
2.8.1. Invocación como método
Se conoce como método a aquella función que se almacena como una propiedad de un objeto. En este caso this
se inicializa con el objeto al que pertenece la función.
var obj = {
valor : 0,
incrementar: function(inc){
this.valor += inc;
}
};
obj.incrementar(3);
console.log(obj.valor); // 3
Al asociarse el valor de this
en tiempo de invocación (y no de compilación), fomenta que el código sea altamente reutilizable.
Los métodos que hacen uso de this
para obtener el contexto del objeto se conocen como métodos públicos.
2.8.2. Invocación como Función
Cuando una función no es una propiedad de un objeto, se invoca como función, y this
se inicializa con el objeto global (al trabajar con un navegador, el objeto window
).
function suma(a,b) {
console.log(a+b);
console.log(this);
}
suma(3,5);
Y por la consola aparece tanto el resultado como todo el objeto window
:
8
Window {top: Window, window: Window, location: Location, ... }
Esto puede ser un problema, ya que cuando llamamos a una función dentro de otra, this
sigue referenciando al objeto global y si queremos acceder al this
de la función padre tenemos que almacenarlo previamente en una variable:
var obj = {
valor: 0,
incrementar: function(inc) { (1)
var that = this;
function otraFuncion(unValor) { (2)
that.valor += unValor;
}
otraFuncion(inc);
}
};
obj.incrementar(3);
console.log(obj.valor); // 3
1 | Se invoca como método y this referencia al propio objeto |
2 | Se invoca como función y this referencia al objeto global |
Y si hacemos uso de call
podemos conseguir lo mismo pero sin necesidad de almacenar this
en una variable auxiliar:
var objBind = {
valor: 0,
incrementar: function(inc) {
function otraFuncion(unValor) {
this.valor += unValor;
}
otraFuncion.call(this, inc); (1)
}
}
1 | Al invocar a una función, le indicamos que toma la referencia this del objeto en vez del global |
2.8.3. Invocación como constructor
Ya hemos visto que JavaScript ofrece una sintaxis similar a la creación de objetos en Java. Dicho esto, cuando invocamos una función mediante new
se creará un objeto con una referencia al valor de la propiedad prototype
de la función (también llamado constructor) y this
tendrá una referencia a este nuevo objeto.
var Persona = function() { // constructor
this.nombre = 'Aitor';
this.apellido1 = "Medrano";
}
Persona.prototype.getNombreCompleto = function(){
return this.nombre + " " + this.apellido1;
}
var p = new Persona();
console.log(p.getNombreCompleto()); // Aitor Medrano
2.8.4. Invocación con apply
Como JavaScript es un lenguaje orientado a objetos funcional, las funciones pueden contener métodos.
El método apply
nos permite, además de construir un array de argumentos que usaremos al invocar una función, elegir el valor que tendrá this
, lo cual permite reescribir el valor de this
en tiempo de ejecución.
Para ello, apply
recibe 2 parámetros, el primero es el valor para this
y el segundo es un array de parámetros.
Usando el ejemplo anterior de prototipado, vamos a cambiar el this
utilizando apply.
var Persona = function() { // constructor
this.nombre = "Aitor";
this.apellido1 = "Medrano";
}
Persona.prototype.getNombreCompleto = function(){
return this.nombre + " " + this.apellido1;
}
var otraPersona = {
nombre: "Rubén",
apellido1: "Inoto"
}
var p = new Persona();
console.log(p.getNombreCompleto()); // Aitor Medrano
console.log(p.getNombreCompleto().apply(otraPersona)); // Rubén Inoto
Así pues, el método apply
realiza una llamada a una función pasándole tanto el objeto que va a tomar el papel de this
como un array con los parámetros que va a utilizar la función.
Autoevaluación -
this ¿Cual es el resultado del siguiente fragmento de código? [1]
|
2.9. Arrays
Se trata de un tipo predefinido que, a diferencia de otros lenguajes, es un objeto. Del mismo modo que los tipos básicos, lo podemos crear de la siguiente manera:
var cosas = new Array();
var tresTipos = new Array(11, "hola", true);
var longitud = tresTipos.length; // 3
var once = tresTipos[0];
Podemos observar que en JavaScript los arrays pueden contener tipos diferentes, que el primer elemento es el 0 y que podemos obtener su longitud mediante la propiedad length
.
Igual que antes, aunque se pueden crear los arrays de este modo, realmente se crean e inicializan con la notación de corchetes de JSON:
var tresTipos = [11, "hola", true];
var once = tresTipos[0];
Podemos añadir elementos sobre la marcha y en la posiciones que queramos (aunque se recomienda añadir los elementos en posiciones secuenciales).
tresTipos[3] = 15;
tresTipos[tresTipos.length] = "Bruce";
var longitud2 = tresTipos.length; // 5
tresTipos[8] = "Wayne";
var longitud3 = tresTipos.length; // 9
var nada = tresTipos[7]; // undefined
Cabe destacar que si accedemos a un elemento que no contiene ningún dato obtendremos undefined
.
Autoevaluación
¿Sabes cual es el contenido del array |
Por lo tanto, si añadimos elementos en posiciones mayores al tamaño del array, éste crecerá con valores undefined
hasta el elemento que añadamos.
Si en algún momento quisiéramos eliminar un elemento del array, hemos de usar el operador delete
sobre el elemento en cuestión.
delete tresTipos[1];
El problema es que delete
deja el hueco, y por tanto, la longitud del array no se ve reducida, asignándole undefined
al elemento en cuestión.
2.9.1. Manipulación
Los arrays soportan los siguientes métodos para trabajar con elementos individuales:
Métodos | Propósito |
---|---|
pop() |
Extrae y devuelve el último elemento del array |
push(elemento) |
Añade el elemento en la última posición |
shift() |
Extrae y devuelve el primer elemento del array |
unshift(elemento) |
Añade el elemento en la primera posición |
var notas = ['Suspenso', 'Aprobado', 'Bien', 'Notable', 'Sobresaliente'];
notas.push('Matrícula de Honor');
var matricula = notas.pop(); // "Matrícula de Honor"
var suspenso = notas.shift(); // "Suspenso"
notas.unshift('Suspendido');
console.log(notas);
Además, podemos usar los siguiente métodos que modifican los arrays en su conjunto:
Métodos | Propósito |
---|---|
concat(array2[,…, arrayN]) |
Une dos o más arrays |
join(separador) |
Concatena las partes de un array en una cadena, indicándole como parámetro el separador a utilizar |
reverse() |
Invierte el orden de los elementos del array, mutando el array |
sort() |
Ordena los elementos del array alfabéticamente, mutando el array |
sort(fcomparacion) |
Ordena los elementos del array mediante la función fcomparacion |
slice(inicio, fin) |
Devuelve un nuevo array con una copia con los elementos comprendidos entre inicio y fin (con índice |
splice(índice, cantidad, elem1[, …, elemN]) |
Modifica el contenido del array, añadiendo nuevos elementos mientras elimina los antiguos seleccionando a partir de índice la cantidad de elementos indicados. Si cantidad es 0, sólo inserta los nuevos elementos. |
Hay que tener en cuenta que los métodos mutables modifican el array sobre el que se realiza la operación:
var notas = ['Suspenso', 'Aprobado', 'Bien', 'Notable', 'Sobresaliente'];
notas.reverse();
console.log(notas); // ["Sobresaliente", "Notable", "Bien", "Aprobado", "Suspenso"]
notas.sort();
console.log(notas); // ["Aprobado", "Bien", "Notable", "Sobresaliente", "Suspenso"]
notas.splice(0, 4, "Apto"); (1)
console.log(notas); // ["Apto", "Suspenso"]
1 | A partir de la posición 0, borra 4 elementos y añade "Apto" . |
Autoevaluación
¿Cual es el valor de la variables Arrays Autoevaluación - http://jsbin.com/gawaju/1/edit?js
|
Una operación muy usual es querer ordenar un array de objetos por un determinado campo del objeto. Supongamos que tenemos los siguientes datos:
var personas = [
{nombre:"Aitor", apellido1:"Medrano"},
{nombre:"Domingo", apellido1:"Gallardo"},
{nombre:"Alejandro", apellido1:"Such"}
];
personas.sort(function(a,b) {
if (a.nombre < b.nombre)
return -1;
if (a.nombre > b.nombre)
return 1;
return 0;
});
Por lo tanto, la función de comparación siempre tendrá la siguiente forma:
function compare(a, b) {
if (a es menor que b según criterio de ordenamiento) {
return -1;
}
if (a es mayor que b según criterio de ordenamiento) {
return 1;
}
// a debe ser igual b
return 0;
}
Finalmente, vamos a estudiar los métodos slice
y splice
que son menos comunes.
var frutas = ["naranja", "pera", "manzana", "uva", "fresa", "naranja"];
var arrayUvaFresa = frutas.slice(3, 5); (1)
var uvaFresa = frutas.splice(3, 2, "piña"); (2)
1 | Crea un nuevo array con los elementos comprendidos entre el tercero y el quinto, quedando ["uva", "fresa"] |
2 | Tras borrar dos elementos a partir de la posición tres, añade piña . En uvaFresa se almacena un array con los elementos eliminados (["uva", "fresa"] ), mientras que frutas se queda con ["naranja", "pera", "manzana", "piña", "naranja"] . |
Si lo que queremos es buscar un determinado elemento dentro de un array:
Método | Propósito |
---|---|
indexOf(elem[, inicio]) |
Devuelve la primera posición (0..n-1) del elemento comenzando desde el principio o desde inicio |
lastIndexOf(elem[, inicio]) |
Igual que |
En ambos casos, si no encuentra el elemento, devuelve -1
.
Autoevaluación
¿Cual es el valor de la variable
|
2.9.2. Iteración
Los siguiente métodos aceptan una función callback como primer argumento e invocan dicha función para cada elemento del array. La función que le pasamos a los métodos reciben tres parámetros:
-
El valor del elemento del array
-
El índice del elemento
-
El propio array
La mayoría de las veces sólo necesitamos utilizar el valor.
Los métodos que podemos utilizar son:
Método | Propósito |
---|---|
forEach(función) |
Ejecuta la función para cada elemento del array |
map(función) |
Ejecuta la función para cada elemento del array, y el nuevo valor se inserta como un elemento del nuevo array que devuelve. |
every(función) |
Verdadero si la función se cumple para todos los valores. Falso en caso contrario (Similar a una conjunción → Y) |
some(función) |
Verdadero si la función se cumple para al menos un valor. Falso si no se cumple para ninguno de los elementos (Similar a un disyunción → O) |
filter(función) |
Devuelve un nuevo array con los elementos que cumplen la función |
reduce(función) |
Ejecuta la función para un acumulador y cada valor del array (de inicio a fin) se reduce a un único valor |
A continuación vamos a estudiar algunos de estos métodos mediante ejemplos.
Si queremos pasar a mayúsculas todos los elementos del array, podemos usar la función map()
, la cual se ejecuta para cada elemento del array:
var heroes = ["Batman", "Superman", "Ironman", "Thor"];
function mayus(valor, indice, array) {
return valor.toUpperCase();
}
var heroesMayus = heroes.map(mayus);
console.log(heroesMayus); // ["BATMAN", "SUPERMAN", "IRONMAN", "THOR"]
O si queremos mostrar todos los elementos del array, podemos hacer uso del método forEach
:
var heroes = ["Batman", "Superman", "Ironman", "Thor"];
heroes.forEach(function(valor, indice) {
console.log("[", indice, "]=", valor);
});
O si tenemos un array con números también podemos sumarlos mediante forEach
:
var numeros = [1, 3, 5, 7, 9];
var suma = 0;
numeros.forEach(function(valor) {
suma += valor
});
Si queremos comprobar si todos los elementos de un array son cadenas, podemos utilizar el método every()
. Para ello, crearemos una función esCadena
:
function esCadena(valor, indice, array) {
return typeof valor === "string";
}
console.log(frutas.every(esCadena)); // true
Si tenemos un array con datos mezclados con textos y números, podemos quedarnos con los elementos que son cadenas mediante la función filter()
.
var mezcladillo = [1, "dos", 3, "cuatro", 5, "seis"];
console.log(mezcladillo.filter(esCadena)); // ["dos", "cuatro", "seis"]
Finalmente, mediante la función reduce
podemos realizar un cálculo sobre los elementos del array, por ejemplo, podemos contar cuantas veces aparece una ocurrencia dentro de un array o sumar sus elementos. Para ello, la función recibe dos parámetros con el valor anterior y el actual:
var numeros = [1, 3, 5, 7, 9];
var suma = numeros.reduce(function(anterior, actual) {
return anterior + actual
});
En el primer paso, como no hay valor anterior, se pasan el primer y el segundo elemento del array (los valores 1 y 3). En siguientes iteraciones, el valor anterior
es lo que devuelve el código, y el actual
es el siguiente elemento del array. De este modo, estamos cogiendo el valor actual y sumándoselo al valor anterior (el total acumulado)
arguments a arraySi queremos convertir el pseudo-array
|
2.10. Destructurar
Una novedad que ofrece ES6 es la posibilidad de destructurar tanto arrays como objetos para transformar una estructura de datos compuesta, tales como objetos y arrays, en diferentes datos individuales. Así pues, en vez de asignar una a una las asignaciones para cada variable mediante múltiples sentencias, al destructurar podemos asignar los valores a múltiples variables en una única sentencia.
En el caso de los arrays, hemos de asignar un array a las variables entre corchetes ([]
):
var numeros = [10, 20];
var [n1, n2] = numeros; // destructurando
console.log(n1); // 10
console.log(n2);
De la misma manera, si queremos extraer los datos de un objeto, lo haremos mediante llaves ({}
):
var posicion = {x: 50, y: 100};
var {x, y} = posicion; // destructurando
console.log(x); // 50
console.log(y); // 100
La sintaxis de destructurar también permite emplear su uso como parámetros de las funciones declaración, ya sea un objeto o un array.
persona.saluda(persona2); (1)
persona.saluda({nombre, apellido1}); (2)
1 | Recibe un objeto como parámetro, y por tanto dentro de la función saluda accederá a los elementos mediante persona2.nombre y persona2.apellido1 |
2 | Recibe un objeto destructurado como parámetro, y por tanto dentro de la función saluda accederá a los elementos mediante nombre y apellido1 directamente |
2.11. Ejercicios
2.11.1. (0.4 ptos) Ejercicio 21. Objeto Usuario
A partir del siguiente objeto el cual se crea mediante una función factoría:
function crearUsuario(usu, pas) {
return {
login: usu,
password: pas,
autenticar: function(usu, pas) {
return this.login === usu && this.password == pas;
}
};
}
Refactoriza el código utilizando una función constructor que haga uso de descriptores de datos y acceso, de manera que no permita consultar el password una vez creado el objeto Usuario
.
Un ejemplo de ejecución sería:
var usuario = new Usuario("l1", "p1");
console.log("login: " + usuario.login); // l1
console.log("password: " + usuario.password); // undefined
usuario.password = "p2";
console.log("password: " + usuario.password); // undefined
console.log("auth? " + usuario.autenticar("l1", "l1")); // false
console.log("auth? " + usuario.autenticar("l1", "p1")); // true
El método autenticar
quedará como un método el objeto, es decir, no como una propiedad get/set.
El objeto se almacenará en una archivo denominado ej21.js
.
2.11.2. (0.4 ptos) Ejercicio 22. Herencia
Crea un objeto Persona
que herede de Usuario
, pero que añada atributos para almacenar el nombre y el email.
Al recuperar el nombre de una persona, si no tiene ninguno, debe devolver el login del usuario.
Tanto el objeto Usuario
como el objeto Persona
se almacenarán en un archivo denominado ej22.js
.
2.11.3. (0.3 ptos) Ejercicio 23. String.repetir()
Define una función repetir
dentro del objeto String
para que, haciendo uso del prototipo, acepte un entero con el número de ocasiones que tiene que repetir la cadena. Por ejemplo:
console.log("Viva JavaScript ".repetir(3));
En el caso de recibir un tipo inesperado o negativo, deberá lanzar un error informativo.
La función se almacenará en una archivo denominado ej23.js
.
2.11.4. (0.4 ptos) Ejercicio 24. Arrays
Sin utilizar las instrucciones for
ni while
, realiza las siguientes funciones:
-
Función
reverseCopia(array)
que a partir de un array, devuelva una copia del mismo pero en orden inverso (no se puede utilizar el métodoreverse
) -
Función
union(array1, array2 [,…arrayN])
que a partir de un número variable de arrays, devuelva un array con la unión de sus elementos.. Cada elemento sólo debe aparecer una única vez.
Por ejemplo:
var frutas = ["naranja", "pera", "manzana", "uva", "fresa", "kiwi"];
var saturf = miReverse(frutas);
console.log(frutas); // ["naranja", "pera", "manzana", "uva", "fresa", "kiwi"]
console.log(saturf); // ["kiwi", "fresa", "uva", "manzana", "pera", "naranja"]
var zumos = ["piña", "melocotón", "manzana", "naranja"];
var batidos = ["fresa", "coco", "chocolate"];
var sabores = union(frutas, zumos, batidos);
console.log(sabores); // ["naranja", "pera", "manzana", "uva", "fresa", "kiwi", "piña", "melocotón", "coco", "chocolate"]
La funciones se almacenarán en una archivo denominado ej24.js
.