Propiedades y métodos 2

Continuando con el estudio de las propiedades y métodos, en este capítulo se estudian las formas en que se pueden crear y añadir métodos (estáticos y dinámicos) a las clases (objetos constructores) y a los objetos literales.

Funciones

Como ya se dijo, los métodos son el equivalente a las funciones de la programación estructurada y en realidad son funciones, sólo que están asociadas a una clase u objeto.

En consecuencia, para crear métodos es necesario saber crear funciones. Por esa razón, en esta sección se hace un repaso de las diferentes formas en que pueden ser creadas.

En JavaScript las funciones pueden ser creadas en al menos 3 formas: funciones estándar, funciones anónimas y funciones flecha. Además, las funciones anónimas y flecha, pueden ser inmediatamente invocadas (funciones IIFE).

Las funciones estándar tienen esencialmente la misma estructura que las funciones en C/C++, excepto que, en lugar del tipo de dato devuelto, se escribe la palabra reservada function:

function nombre_de_la_función(lista_de_parámetros) { Instrucciones JavaScript ... return valor/expresión; }

El nombre_de_la función, al ser un identificador, sigue las mismas reglas que se aplican a los nombres de las variables.

Las instrucciones JavaScript son instrucciones válidas del lenguaje, escritas una a continuación de otra y separadas con puntos y comas (;). Una función puede tener entre 1 y cientos de instrucciones.

La instrucción de retorno: return, termina la ejecución de la función y devuelve el valor o el resultado de la expresión. Esta instrucción es opcional y puede estar ubicada en cualquier parte de la función, pero se recomienda que sea la última instrucción o parte de la última instrucción. Si en la función no se escribe la instrucción return, la función termina cuando se ejecuta su última instrucción y no devuelve ningún valor (el resultado es indefinido: undefined).

La lista_de_parámetros son los nombres de las variables (separadas con comas) en las que la función recibe los datos que necesita. Al igual que en C/C++, los parámetros, así como las variables declaradas dentro de la función, son locales y temporales, es decir que sólo existen dentro de la función y dejan de existir (son destruidas) cuando la función termina.

Por ejemplo, la siguiente instrucción crea la función estándar "sum2", que recibe dos valores en las variables "x" y "y" y devuelve su suma:

function sum2(x, y) {
return x+y;
}

Una vez creada, la función puede ser llamada (empleada) simplemente escribiendo su nombre (sum2) y entre paréntesis, los valores a sumar. Por ejemplo, en las siguientes instrucciones, se emplea la función sum2, para calcular la suma de 7 y 12, 5.76 y 9.32 y 54.3123 y 89.2443:

sum2(7, 12)
sum2(5.76, 9.32)
sum2(54.3123, 89.2443)

Las funciones también pueden ser creadas como funciones anónimas:

var|const|let nombre_de_la_función = function(lista_de_parámetros) { Instrucciones JavaScript ... return valor/expresión; };

Donde nombre_de_la_función es la variable en la cual se guarda la función. Este tipo de funciones se denominan anónimas porque la función en sí no tiene nombre, por eso es necesario guardarla en una variable. Como de costumbre, se emplea "var" cuando se declaran variables en la calculadora (para poder hacer correcciones en caso de error) y "const" (más frecuentemente que "let") cuando se crea al interior de un módulo, librería o función.

El cuerpo de la función es el mismo que el de una función estándar, por lo que sólo cambia la forma de declararla, así la función suma2, en forma de una función anónima, es:

var sum2 = function(x, y) {
return x+y;
}

Y se llama y emplea igual que una función estándar, así para sumar 10 y 15, se escribe:

sum2(10, 15)

Las funciones pueden ser creadas también como funciones flecha, de acuerdo a la siguiente sintaxis:

var nombre_de_la_función = (lista_de_parámetros) => { Instrucciones JavaScript ... return valor/expresión; };

Que es una forma parecida a las funciones anónimas, excepto que no se emplea la palabra function y después de los parámetros se escribe una flecha: =>. Esta es la forma más práctica cuando la función consta de una sola expresión, pues en esos casos no se requieren ni las llaves, ni la instrucción de retorno (return). Así, la función "sum2", con una función flecha se crea con la siguiente instrucción:

