SIMULAR EL MUNDO CON OBJETOS, PROPIEDADES Y VALORES: EL MODELO DE LA PROGRAMACIÓN ORIENTADA A OBJETOS
En nuestra vida lo tangible tiene propiedades táctiles y visualizables. Allí un objeto físico tiene textura, peso, y color; como una manzana. Y esta podría ser alterada por acción nuestra. Por ejemplo, la propiedad 'forma' cambiará si partimos la manzana. Pero en sistemas informáticos los objetos apenas contienen valores (textuales o numéricos) que los representan.
Aristóteles y la programación orientada a objetos
Un artículo (en inglés) por Adam Reed, publicado en el 2003, explora las relaciones entre los conceptos de la programación orientada a objetos y la causalidad aristotélica; en la cual "las entidades –acciones o métodos– actúan y modelan el estado de una realidad –una clase–", y viceversa.
Por ejemplo, para crear un objeto desde cero, luego agregarle valores, y luego accederlos, corresponde la siguiente sintaxis:
En Javascript
// Objeto vacio
let bandas = {};
// Insertar propiedades y valores
bandas["ViejoNapo"]= "Guayaquil";
bandas["U2"]= "Irlanda";
bandas["NTN"]= "Guayaquil";
console.log(bandas["ViejoNapo"]);
// Guayaquil
Es así que cuando los lenguajes de cómputo requieren representar un objeto del mundo real, deben añadir a esa estructura propiedades identificables mediante datos, como números o palabras.
El ejemplo de abajo hay objetos con dos tipos de datos: con propiedades pares de datos, y con propiedades de acceso:
// OBJETO CON PROPIEDADES DE DATOS
var objeto = {
prop1: 'valorA',
prop2: 'valorB'
};
// Invocar
console.log(objeto.prop1);
// valorA
// OBJETO CON PROPIEDADES DE ACCESO (a una función, por ejemplo)
O, creando primero el objeto y luego darle valores pares (propiedad: "valor") gracias a la palabra clave Object
, que funciona como un generador (constructor) de objetos:
var artista = new Object();
// Insertando propiedades/valores
artista.rock = "NTN";
artista.salsa = "Cuchitos";
artista.cumbia = "Duo Rosario";
// Invocando
console.log(artista);
// { rock: 'NTN', salsa: 'Cuchitos', cumbia: 'Duo Rosario' }
console.log("Una banda de rock es " + artista.rock);
// Una banda de rock es NTN
// OJO. DETECTAR VALORES DE PROPIEDAD
/* Esté método detecta incluso si la propiedad tiene
un valor (sea este false, o undefined).*/
console.log("rock" in artista);
// True
O al borrar mediante el operador delete
una de las propiedades del objeto ya creado mediante constructor new Object()
. Cuando delete
halla y borra el elemento dentro del objeto retornará (por interno) un valor de true
.
var color = new Object();
color.rojo = "rojo";
color.verde = "verde";
// Invocar
console.log("Mi color es " + color.rojo);
// Mi color es rojo
// Devuelve true si lo halla y lo borra
delete color.rojo;
// Item borrado
if (delete color.rojo == true) {console.log("Item borrado");}
// Confirmar que ya no está en el objeto
console.log(color);
// { verde: 'verde' }
Y con objetos insertados –uno dentro de otro– con la siguiente sintaxis. Recordar que la coma siempre separará objetos de otros objetos, o valores pares de otros valores. Solo el último elemento del objeto carece de coma.
En Javascript
var objeto = {
// Propiedades: 'valores'
prop0: 'valorA',
prop1: 'valorB',
objAnidado: {
prop2: 'valorC',
prop3: 'valorD'
}};
// Invocar valor de objeto anidado
console.log(objeto.objAnidado.prop2);
//-> valorC
Luego con ejemplos:
// Objeto manzana
var manzana = {
// Propiedades con sus valores
color: 'rojo',
forma: "redonda",
tipo: "fruta",
// Objeto anidado con propiedades y sus valores
origen: {
chilena: true,
española: false,
ecuatoriana: false
}};
// Invocando propiedades y valores
// "..." + nombreObjeto.propiedad
console.log("La manzana tiene color " +
manzana.color);
//-> La manzana tiene color rojo
Por otro lado, un método de objeto es una función dentro del objeto que actuará como un valor de una propiedad. Si bien las propiedades y sus valores (como 'precioUnidad') ya cuentan con valores dentro del objeto, no podremos acceder a estas sin el operador this
. Este operador 'desbloquea' el acceso (desde un mismo ámbito local) y nos permite intercambiar datos desde una función a una propiedad desde el mismo objeto.
Luego, habrá una diferencia entre concatenar valores de propiedades, contra operarlos (sumar, restar, etc.). Para que dos o más valores de propiedad sean operables, deberemos primero obtenerlos desde this.propiedad
y luego encerrar entre paréntesis la expresión operable (literal A, siguiente código).
Observar que al final del código solo estamos invocando a un método (alojado en la variable 'almuerzo') pero ubicada dentro del objeto 'comida':
var comida = {
desayuno: "Café y bolón",
precioUnidad: 15.5,
almuerzo: function() {
console.log(this.desayuno +
" o menestra y carne" +
" a $" + (this.precioUnidad + this.precioUnidad));// 🅰
}
};
comida.almuerzo();
// Café y bolón o menestra y carne a $15.5
Por lo tanto, un objeto puede contener datos tipo "propiedad:valor"
. Pero también puede contener objetos y funciones. La gran diferencia entre ellos es la forma de obtenerlos: un objeto dentro de objeto se lo accede mediante sintaxis de punto; mientras que una función dentro de objeto se la invoca desde su nombre identificador.
A UN VALOR 'OBJETO' LO ACCEDEMOS
nombre_Objeto . objeto_Anidado . propiedad_Objeto
A UN VALOR 'FUNCIÓN' LA INVOCAMOS
objetoConFuncion . propiedadFuncion();
// Objeto contiene objeto
var objetoConObjeto = { a: 1, b:2,
objeto:{ c:3, d:4 }
}
// ACCEDEMOS
console.log(objetoConObjeto.objeto.d);
// 4
// ---*---
// Objeto contiene función
var objetoConFuncion = { a: 1, b:2,
objeto: function(){ console.log(2*2); }
}
// 4
// INVOCAMOS
objetoConFuncion.objeto();
Según Antani y Stefanov[4], una analogía literaria con el modelo de programación orientada a objetos sería la siguiente:
- Un sustantivo –como una manzana– sería el objeto a modelar.
- Los verbos –como las acciones a ejecutar– serían los métodos.
- Los adjetivos –color: "rojo", velocidad: 200- serían las propiedades y sus valores, respectivamente.
CLASES Y OBJETOS: INTRODUCCIÓN
En la naturaleza existen millones de seres. Por ejemplo, para que su estudio no sea más complejo de lo que ya es, la ciencia agrupa cada gran conjunto de seres vivos en reinos.
Uno de esos reinos es el animal. Al fin y al cabo, cada reino corresponde a un inmenso conjunto de clases de seres. Luego, cada clase contendrá subclases según una jerarquía más detallada de propiedades, géneros, etcétera.
En programación para cómputo orientada a objetos se intenta mantener esa organización de elementos mediante entidades llamadas objetos y clases.
Para Antani y Stefanov[4], en programación orientada a objetos estos se agrupan en una gran clase cuando hay similitudes entre objetos. Así mismo, una gran clase permite crear nuevas subclases llamadas hijos. Estos hijos contendrán gran parte de las características de la clase padre, como iguales propiedades (por ejemplo, color) pero diferentes valores (una clase tendrá color rojo; otra verde).
En código para computadores cada nuevo objeto es creado en una instancia –del neologismo instance–, que refiere a un 'ejemplar' o 'espécimen'. Una clase padre instancia varios objetos hijos a partir de un solo conjunto de propiedades. Pero los valores de esas instancias sí podrían ser diferentes, evitando así duplicaciones confusas.
Javascript no es un lenguaje puro del modelo de clases. ¿Por qué se dice eso?: porque JS no muestra una sintaxis más elaborada –o más compleja– para crear objetos hijo desde clases, como sí lo tienen otros lenguajes, como C++.
Para otros autores la importancia no está en las clases, sino en los objetos, que son los verdaderos representantes digitales del mundo real en programación.
Para Marin Benčević en "Why JavaScript is an OOP (Object Oriented programming) Language" una clase no contiene valores de importancia, solo sus objetos. Son las clases quienes dan información clave a las instancias. Finalmente Benčević y Antani y Stefanov[4, p. 16] proponen: en lenguajes orientados a objetos, "ellos [los objetos] son las estrellas del show, no las clases".
CONCEPTOS DE LA PROGRAMACIÓN ORIENTADA A OBJETOS
ENCAPSULACIÓN
Encapsular significa que en JS un objeto protege de accesos externos a las propiedades y valores dentro de bloques de código llamados funciones. Es decir, encapsular un objeto protege sus datos de cualquier otro evento que no esté contenido en él.
En los ejemplos siguientes la encapsulación ocurre cuando los datos de las variables se 'enganchan' a funciones que ejecutan acciones. Palabas clave como return y this
permiten acceder a los datos de las clases y leerlos.
- Función constructor
- Es la combinación de una función padre, con la clave
this
y con la clave de instanciaciónnew
. Lo que sucede es quethis
conecta los parámetros de la función para poder ser alimentadas por argumentos de cada instanciación. Esta instanciación será creada pornew
, generando así objetos-hijo que heredarán los propiedades de la función padre.
- clave
- Dentro de la función vincula los parámetros de esta hacia el contexto de las nuevas instancias. La clave
this
también prepara los parámetros de la función padre para conectarlas al nuevo objeto-hijo creado (o instanciado).
this
/* FUNCIÓN PADRE, CONSTRUCTOR NEW y CLAVE THIS */
function NuevoHumano(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
// 'new' genera objeto desde función constructora
var obj1 = new NuevoHumano("Dudu", 20);
var obj2 = new NuevoHumano("Sandra", 30);
// Objeto nuevo
console.log(obj1.nombre + " " + obj1.edad);
// Objeto nuevo
console.log(obj2.nombre + " " + obj2.edad);
// Dudu 20
// Sandra 30
Recordar: Es un estándart que todos los nombres identificadores de las clases inicien con mayúscula. También el que los nombres de las propiedades no tengan tipo de variable. Aunque esto último no podría ser leído en navegadores antiguos:
En Javascript
class nombrar{
// Propiedad SIN tipo variable
nombre = "Edu";
apellido = "Romero";
/* Cuando construyes, debes
agregar el nombre de la propiedad */
constructor(nombre, apellido){
this.nombre = nombre;
this.apellido = apellido;
}
}
// Hacer que NAZCA un nuevo objeto
const instanceA = new nombrar("Dudu", "Romero");
console.log(instanceA.nombre, instanceA.apellido);
// Dudu Romero
Abajo, un ejemplo con el operador class
del que detallaremos en otra sección. Este operador funciona como una función especial, esquema o plantilla para crear métodos (que operarán datos) y nuevas instancias derivadas de las propiedades de la clase-padre.
En resumen, en el ejemplo de abajo creamos una clase-padre que hará el papel de una plantilla. Esta plantilla servirá para crear objetos-hijos. Cuando 'nazcan' esos objetos mediante el operador new
ellos heredarán las propiedades y los métodos ya insertados
en la clase-padre cuando esta fue creada.
Finalmente, para usar los métodos de la clase-padre, invocaremos a la variable que aloja al objeto-hijo y accederemos (mediante el punto) al método que deseamos usar:
- Bucearemos en el concepto de
class{}
más adelante, en esta sección.
En Javascript
class Humanoide {
// Método
darleNombre(name){ this.name = name; }
// Método
obtenerNombre(){ return this.name; }
}
// Instancias
var human1= new Humanoide();
var human2= new Humanoide();
// Nuevos valores
human1.darleNombre("Dudu");
human2.darleNombre("Xavier");
// Invoca
console.log(human1.obtenerNombre() +
", "+ human2.obtenerNombre());
Abajo, otro ejemplo. Primero, creamos la clase-padre 'Circulo{}' (recordemos que toda clase inicia en mayúscula).
Dentro de esa clase está el constructor() que aloja un parámetro 'radio'. En este contexto de clase, ese parámetro toma el papel de una propiedad de la clase Circulo. Mediante 'this' apuntamos a la propiedad y asi asegurar que cada nuevo objeto-hijo la heredará.
Dentro de esa clase también están los métodos. Luego de nombrar al método como 'calculoArea()' pedimos mediante 'this' obtener nuevamente el valor de la propiedad para operarla.
Luego, creamos instancias (u objetos-hijo desde la clase padre) desde nuevas variables y alojamos el nuevos subobjetos mediante el operador'new' sin olvidarnos de apuntar al nombre de la clase-padre, y alimentando la propiedad con valor numérico.
Finalmente, hacemos las invocaciones de los nuevos subobjetos.
CUIDADO
El nombre de los métodos no debe ser el mismo que el de las propiedades. Es decir, en el siguiente ejemplo no debería haber un método de nombre radio() {...}
// Crea class
class Circulo {
// Genera propiedad de clase
/* El radio va del centro a cualquier punto del círculo */
constructor(radio) {
// apunta a propiedad
this.radio = radio;
}
// Área: superficie del círculo
// Método: PI * radio al cuadrado
calculoArea() {
return Math.PI * this.radio * this.radio;
}
// Perímetro: longitud de la línea circular
// Método: PI * radio * 2
calcularPerimetro() {
return 2 * Math.PI * this.radio;
}
}
// Instanciar Objetos.
var Circulo1 = new Circulo(5);
var Circulo2 = new Circulo(10);
// Invoca método calculoArea()
console.log("Circulo 1 area:", Circulo1.calculoArea());
console.log("Circulo 2 area:", Circulo2.calculoArea());
// Invoca método calcularPerimetro()
console.log("Circulo 1 perimeter:", Circulo1.calcularPerimetro());
console.log("Circulo 2 perimeter:", Circulo2.calcularPerimetro());
/*
Circulo 1 area: 78.53981633974483
Circulo 2 area: 314.1592653589793
Circulo 1 perimeter: 31.41592653589793
Circulo 2 perimeter: 62.83185307179586
*/
LA IMPORTANCIA DE LA FUNCIÓN CONSTRUCTOR() EN FUNCIONES CLASE
Recordar que cada propiedad alojada en constructor()
estará dispuesta a ser obtenida siempre que un nuevo objeto 'nazca' de la clase padre mediante el operador new
.
Así mismo, la función constructor() podría usar otro nombre. Pero eso obliga a dar un paso más: el de repetir la invocación y el añadido del argumento en el nuevo nombre dado a constructor(). Por lo que es mejor práctica mantener la convención y darle el nombre de constructor():
// Clase Padre
class SumaFriki {
// Le dimos otro nombre a constructor()
blablaConstructor(x){this.x = x;}
// Método
restaFriki(){ return 85 - this.x; }
// Nuevo método con operaciones
divisionFriki() { return 125 / this.x; }
}
// Instancia
var circle = new SumaFriki();
// PERO VOLVEMOS A INVOCARLO Y AGREGARLE EL PARÁMETRO (!!)
circle.blablaConstructor(5);
// Invoca
console.log(circle.restaFriki()); //-> 80
console.log(circle.divisionFriki()); //-> 25
Por lo tanto, es mejor que el sistema identifique por convención a constructor() como la única función agregadora de propiedades en instancias de objeto desde la clase padre.
Finalmente, una simple invocación a la clase se conoce como invocación explícita. Pero como no admite parámetros, tampoco admite la clave this. Por lo que si deseamos usar parámetros y argumentos necesitaremos al constructor().
// Clase
class Perrito {
// Método
ladrido(){console.log('Guau!')}
}
// Instancia
const firulais = new Perrito();
// Invoca explícito: instancia + método
firulais.ladrido();
//-> Guau!
// -- OTRO EJEMPLO --
class Person {
// Método
saludo(nombre){console.log(`Holi, ${nombre}!`)}
}
// Instancia
const nuevaPerson = new Person();
// Llamado explícito
nuevaPerson.saludo('Sandra');
//-> Holi, Sandra!
COMPOSICIÓN o 'Aggregation'
Significa que en lenguajes orientados a objetos es posible seccionar el código en parcelas más pequeñas. Esas parcelas pueden ser objetos contenidos en otros objetos. En el ejemplo de abajo, el objeto vehiculo
contiene como argumento un objeto recién instanciado: el objeto 'placa':
En Javascript
// Clase
class Maquina {
// constructor con parámetro
constructor(marca) {
/* Apunta a valor de parámetro
para luego alimentar a objeto hijo */
this.marca = marca;
}
}
// Nueva clase
class Auto {
// constructor con parámetro
constructor(placa) {
/* Apunta a valor de parámetro
para luego alimentar a objeto hijo */
this.placa = placa;
}
}
/* Crear nuevo contexto de instancia y
nuevo objeto con argumento 'GYE-343' */
const placa = new Maquina("GYE-343");
/* Crear nuevo contexto de instancia y
nuevo objeto con LA VARIABLE DE CONTEXTO DE INSTANCIA
CREADA ARRIBA */
/* El objeto 'vehiculo' contiene el objeto 'placa' */
const vehiculo = new Auto(placa);
console.log("La placa es " + vehiculo.placa.marca);
//-> La placa es GYE-343
Un ejemplo final de agregación. Pero antes, tomar en cuenta lo siguiente: en las clases Estudiante y Cursos existe una propiedad 'nombre'. Esta propiedad es única para cada clase aunque se muestre con el mismo nombre. Ya que cada clase representa un contexto diferente: el nombre del curso es un valor diferente al nombre del profesor.
En Javascript
// Clase
class Curso {
constructor(nombre, profe) {
// Propiedad 'nombre' única de la clase 'Curso'
this.nombre = nombre;
// Otra propiedad
this.profe = profe;
}
}
// Clase
class Estudiante {
// Propiedad 'nombre' única de la clase 'Estudiante'
constructor(nombre, grado) {
this.nombre = nombre;
// Otra propiedad
this.grado = grado;
}
}
// Variables de instanciación
// Variable math guarda instancia de Curso
const math = new Curso("Math", "Dudu");
// Variable ciencia guarda otra instancia de Curso
const ciencia = new Curso("Science", "Sandra");
// Variable curso guarda objetos math y ciencia
// Este es un dato arreglo
const curso = [math, ciencia];
// variable estudiante guarda instancia de Estudiante
const estudiante = new Estudiante("Juanito", curso);
console.log(math);
// Curso { nombre: 'Math', profe: 'Dudu' }
console.log(ciencia);
// Curso { nombre: 'Science', profe: 'Sandra' }
console.log(curso);
/* Muestra en forma de arreglo los dos
objetos con sus propiedades y valores */
/* [
Curso { nombre: 'Math', profe: 'Dudu' },
Curso { nombre: 'Science', profe: 'Sandra' }
] */
console.log(estudiante);
/* Estudiante {
nombre: 'Juanito',
grado: [
Curso { nombre: 'Math', profe: 'Dudu' },
Curso { nombre: 'Science', profe: 'Sandra' }
]
} */
HERENCIA
Para Antani y Stefanov[4] heredar clases significa "reusar código existente" en donde las propiedades de una clase madre serán heredadas hacia nuevos objetos instanciados.
En este contexto, los términos súper clase o clase padre refiere a lo mismo; es decir, a la gran clase desde la cual otras subclases heredarán sus propiedades y métodos.
En JS "los objetos se heredan entre objetos"[4], pero a su vez pueden actualizar los valores de propiedades según el contexto. Tres palabras claves son necesarias para generar herencia: extends
, super
y this
.
- CLAVE EXTENDS
- Usada dentro de la declaración de una subclase para crear una clase Hijo –o subclase– a partir de una clase Padre –o súper clase–.
- Una vez creada la subclase, extends también abrió una via libre para acceder –cuando sea necesario– a los nombres y valores de las propiedades y los métodos reunidos en la clase Padre. Es decir, extends activa la vía para la herencia.
- CLAVE SUPER
- Invoca y pide al constructor de la clase Padre el acceso total a las propiedades y métodos del Padre para luego usarlas en cada nueva instancia de clase.
- No se puede invocar propiedades antes de usar super. Es decir, primero se usa super(); y luego, se apunta a la propiedad deseada. Ejemplo: super(.., ..) this.propiedad = propiedad;
Las diferencias entre las claves extends
y super
versus la función constructor()
son:
constructor() | extends | super() |
---|---|---|
Método que da define las propiedades y valores iniciales desde la clase Padre para que, luego de ser creado un nuevo objeto, este herede esa estructura. | Crea una clase hijo a partir de la clase Padre. | Conecta con la función constructor de la clase Padre y lleva las propiedades y métodos al hijo. |
Entonces, el constructor()
define desde el Padre la futura estructura de propiedades y valores de un objeto a crear. Mientras que extends
crea clase hijo y super
conecta hijo con Padre. Por ejemplo:
En Javascript
// Clase
class Animal {
// Constructor con parámetros de propiedades
constructor(nombre, tipoDeAnimal) {
// Se apunta a propiedades
this.nombre = nombre;
this.tipoDeAnimal = tipoDeAnimal;
}
// Método de clase
ladridoSuave() {
// Muestra
console.log("guu guu!");
}
}
/* Ahora Se crea subClase a partir de SuperClase Animal */
class Perro extends Animal {
/* Tomará los valores de la propiedad 'nombre',
a la vez que crea otra propiedad de nombre 'raza'... */
constructor(nombre, raza) {
/* La clave super llama al constructor
de la clase Animal para que la nueva clase 'Perro'
obtenga las propiedades de 'nombre' y tipoDeAnimal' */
super(nombre, "Perro");
this.raza = raza;
}
// Método de subclase
ladridoFuerte() {
console.log("WOOF! WOOF!");
}
}
// Contexto de instancia con variable
/* Invoca la subClase Perro y alimenta
las propiedades de nombre y raza */
let Nicoletto = new Perro("Nico", "Runa");
// Se invoca variable de instancia y propiedad 'nombre'
console.log(Nicoletto.nombre);
// Nico
// Se invoca variable de instancia y propiedad 'tipoDeAnimal'
console.log(Nicoletto.tipoDeAnimal);
// Perro
// Se invoca variable de instancia y propiedad 'raza'
console.log(Nicoletto.raza);
// Runa
// Se invoca método de Clase
Nicoletto.ladridoSuave();
// guau!
// Se invoca método de subClase
Nicoletto.ladridoFuerte();
// WOOF! WOOF!
En otro ejemplo, la clase Vehiculo
con las propiedades 'nombreCarro' y 'modelo' tendrá en cada nuevo objeto esas mismas propiedades.
// Clase madre
class Vehiculo {
// Constructor con argumento de la variable
constructor(marca) {
/* Apuntamos a variable para que se
reinicialice con un argumento */
this.nombreCarro = marca;
}
presente() {
/* Muestra el valor apuntado al
contexto del constructor */
return '** ' + this.nombreCarro;
}
} // Cierra class Vehiculo
/* extends pide que Modelo sea
una clase hijo de Vehiculo */
class Modelo extends Vehiculo {
constructor(marca, mod) {
super(marca);
this.Modelo = mod;
}
show() {
return this.presente() +
' modelo ' + this.Modelo;
}
}
// NUEVOS OBJETOS
let carroComprado1 =
new Modelo("Jeep", "Compass");
let carroComprado2 =
new Modelo("Renault", "Silver");
console.log(carroComprado1.show());
console.log(carroComprado2.show());
//- > ** Jeep modelo Compass
//-> ** Renault modelo Silver
s
En un ejemplo final, note como la clave super()
es usada tanto para conectar con las propiedades de una clase padre como para conectar con los métodos también de la clase Padre y así, traerlos hacia la clase hijo. Este ejemplo fue tomado de Anurag Majumdar en Medium.
En Javascript
// Super clase o clase Padre
class Animal {
/* Función constructora con dos parámetros
en el papel de propiedades */
constructor(nombre, peso) {
// Se apuntan parámetros y se los inicializa
this.nombre = nombre;
this.peso = peso;
}
// Método de clase
come() {
// Apunta a propiedad de clase
return `${this.nombre} ...come carne.`;
}
// Método de clase
duerme() {
// Apunta a propiedad de clase
return `${this.nombre} ...duerme.`;
}
// Método de clase
despierta() {
// Apunta a propiedad de clase
return `${this.nombre} ...esta despierto.`;
}
}
/* Se crea una subClase o clase hijo desde superClase Animal */
class Gorila extends Animal {
// constructor() obtiene las propiedades de la clase Padre
constructor(nombre, peso) {
// Obtiene los valores de la clase Padre
super(nombre, peso);
}
// Método de subclase
trepa() {
// Se apunta a propiedad nombre
return `${this.nombre} ...sube árboles.`;
}
// Método de subclase
golpeaPecho() {
// Se apunta a propiedad nombre
return `${this.nombre} ...golpea pechito.`;
}
/* OJO aquí la CLAVE SUPER es usada como objeto.
Llama a los métodos de la clase Animal de
forma explícita */
// Método de subclase
seMuestraSano() {
// Se apunta a propiedad nombre
return `${super.come()}
${this.golpeaPecho()}`;
}
// Método de subclase
suRutinaDiaria() {
// Se apunta a propiedad nombre
return `${super.despierta()}
${this.golpeaPecho()}
${super.come()}
${super.duerme()}`;
}
}
// Método global -no está en ninguna clase-
function mostrar(mostrarElArgumentoQseAgregue) {
console.log(mostrarElArgumentoQseAgregue);
}
// Se crea VARIABLE de instancia de nuevo OBJETO
/* Se alimenta con las propiedades de la clase
Padre pero con nuevos valores */
const gorilla = new Gorila('George', '160Kg');
/* Se usa la función 'mostrar' para insertarle
la invocación del objeto + los métodos
de la subclase 'gorilla' */
mostrar("* " + gorilla.golpeaPecho() + "\n" );
// George ...golpea pechito.
mostrar( "* " + gorilla.duerme() + "\n" );
// George ...duerme.
mostrar( "* " + gorilla.seMuestraSano() + "\n" );
// George ...come carne. George ...golpea pechito.
mostrar( "* " + gorilla.suRutinaDiaria() + "\n" );
/*->
George ...esta despierto.
George ...golpea pechito.
George ...come carne.
George ...duerme.
*/
Herencia > Clave extends
La clave extends
señala que una clase deriva o es hija de una clase madre. En el ejemplo inferior, la clase 'Madre' tiene como hijo a la clase 'hijo'; y así mismo, 'hijo' nace de la clase 'Madre'.
Herencia > Clave super
La clave super
llama al constructor desde la clase madre y dispone que propiedades y métodos se hereden de inmediato.
Finalmente, en el código de abajo se muestran tres acciones: primero, establecer la clase 'Madre' con dos propiedads iniciales –edad y sexo–. Segundo, establecer que 'hijo' deriva de la clase 'Madre', con una nueva propiedad 'colorCabello'. Y tercero, crear una variable que almacene cada nueva instancia con las propiedades heredadas y con nuevos valores.
// Clase con propiedades asignadas
class Madre {
constructor(edad, sexo){
this.edad = edad;
this.sexo = sexo;
}
}
// Nace una clase hijo
class hijo extends Madre{
// Se heredan propiedades
constructor(edad, sexo, colorCabello)
{
super(edad, sexo);
// Se asigna propiedad
this.colorCabello = colorCabello;
}
}
/* Se crea variable que guarda
nueva instancia que hereda propiedades.
Pero aquí se agregan valores a esas propiedades */
let dudu = new hijo(35, "hombre", "negro");
console.table(dudu);
//-> Tiene 35 es hombre y cabello negro
/*
┌──────────────┬──────────┐
│ (index) │ Values │
├──────────────┼──────────┤
│ edad │ 35 │
│ sexo │ 'hombre' │
│ colorCabello │ 'negro' │
└──────────────┴──────────┘
*/
/* Nueva variable que guarda otra instancia */
let adriana = new hijo(38, "mujer", "rojo");
console.table(adriana);
/*
┌──────────────┬─────────┐
│ (index) │ Values │
├──────────────┼─────────┤
│ edad │ 38 │
│ sexo │ 'mujer' │
│ colorCabello │ 'rojo' │
└──────────────┴─────────┘
*/
POLIMORFISMO
Polimorfismo o 'muchas formas' se define como "llamar al mismo método o acción desde diferentes objetos"[4] en donde cada método tendrá un comportamiento diferente –o muchas formas de realizar la ejecución– desde cada objeto. Un ejemplo sencillo son los datos numéricos. Una vez operados estos datos cambian -restados, sumados, multiplicados, etc–.
En el siguiente ejemplo, una sola función –operar(x,y)
y operar(a,b)
– pero con diferentes parámetros ejecuta acciones diferentes según métodos de función:
// ABRE CLASE
class Padre {
// función operar() y dos argumentos
operar(x, y) {
console.log(x * y);
}
}
/* Activar herencia: Hijo hereda
propiedades de Padre */
class Hijo extends Padre {
// Otra ves la función operar()
operar(a, b) {
// super : heredar propiedades y métodos
super.operar(a, b);
console.log('Holi!')
}
}
var objeto1 = new Hijo();
var mostrar = objeto1.operar(2, 3);
//-> 6
//-> Holi!
Finalmente, un interesante artículo de Zell Liew acerca de los tipos de polimorfismo amplia ejemplos de ese concepto. Entre ellos:
- Múltitrabajo de los operadores: por ejemplo, el signo '+' sirve para sumar pero también para concatenar datos alfanuméricos. Es decir, su comportamiento cambia según el contexto a ser usado.
- Múltitrabajo en funciones: los datos de una misma función puedan dar diferentes resultados según nuevos datos insertados.
- Coerción de tipo: Un valor puede ser convertido en otro según condiciones. Por ejemplo, un valor booleano True a False.
- Múltitrabajo en variables: Tipos de variables pueden contener diferentes tipos de datos:
let dia="lunes", semana= 8;
- Polimorfismo paramétrico: Datos pueden contener datos, como en el caso de los objetos y arreglos.
- Polimorfismo de subtipo:Objetos padres derivan de ellos otros objetos hijos, también llamado herenciao subclase.
En cuanto a la herencia –derivar propiedades creadas desde una clase padre y dirigirlas cada nueva entidad– se debe usar la clave this
para apuntar a una propiedad ubicada en un contexto diferente del que se lo invoca:
/* El ejemplo base se tomó de
https://zellwk.com/blog/polymorphism-javascript/ */
// Clase padre
class Amiwis {
/* Se vincula un parámetro al que será
el creador de nuevos objetos */
constructor(nombre) {
/* Se le da a la variable 'nombre' un apuntador
mediante 'this' para invocarlo su valor
desde fuera de esta función */
this.nombre = nombre;
}
// Método o función
saludar() {
/* ATENCIÓN! Cuando invoquemos el valor del argumento,
debemos usar 'this' para apuntar al valor
del parámetro del constructor. */
console.log(`Qué tal, soy ${this.nombre}`);
}
}
/* ¡¡ALERTA!! No hay paréntesis () luego del nombre de la
clase */
class Compitas extends Amiwis {
saludar() {
/* ATENCIÓN! Cuando invoquemos el valor del argumento,
debemos usar 'this' para apuntar al valor
del parámetro del constructor. */
console.log(`Qué tal, soy ${this.nombre}.`);
}
}
class Panitas extends Amiwis {
saludar() {
/* ATENCIÓN! Cuando invoquemos el valor del argumento,
debemos usar 'this' para apuntar al valor
del parámetro del constructor. */
console.log(`Qué tal, soy ${this.nombre}.`);
}
}
// Usamos 'new' para crear objeto
const nombre1 = new Amiwis("Bruno");
const nombre2 = new Compitas("Rick");
const nombre3 = new Panitas("Dudu");
// Derivamos de método 'saludar'
nombre1.saludar();
nombre2.saludar();
nombre3.saludar();
/* ->
Qué tal, soy Bruno
Qué tal, soy Rick.
Qué tal, soy Dudu.
*/
OBJETOS (II)
Los siguientes son objetos globales –accesibles desde fuera de funciones– ya insertos en JS para operaciones matemáticas. Cada objeto agrupa invisibles propiedades que alteran datos numéricos. mdn web docs alerta que algunos navegadores pueden dar resultados ligeramente diferentes en los cálculos: "This means that different browsers can give a different result".
Métodos del objeto Math
El objeto Math() contiene métodos internos que automatizan matemática compleja.
Math.pow()
Una de esos métodos es Math.pow(), cuyo primer argumento es un número –la base–; y el segundo argumento, las veces que se multiplica por sí mismo –el exponente–.
var a = Math.pow(2, 4);
console.log(a);
// --> 16
var b = Math.pow(2, 8/4);
console.log(b);
// --> 4
var c = Math.pow(2, -8);
console.log(c);
// --> 0.00390625
// Pide multiplicar el número 3 en 0.33 veces
console.log(Math.pow(3,1/3));
// R. 1.44224957...
Math.round()
Devuelve la conversión de un número decimal a un número entero, según lo siguientes criterios:
- Si el decimal es mayor o igual a 0.5 --> la conversión resultará en el entero inmediato mayor: uno.
- Si el decimal es menor a 0.5 --> la conversión será al entero inmediato menor: cero.
/* Decimal MENOR a 0.5... */
console.log(Math.round(0.4));
// --> 0
console.log(Math.round(-0.2));
// --> -0
/* Decimal MAYOR o IGUAL a 0.5... */
console.log(Math.round(0.6));
// --> 1
console.log(Math.round(3.1000));
// --> 3
console.log(Math.round(3.6000));
// --> 4
Operar desde una variable puede ser útil:
var calc = 55.332/11.4344;
console.log(calc);
console.log( Math.round(calc) );
// 4.8 –-decimal mayor a 0,5 --> redondea a 5
Math.ceil()
Devuelve la conversión decimal desde 0.1 a 0.9 hacia el entero inmediato más alto. A diferencia de Math.round(), que redondea hacia el entero más alto desde 0.5 a 0.9; Math.ceil() redondeará desde un decimal más bajo (0.1).
// Decimal
var num = 0.75;
// Redondea a entero inmediato
console.log(Math.ceil(num))
// --> 1
// Decimal
var num = 35.1;
// Redondea a entero inmediato
console.log(Math.ceil(num))
// --> 36
// Decimal
var num = 2.00000001;
// Redondea a entero inmediato
console.log(Math.ceil(num))
// --> 3
/* Incrementa el entero al mínimo decimal */
var num = -2.00000001;
// Redondea a entero inmediato
console.log(Math.ceil(num))
// --> -2
/* Negativos en cero redondean
desde el mínimo decimal a 0 cero */
console.log(Math.ceil(-0.10));
// --> 0
Math.floor()
Redondea al entero inferior o igual del número dado más bajo aunque sea un decimal de 0.9
console.log(Math.floor(2.9999) );
// R. 2
console.log(Math.floor(10.9) );
// R. 10
console.log(Math.floor(0.9) );
// R. 0
Math.abs()
Devuelve el valor absoluto (sin signo)
console.log( Math.abs(-0.6));
// --> 0.6
console.log( Math.abs(-1.999999));
// --> 1.999999
Math.min() y Math.max()
Devuelve el entero o el decimal más grande -Math.max()- o el más bajo -Math.min()-. Permite números strings.
// Devuelve el número mayor
console.log(Math.max(9.6, "10",4));
/// R. 10
/* Devuelve '10' aún siendo string.
JS convierte internamente los strings
literales en numéricos. */
// Devuelve el entero o decimal mayor
console.log(Math.max(0.6, "0.1",0.009999));
// R. 0.6
// Devuelve un entero o decimal más bajo
console.log(Math.min(1, 0.5, 2));
// R. 0.5
Math.sqrt()
Devuelve raíz cuadrada del argumento numérico dado.
console.log(Math.sqrt(-0)); // -0
console.log(Math.sqrt(2)); // 1.414213562373095
console.log(Math.sqrt(9)); // 3
console.log( Math.sqrt(2*3/100) );
// 0.2449489742783178
Math.cbrt()
Da la raíz cúbica de argumento numérico.
// Raíz cúbica de (argumento)
// ¿Qué número, multiplicado
// tres veces, da (el argumento)?
console.log(Math.cbrt(8));
// --> 2
Math.hypot()
Retorna la raíz cuadrada de la suma de dos valores –los dos lados de un triángulo rectángulo–, por ejemplo.
var ladoA = 3, ladoB = 6;
console.log(Math.hypot(ladoA,ladoB));
// 6.708203932499369
/* Para calcular la distancia entre dos dimensiones */
console.log(Math.hypot(3, 4, 3));
// 5.830951894845301
/* Para calcular la distancia entre dos puntos cartesianos */
// A=(x,y) B=(x,y)
console.log(Math.hypot( (1,2), (4,6) ) );
// 6.324555320336759
Math.trunc()
Expresa un entero decimal quitando el decimal.
console.log(Math.trunc(3.2) );
// 3
console.log(Math.trunc(-2.444) );
// 2
/* Se mantiene negativo.
Si dese desea quitar el negativo,
entonces va Math.abs() */
var num = 3.4*3.98/9.908;
console.log(Math.trunc(num));
// 1
// Convertir a entero quitando el decimal
console.log(Math.trunc(-0.1122) );
// 0
Math.random()
Devuelve un número aleatorio decimal, entre 0 y 1.
Un ejercicio clásico es mostrar un número aleatorio según límites dados por un rango:
var aleat = Math.random();
// 0.283... o cualquiera
// Rango: Dame números desde 0 hasta el 100
var centenar = 100;
// Opción: Rango hasta el 1000
var miles = 1000;
// Multiplica el resultado random por el límite
var opera = centenar * aleat;
// Resultado con decimales.
// Necesitamos redondear
console.log((opera));
// --> 34.85451320941382
// NÚMERO FINAL
// Redondea el resultado al entero más bajo
console.log( Math.floor(opera) );
Una estructura más simplificada para el mismo resultado sería el siguiente –donde el número sería el rango límite–.
/* Desde dentro hacia afuera,
Math.random() da un número decimal entre 0 y 1.
Luego, multiplica ese número por 10.
Como sigue siendo decimal, Math.floor()
lo convierte al entero inferior inmediado. */
console.log(Math.floor( Math.random() * 10) );
/* En cada actualización,
se mostrará un nuevo valor */
/* 5...2...0....1 (...) */
TRABAJAR CON ARGUMENTOS Y PARÁMETROS
Para la Guía de Visual Basic de Microsoft existen diferencias entre parámetros y argumentos.
- Parámetros: es el identificador –nombre– de un valor, que a su vez es contenido en sí mismo como una variable que se define en un contexto, como una función. Este parámetro podrá recibir un valor llamado argumento.
- Argumento: es un valor/objeto –numérico, alfanumérico, literal– ingresado en el parámetro. Ese valor será llamando cuando el contexto –una función, por ejemplo- sea invocada. Los argumentos estan rodeados por paréntesis ().
Para la PC Magazine enciclopedia "los argumentos son valores independientes" que proveen de más datos a un contexto, como una función. Finalmente, es el usuario final quién decidirá qué valor podría ese argumento. La expresión 'pasar un argumento' significa que se ha alimentado con un valor a un parámetro.
¿Qué puede ser un argumento?
- Un valor numérico, un string, booleano.
- Una función
- Una palabra clave, como
this
- Un método
En otras palabras, un parámetro es el identificador –o nombre– de una variable; y un argumento es el valor enviado al parámetro.
Similar a una bodega con dos grandes puertas que permiten ingresar objetos, las puertas de ingreso son los parámetros –con un letrero identificatorio que son los nombres de cada puerta–; mientras que los argumentos son los objetos ingresados externamente.
// Se crea la función
function proceso(parametro1, parametro2) {
// Se pide devolver -cuando se requiera- los parámetros
return "Entraron " + parametro1 + " y " + parametro2;
}
// Se alimenta DE ARGUMENTOS los parámetros
console.log(proceso("objeto1", "objeto2"));
// Entraron objeto1 y objeto2
// * * * * * * * *
// * * * * * * * *
// * * * * * * * *
// Se crea la función
function bodega(puerta1, puerta2) {
// Se pide devolver -cuando se requiera- los parámetros
return "Entraron " + puerta1 + " y " +puerta2;
}
// Se alimenta DE ARGUMENTOS los parámetros
console.log(bodega("silla", "escritorio"));
// Entraron silla y escritorio
FUNCIONES PREDEFINIDAS
Ya planteamos que las funciones son territorios delimitados cuyo papel es trabajar con datos desde un solo bloque de código; y nos permiten –mediante parámetros– insertar nueva información –en forma de argumentos–.
// parámetro 'lugar'
function turismo(lugar){
// Condicional + ubicación dada
if (lugar == "cerro Santa Ana"){
console.log("El " + lugar +
" se ubica en el centro de Guayaquil");
} else if (lugar == "Malecón 2000"){
console.log("El " + lugar +
" se ubica frente al río Guayas");
} else {
console.log(lugar + " No está en Guayaquil");
}}
// INVOCAR FUNCIÓN
// Argumento 'Malecón 2000'
turismo("Malecón 2000");
/* --> El cerro Santa Ana
se ubica en el centro de Guayaquil */
Como veremos, otras funciones ya predefinidas nos serán muy útiles.
parseInt()
Solo si un dato numérico esta ubicado antes que uno alfanumérico, entonces parseInt() devuelve solo el dato numérico entero. Si parseInt() no puede convertir los primeros números a enteros, retorna la propiedad NaN. Además de eso, parseInt() convierte un valor radix. Este determina el sistema numérico. Por ahora, solo mantendremos la conversión de alfanumérico a dato tipo número.
// parseInt()
var ingreso = '2023Guayaquil';
// Solo muestra el primer conjunto de números
console.log(parseInt(ingreso));
// --> 2023
// ...y sigue siendo string
console.log(typeof ingreso);
//-> string
// No puede convertir si el entero está al final
console.log(parseInt("Edu2023"));
// NaN
/* Devuelve NaN si el dato alfanum. está al final.
El sistema lo detecta una inválida
representación de un número */
var ingreso = 'Guayaquil 2023';
console.log(parseInt(ingreso));
// NaN
/* Devuelve solo el entero */
var ingreso = '20.98453 Holi';
console.log(parseInt(ingreso));
// 20
parseFloat()
Busca el primer conjunto de decimales y los muestra, obviando otros caracteres que no sean números.
// parseFloat()
var ingreso = '0.1416...es PI';
console.log(parseFloat(ingreso));
// 0.1416
// --> Sigue siendo string
console.log(typeof ingreso);
// string
// También convierte exponentes
console.log(parseFloat('123e-2'));
// --> 1.23
// El entero lo muestra igual
console.log(parseFloat('23234'));
// 23234
// No convierte
console.log(parseFloat("a1234"));
// --> NaN
isNaN()
Not a Number. Muestra true cuando su argumento es un dato no numérico. Muestra false cuando su argumento sí es un dato numérico. Es decir, si NaN() contiene un dato numérico, negarlo mostrará false.
console.log(NaN(10)); | console.log(NaN("holi")); |
---|---|
Afirma que el número 10 No es un Número. La expresión da false. | Afirma que string "holi" no es un Número. La expresión da true. |
// El dato NO es número
console.log(isNaN("abc"));
// --> true
// lo indefinible no es número
console.log(isNaN(undefined));
// --> true
// Lo vacío es no es un no número
console.log(isNaN(null));
// --> false
// Negar que 200 es un número es erróneo.
console.log(isNaN(200));
// --> false
// Negar la negación da una afirmación:
// 200 sí es un número
console.log(!isNaN(200));
// --> true
eval()
Cuidado. Esta función es un hacha afilada. Permite insertar líneas de texto como si fueran códigos ejecutables. Bien usada es aplicada en pruebas. Pero en manos equivocadas, es un peligro. SE RECOMIENDA EVITAR SU USO. Para más referencias, revisar aquí y aquí. Finalmente, en el artículo Eval is evil - Why we should not use eval in JavaScript, Amit Khonde detalla los problemas de desempeño y de seguridad que tiene esa función.
/* Este podría ser un código malicioso */
eval(' var ojito = "Cuidado"; ');
console.log(ojito);
// --> Cuidado
// También este
let expression = "2 + 2";
let result = eval(expression);
console.log(result);
// 4
Un ejemplo válido de uso con eval() y plantillas literales sería el siguiente:
En Javascript
/* OJO: los {{ }} no deben mostrarse separados */
/* La variable en {{ }} debe mostrarse centrada {{ variable }} */
/* Las comillas de "{{ }}" no deben separarse */
/* Usar comillas simples dentro de console... */
/* La expresión de console.log() no se puede cortar. */
let linea = "console.log('Es' + ' ' + ({{ expression }}) + '.')";
let expression = "2 + 2";
let code = linea.replace("{{ expression }}", expression);
eval(code);
// La respuesta es 4.
Alternativas para eval() son el método JSON.parse() o plantillas literales.
Subir al inicio Avanzar