var sum2 = (x, y) => x+y;

Y, por supuesto, se emplea igual que los otros tipos de funciones. Por ejemplo, para calcular la suma de 12 y 23, se escribe:

sum2(12, 23)

Pero además, si la función recibe un sólo parámetro, no es necesario encerrar ese parámetro entre paréntesis. Por ejemplo, para programar una función, que devuelva el resultado de la siguiente expresión:

\[ \sqrt[3] {{y+ 4! + y^{3.2}} \over {\text{e}^y + \cos(y)} } \]

Se escribe:

var fy = y => Math.cbrt((y+Math.factorial(4)+y**3.2)/(Math.exp(y)+Math.cos(y)));

Que es empleada (es llamada) como de costumbre, es decir escribiendo el nombre de la función ("fy") y el valor requerido ("y") entre paréntesis, así el resultado de la expresión, para y=3.1, se calcula con:

fy(3.1)

Cuando es necesario crear una función, pero sólo es empleada una vez (algo que no es muy raro en programación) no es necesario guardarla, sino que puede ser creada y empleada (llamada) inmediatamente. Estas funciones se conocen como IIFE: Immediately Invoked Function Expresiones (Expresiones Función Invocadas Inmediatamente: funciones ifi).

Las IIFE pueden ser creadas tanto con las funciones anónimas como con las funciones flecha de acuerdo al siguiente formato:

(function(lista_de_parámetros) { // instrucciones JavaScript return valor/expresión; })(valores_con_los_que_se_llama_a_la_función); ((lista_de_parámetros) => { // instrucciones JavaScript return valor/expresión; })(valores_con_los_que_se_llama_a_la_función);

Por ejemplo, si se quiere calcular la suma de los cubos de 1, 5 y 9, se puede escribir la siguiente IIFE:

(function(x, y, z) {
return x**3+y**3+z**3;
})(1, 5, 9)

O también con:

((x, y, z) => x**3+y**3+z**3)(1, 5, 9)

La función se encierra entre paréntesis, ya sea solo la función, sin los datos (de acuerdo al formato mostrado para la primera forma) o la función y los datos (de acuerdo al formato mostrado para la segunda forma) para que JavaScript trate a la función como una expresión y la ejecute inmediatamente.

Cuando la expresión o problema a resolver requiere más de una instrucción, que es el caso más frecuente, las instrucciones deben separarse con puntos y comas y si se requieren variables adicionales, deben ser declaradas con const (si no son modificadas) o con let (si son modificadas, es decir, si su valor cambia dentro de la función). Por ejemplo para calcular el área de un triángulo, de lados "a", "b" y "c", con la siguiente expresión:

\[ \begin{aligned} \text{Area} &= \sqrt{s(s-a)(s-b)(s-c)}\\[0.3cm] donde : &\phantom{=}\\ s &= \dfrac{a+b+c}{2} \end{aligned} \]

Se debe calcular (y guardar) primero el valor de "s", luego con ese valor se calcula el área del triángulo, es decir:

function areaTriangulo(a, b, c) {
const s = (a+b+c)/2;
return Math.sqrt(s*(s-a)*(s-b)*(s-c));
}

Como se puede ver la variable local s ha sido declarada como constante, que es lo correcto en este caso, porque su valor, una vez calculado, no cambia.

Como de costumbre, para llamar a la función se escribe su nombre y entre paréntesis los valores separados con comas, como ocurre en las siguientes instrucciones:

areaTriangulo(10, 15, 12)
areaTriangulo(17, 14, 19)
areaTriangulo(20, 30, 40)

Parámetros

Los parámetros empleados hasta el momento son parámetros por valor y funcionan igual que en C/C++. Como su nombre sugiere, estos parámetros sólo guardan el valor que se les manda, sin importar que dicho valor sea literal, esté guardado en una variable o sea el resultado de una función o expresión.

Por ejemplo, si se llama a la función sum2, guardando previamente los valores a sumar en las variables "a" y "b":

var a = 7, b = 8;
sum2(a, b)

La función sum2, no guarda las variables "a" y "b" en las variables "x" y "y", sino únicamente sus valores (los números 7 y 8).

Las variables originales permanecen inalteradas porque, como se dijo, la función no recibe las variables, sino únicamente sus valores. Como se puede comprobar en la siguiente función, que recibe dos números en los parámetros "c" y "d", estos parámetros son modificados dentro de la función, pero, como se puede ver, las variables externas permanecen inalteradas.

var c = 5, d = 7;
print(c, d);
function fun1(c, d) {
c = c*3;
d = d*4;
return c+d;
}
print(fun1(c, d));
print(c, d)

Al igual que en C/C++, los parámetros pueden ser también parámetros por defecto, es decir que, en caso de no recibir ningún valor, toman el valor por defecto.

Por ejemplo, en la siguiente función, que calcula la suma de hasta 3 números, los parámetros tienen valores por defecto iguales a cero:

var suma = (a=0, b=0, c=0) => a+b+c;

Si no se manda uno o más de los 3 valores, los parámetros que no reciben ningún valor toman el valor por defecto: 0. Ese es un valor conveniente en este caso, porque al ser cero el valor neutro en la suma, no interfiere en el resultado (0 más cualquier otro valor, devuelve el mismo valor), de esa manera la función devuelve el resultado correcto ya sea que se le mande uno, dos o los tres valores.

Así, a continuación, en la primera llamada que se hace a la función (con 3 valores) "a" es igual a 1, "b" es igual a 2 y "c" es igual a 3; en la segunda llamada (con 2 valores) "a" es igual a 1, "b" igual a 2 y "c" es igual a 0 (el valor por defecto); en la tercera llamada (con un valor) "a" es igual a 1, mientras que "b" y "c" son iguales a 0 (el valor por defecto) y en la última llamada (sin valores) "a", "b" y "c" son iguales a 0 (el por defecto):

suma(1, 2, 3)
suma(1, 2)
suma(1)
suma()

Como se puede ver, en todos los casos la función devuelve el resultado correcto y cuando no se manda ningún valor, devuelve 0.

A diferencia de C/C++, en JavaScript los parámetros por defecto pueden estar en cualquier posición (no siempre al final). Para que la función emplee un valor por defecto, en uno de los parámetros que no están al final, se debe mandar el valor undefined en esa posición. Por ejemplo, para que suma emplee el valor por defecto para el segundo parámetro, se manda el valor undefined en la segunda posición:

suma(12, undefined, 45)

En este caso "a" toma el valor 12, "b" toma el valor 0 (el valor por defecto) y "c" toma el valor 45.

Aunque estrictamente hablando, las funciones en JavaScript (al igual que en C/C++) no admiten parámetros nombrados, pueden ser emulados recurriendo a los objetos literales, de acuerdo a la siguiente sintaxis:

({par1[=val1], par2[=val2], ..., parn[=valn]}={})

Donde, par1, par2, etc., son los nombres de los parámetros. Como se puede ver, los parámetros nombrados también pueden tener valores iniciales por defecto. Los parámetros nombrados se caracterizan porque, como su nombre sugiere, los datos deben ser mandados incluyendo el nombre del parámetro y porque esos nombres pueden ser mandados en cualquier orden (a diferencia de los parámetros por valor y por defecto, que deben ser mandados en el orden en que fueron declarados).

El símbolo de igualdad y las llaves (={}), son necesarios únicamente para los casos en los que se llama a la función sin mandar ningún valor. Si, al crear una función, se sabe con certeza que siempre será llamada con algún valor, entonces puede ser programada sin escribir el símbolo de igualdad y las llaves.

Los parámetros nombrados son particularmente útiles cuando la función consta de 3 o más parámetros, porque, en esos casos, es más fácil recordar el nombre de los parámetros que la posición en la que fueron declarados.

Por ejemplo, para calcular con la siguiente ecuación, la distancia (d) que alcanza un proyectil disparado con una velocidad inicial \(v_0\) (en m/s) y con un ángulo de disparo θ (en grados), siendo la aceleración de la gravedad g:

\[ d = \dfrac{v^2_0 \sin(2\theta)}{2g} \]

Se pueden emplear parámetros nombrados. En este caso, es conveniente declarar como parámetro nombrado, con un valor por defecto, la aceleración de la gravedad (g): 9.80665 (m/s2), porque ese valor, que corresponde a la gravedad estándar en la tierra, generalmente no cambia, por lo que rara vez se manda un valor diferente para el mismo:

var alcance = function({v0, O, g=9.80665}={}) {
// Se convierte el ángulo a radianes
O = O*Math.PI/180;
//Se calcula y devuelve la distancia
return v0**2*Math.sin(2*O)/(2*g);
};

Al igual que en C/C++, el texto escrito después de los dos quebrados (//) son comentarios. Como se sabe, los comentarios son ignorados por el lenguaje. Se emplean para documentar el código (hacer más entendible el código). Es aconsejable acostumbrarse a escribir comentarios, porque así es mucho más fácil recordar el razonamiento seguido al elaborar el programa.

Llamando a la función, para calcular la distancia que alcanza un proyectil disparado con una velocidad inicial de 150 m/s y un ángulo de 30 grados, se obtiene:

alcance({O:30, v0:150})

Por lo tanto, el proyectil alcanza una distancia de 993.5 m.

El programa también puede ser elaborado, empleando la función toRad, para convertir el ángulo a radianes, en lugar de hacer la conversión explícita:

var alcance_ = function({v0, O, g=9.80665}={}) {
// Se convierte el ángulo a radianes
O = Math.toRad(O);
//Se calcula y devuelve la distancia
return v0**2*Math.sin(2*O)/(2*g);
};

Qué, por supuesto, devuelve el mismo resultado:

alcance_({O:30, v0:150})

En este ejemplo se han mandado los datos a la inversa de como fueron declarados (en desorden) algo que es posible cuando se trabaja con parámetros nombrados.

Para obtener un resultado más preciso, se puede mandar también el valor de la aceleración local de la gravedad g=9.77721 m/s2:

alcance({g:9.77721, O:30, v0:150})

Que es casi 3 metros mayor al resultado anterior (note que, en este caso, los datos han sido mandados igualmente en desorden).

Propiedades y métodos estáticos

Una vez que se sabe como declarar variables y crear funciones, se pueden añadir propiedades y métodos a las clases y objetos literales.

Inicialmente se añadirán propiedades y métodos a las clases y objetos predefinidos, posteriormente serán añadidos a objetos y clases propias. El añadir propiedades y métodos a un objeto predefinido no es una buena práctica, porque se modifica un objeto creado por otro autor (en este caso ECMAScript) y los usuarios de este objeto, esperan que tenga únicamente las propiedades y métodos definidos por el autor. El incluir propiedades y métodos no definidos por el autor, puede dar lugar a errores difíciles de ubicar en el programa. En esta sección, se añaden propiedades y métodos a objetos predefinidos, únicamente porque aún no se cuenta con objetos propios.

Como ya se dijo, las propiedades y métodos estáticos son simplemente variables y funciones normales, sólo que están asociadas a un objeto (literal o constructor).

Para añadir una propiedad a una clase, se emplea la notación de punto, de acuerdo a la siguiente sintaxis:

Nombre_de_la_clase.nombre_de_la_propiedad = valor;

Así, en la siguiente instrucción, se añade la propiedad "miPropiedad" al objeto Math:

Math.miPropiedad = "Propiedad estática";

La propiedad se recupera y utiliza con la notación de punto:

Math.miPropiedad

Pudiendo, igualmente, ser modificada:

Math.miPropiedad = "Propiedad modificada";
Math.miPropiedad

En consecuencia, las propiedades estáticas se crean y emplean de forma similar a las variables estándar, excepto que están precedidas por el nombre del objeto y no requieren de las instrucciones var, let o const.

Los métodos estáticos se crean, igualmente, empleando la notación de punto y son funciones normales. La función puede ser creada en cualquiera de las 3 formas estudiadas previamente.

Por ejemplo, en la siguiente instrucción, se añade al objeto Math el método rquinta, que calcula la raíz quinta de un número, empleando una función flecha:

Math.rquinta = x => x**(1/5);

Los métodos añadidos se emplea igual que cualquiera de los método del objeto Math. Así, para calcular la raíz quinta de 23.67, se escribe:

Math.rquinta(23.67)

Igualmente se puede emplear una función anónima, así, en la siguiente instrucción se añade el método log7 (que calcula el logaritmo en base 7) al objeto Math. Luego, el método es empleado para calcular el logaritmo en base 7 del número 12:

Math.log7 = function(x) {
return Math.log(x)/Math.log(7);
};
Math.log7(12)

También se puede emplear una función estándar. En ese caso primero se crea la función y luego se la asigna a la clase.

Por ejemplo, en las siguientes instrucciones se añade al objeto Math el método areaCirculo (que calcula el área de un círculo: π×r2). Luego se calcula el área de un círculo de 5 unidades de radio:

function areaCirculo(r) {
return Math.PI*r**2;
};
Math.areaCirculo = areaCirculo;
Math.areaCirculo(5)

Propiedades y métodos dinámicos

Las propiedades y métodos dinámicos, se añaden a la propiedad prototype de la clase, sin embargo, el añadir propiedades y métodos dinámicos a un objeto predefinido (a una clase predefinida), es una práctica menos aconsejable aún que la de añadir objetos y métodos estáticos, porque los métodos dinámicos afectan a todos los objetos creados a partir de la clase. En este capítulo (al igual que en la sección anterior) son añadidos sólo porque, por el momento, no se han creado aún clases propias (objetos constructores propios).

Aunque es posible, en la práctica no se añaden propiedades dinámicas a una clase, porque, cuando se crean propiedades dinámicas, todos los objetos de esa clase tienen acceso a esas propiedades y normalmente, no tiene sentido que todos los objetos tengan acceso y puedan modificar las mismas propiedades.

Por ejemplo la siguiente instrucción añade la propiedad dinámica miPropiedad a la clase Number:

Number.prototype.miPropiedad = 23;

Ahora, si se crean tres variables numéricas y se accede desde ellas a "miPropiedad", se obtiene, en todos los casos, el mismo valor:

var x = 12.34, y = 151, z = 4.3e6;
x.miPropiedad
y.miPropiedad
z.miPropiedad

En este ejemplo ha sido posible acceder a la propiedad "miPropiedad" desde variables numéricas (desde números), porque (como se dijo) JavaScript convierte automáticamente los valores primitivos (como los números y las cadenas) en objetos.

De hecho, se puede acceder a las propiedades y métodos de la clase Number, directamente desde cualquier número literal (sin necesidad de crear una variable):

(832.432).miPropiedad
(482).miPropiedad
(42e6).miPropiedad

Sin embargo, como se puede ver en estos ejemplos, no tiene sentido que todos los objetos tengan acceso a un mismo valor, el cual, además, puede ser modificado por cualquiera de los objetos.

Formalmente, aunque casi nunca se emplea con números y otros valores primitivos, los objetos (las instancias de una clase) se crean con el comando new. Por ejemplo, en las siguientes instrucciones se crean las variables "n1" y "n2" con los números 62 y 89.32, luego se suman y se accede a la propiedad "miPropiedad" (la que fue añadida en los ejemplos anteriores):

var n1 = new Number(62);
var n2 = new Number(89.32);
n1+n2
n1.miPropiedad

Es importante recordar que la propiedad prototype sólo existe en las clases (en los objetos constructores), no en los objetos literales (como es el caso del objeto Math) ni en las instancias de una clase.

Al contrario de lo que ocurre con las propiedades, los métodos son añadidos, casi siempre, de forma dinámica. Es así, porque no tiene sentido que todos los objetos tenga una copia del mismo método, pues las copias sólo consumirían memoria y recursos adicionales, inútilmente.

Todos los objetos creados a partir de una clase (las instancias) pueden llamar a los métodos dinámicos de la clase, a su vez, los método dinámicos tiene acceso a las propiedades (al estado) del objeto desde el cual son llamados.

Por ejemplo, en la siguiente instrucción, se añade a la clase Number el método dinámico volEsfera (que calcula el volumen de una esfera: 4/3*π*r3) :

Number.prototype.volEsfera = function() {
return 4/3*Math.PI*this**3;
};

Donde la variable this, es el nombre que le asigna JavaScript (automáticamente) al objeto desde el cual se llama al método.

Cuando JavaScript requiere el valor de un objeto, lo obtiene llamando al método valueOf. En el caso de los objetos de tipo Number, valueOf, devuelve el valor primitivo del número (el número propiamente).

Así, cuando JavaScript encuentra this en una expresión matemática (como en el ejemplo) recupera su valor primitivo llamando al método valueOf() y emplea ese valor en las operaciones matemáticas.

Para calcular con el método elaborado el volumen de una esfera de 6.72 metros de radio y el de otra de 5 metros de radio, se llama al método desde esos números:

(6.72).volEsfera()
(5).volEsfera()

No obstante, se debe recordar que this es el objeto desde el cual se llama al método, no su valor primitivo (no el número propiamente). En consecuencia this no siempre se comporta como el valor primitivo. Por ejemplo, si se emplea this, con los métodos estáticos isInteger e isFinite, devuelve falso, aún cuando se llama al método desde un número entero, como se puede comprobar modificando el método de la siguiente forma:

Number.prototype.volEsfera_ = function() {
print(Number.isInteger(this));
print(Number.isFinite(this));
return 4/3*Math.PI*this**3;
};

Y llamando al método desde un número entero, se obtiene:

(12).volEsfera_()

Que debería devolver true y true (no false y false), porque se ha llamado al método desde un número entero. Sucede así, porque, como se dijo, en isInteger e isFinite, this es tratado como un objeto, no como un número.

Para que un número sea tratado como tal dentro del método, no como un objeto, se debe recuperar su valor primitivo con el método valueOf, como se muestra en el siguiente método, donde "x" es el valor primitivo del número:

Number.prototype.volEsfera__ = function() {
const x = this.valueOf();
print(Number.isInteger(x));
print(Number.isFinite(x));
return 4/3*Math.PI*x**3;
};

Que ahora devuelve los resultados esperados:

(12).volEsfera__()

Si el método requiere datos adicionales (que no son parte de las propiedades del objeto) puede recibirlos como parámetros. Por ejemplo, el siguiente método calcula la raíz enésima del número desde el cual es llamado, pero como requiere el orden de la raíz y el orden no es una propiedad del número, debe ser recibido en forma de parámetro (n):

Number.prototype.raizn = function(n) {
const x = this.valueOf();
return x**(1/n);
};

Para calcular con este método la raíz cuarta de 8.23 y la raíz séptima de 12, se escribe:

(8.23).raizn(4)
(12).raizn(7)

En este ejemplo, como el número está siendo empleado en una expresión matemática no es necesario crear la variable "x", porque como se explicó, en esos casos JavaScript llama automáticamente a valueOf para obtener el número primitivo. Por lo tanto, la siguiente versión del método:

Number.prototype.raizn_ = function(n) {
return this**(1/n);
};

Devuelve los mismos resultados:

(8.23).raizn_(4)
(12).raizn_(7)

No obstante, para evitar errores, cuando el método es llamado desde un número literal (que es lo más frecuente), se aconseja emplear valueOf, para garantizar que se trabaje con el valor primitivo del número y no con el objeto.

Los métodos dinámicos no pueden ser creados con funciones flecha, porque, en las funciones flecha, JavaScript no crea la variable this y, en consecuencia, no es posible acceder a las propiedades del objeto desde el cual se llama al método.

Todas las propiedades y métodos añadidos en este capítulo son temporales (dejan de existir cuando se cierra el capítulo). Para que las propiedades y métodos puedan ser reutilizadas, deben ser guardadas en un archivo externo (una librería), como se verá posteriormente.