“Funciones en node.js y Javascript. Más detalles”, es la segunda parte de lo más importante de las funciones en node.js y Javascript. El tema de funciones es muy importante, y también muy extenso. Aprenderás sobre el prototipo de una función, el comportamiento de la referencia a this,
propiedad especial prototype
, elevado de funciones y sus beneficios, variable adicional arguments
y métodos útiles como call
and apply
. Y como extra muy importante, si llegas al final, las mejores recomendaciones de comunicación a través del código.
El prototipo de una función en Node.js y Javascript
En la primera parte de Funciones en Node.js y Javascript. Pudimos entender que las funciones son objetos, y el porqué las funciones son objetos. Una función tiene un prototipo, si no sabes que es un prototipo en Javascript, esto explica en this post.
Las funciones tienen como prototipo a Function.prototype
. Aunque a diferencia de los objetos literales no se ven tan clara la relación, podemos ejecutar la siguiente prueba.
functions greet() {
return 'Hola';
}
greet.__proto__ = function.prototype; // true
El prototipo de Function.prototype
is Object.prototype
. Con esta última sentencia comprobamos lo que hemos dicho de los objetos en Javascript, tienen como objeto base a Object.prototype.
greet.__proto__.proto === object.prototype; // true;
Aquí el diagrama.
Tipos de invocaciones y referencia this
Las funciones en Node.js y Javascript se comportan de manera extraña al ser invocadas. Este comportamiento cambia según la forma en como se invoque e influye en como se trata la referencia this
. En las siguientes cuatro secciones se explican estos tipos de invocaciones.
Invocación de funciones en Node.js y Javascript como método
Cuando una función se asigna a una propiedad de un objeto entonces decimos que es un método. Y cuando un método es invocado se puede hacer referencia al objeto al que pertenece a través de la palabra reservada this
.
Como vimos en el tema de objetos, se puede acceder a una propiedad a través de corchetes, así ['nombreDePropiedad']
.
const James = {
name: 'Jaime',
greet: functions() {
return `Hola soy ${this.name}`;
}
};
James.greet(); // 'Hola soy Jaime'
jaime['greet'](); // 'Hola soy Jaime'
Invocación de funciones en Node.js y Javascript como función
Cuando una función se invoca de esta manera y quiere hacer referencia a this
, este debería obtener null
either undefined
, pero en lugar de eso, hace referencia al objeto global del ambiente donde se está ejecutando el código, en el caso del navegador web hace referencia a window
. En el caso de node.js es el objeto global
.
functions greet() {
return this;
}
greet(); // En el navegador es window y en node.js es global
Dentro del navegador web, es algo como lo siguiente.
Window {
alert: ƒ alert()
console: console {},
fetch: ƒ fetch(),
isNaN: ƒ isNaN(),
clearInterval: ƒ clearInterval(),
clearTimeout: ƒ clearTimeout(),
setInterval: ƒ setInterval(),
setTimeout: ƒ setTimeout()
...
}
Por otro lado en node.js es algo como esto.
<ref *1> object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[symbol(nodejs.util.promisify.custom)]: [Getter]
},
queueMicrotask: [Function: queueMicrotask],
performance: [Getter/Setter],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[symbol(nodejs.util.promisify.custom)]: [Getter]
}
...
}
Ahora, si una función interna hace referencia a this
.
functions greet() {
functions saludarInternamente() {
return this;
}
return saludarInternamente();
}
greet(); // En el navegador web es window, en Node.js es global
Esta función interna sigue haciendo referencia al objeto window
either global
. Y lamentablemente este mismo comportamiento sucede aun si la función greet
es el método de un objeto.
const James = {
name: 'Jaime',
greet: functions() {
functions saludarInternamente() {
return this;
}
return saludarInternamente();
}
};
James.greet(); // En el navegador web es window, en Node.js es global
Cuando el objeto se crea a través de una clase, la referencia a this
is undefined
, esto es una mejora, sin embargo, ¿Tendría más sentido que si hiciera referencia al objeto en cuestión? Yo opino que si, pero ese ejemplo vamos a guardarlo para cuando toquemos las clases.
Invocación de funciones en Node.js y Javascript como constructor
Javascript es un lenguaje de programación orientado a objetos basado en prototipos. Originalmente no usa clases, pero intenta simular una sintaxis basada en clases. Estamos hablando del operador new
.
Las funciones constructoras son funciones que se invocan con el operador new
para crear nuevos objetos. Como el ejemplo de abajo.
functions Persona(name, edad) {
this.name = name;
this.edad = edad;
}
const James = new Persona('Jaime', 33);
Esta forma de crear objetos con una función constructora permite que la referencia a this
esté correctamente enlazada al nuevo objeto. Lo podemos comprobar cuando ejecutamos las siguientes líneas.
James.name; // 'Jaime'
James.edad; // 33
Propiedad especial prototype
Existe una propiedad especial llamada prototype
en las funciones, esta no es la misma propiedad oculta que enlaza a Function.prototype
.
Esta propiedad especial prototype
permite usar a una función constructora para simular la herencia clásica. Así cuando se crea un nuevo objeto usando la función constructora, este nuevo objeto pueda usar las propiedades de prototype
. Esto propiedad básicamente funciona igual que las propiedades Object.prototype
, Function.prototype
, Array.prototype
, y demás prototipos de objetos en Javascript. Con la diferencia, que nosotros agregamos el contenido manualmente a prototype
. A continuación un ejemplo.
functions Persona(name, edad) {
this.name = name;
this.edad = edad;
}
Persona.prototype.greet = functions () {
return `Hola soy ${this.name} y tengo ${this.edad} años`;
};
Y con este ejemplo comprobamos que la función greet
invocada desde el objeto James
, efectivamente tiene la referencia a this
enlazada correctamente. Y puede acceder al name
and edad
del nuevo objeto James
.
const James = new Persona('Jaime', 33);
James.greet(); // Hola soy Jaime y tengo 33 años
Visualmente es como el siguiente diagrama.
Por último te preguntarás, ¿Por qué no agregamos los métodos dentro de la función constructora Persona? Muy parecido a lo que hicimos cuando vimos el tema de closure.
functions Persona(name, edad) {
this.name = name;
this.edad = edad;
this.greet = functions () {
return `Hola soy ${this.name} y tengo ${this.edad} años`;
};
}
const James = new Persona('Jaime', 33);
James.greet(); // Hola soy Jaime y tengo 33 años
Pues la respuesta es que también se puede hacer, pero la desventaja es que estaríamos creando nuevas funciones cada vez que creamos una instancia de Persona
. Con el uso de la propiedad prototype
no se crean nuevas funciones, solo se hacen referencia a ellas, es decir, cada nuevo objeto debe ir a buscar en sus prototipos a la función saludar.
Nota importante sobre las funciones constructoras
Las funciones constructoras siempre empiezan con una letra mayúscula, no porque la sintaxis del lenguaje lo necesite, sino para que los programadores identifiquen a una función constructora de una que no lo es y se use adecuadamente el operador new
.
Invocación de funciones en Node.js y Javascript con apply
and call
Como ya hemos hecho mucho hincapié, las funciones en Node.js y Javascript son objetos. Por lo tanto una función también tiene métodos. Los métodos del prototipo Function.prototype.apply
and Function.prototype.call
, permiten definir explícitamente la referencia a this
al ejecutarse el cuerpo de la función.
Invocación con Function.prototype.apply
const James = {
name: 'Jaime',
greet: functions(saludo) {
return `${saludo}, soy ${this.name}`;
}
};
James.greet('Hola'); // Hola, soy Jaime
James.greet.apply({ name: 'Pepito' }, ['Buenos días']); // Buenos días, soy Pepito
El método apply
recibe como primer parámetro el objeto al que se va a enlazar la función y podrá hacer referencia usando this
y como segundo parámetro un arreglo de valores que son los parámetros de la función. En este caso la función saludar solo recibe una cadena de caracteres a través del parámetro saludo
. Es por eso que solo necesita un arreglo con un solo elemento.
Aquí un ejemplo con una función de dos parámetros.
functions sumar(to, b) {
return to + b;
}
sumar.apply(null, [1, 2]); // 3
En este ejemplo el primer parámetro del método apply
is null
porque la función no se enlaza a un objeto y no necesita usar la referencia this
.
Invocación con Function.prototype.call
Este método es una simplificación del método apply
, igualmente recibe dos parámetros, el objeto al cual se hará referencia con this
y los argumentos de la función que va a invocar. La única diferencia es que en lugar de recibir un array de parámetros para la función que se va a invocar, recibe cualquier número de parámetros separados con comas. A continuación un ejemplo.
functions sumar(to, b) {
return to + b;
}
sumar.call(null, 1, 2); // 3
Parámetro extra arguments
de las funciones en Node.js y javascript
Cuando una función es invocada, adicional a la lista de parámetros, tiene acceso a un parámetro llamado argument
. Este parámetro extra contiene la lista de todos los parámetros pasados en la invocación de una función.
functions greet(complemento) {
return arguments;
}
greet('Hola');
/*
Arguments [
0: "Hola",
callee: ƒ saludar(saludo),
length: 1,
Symbol(Symbol.iterator): ƒ values(),
[[Prototype]]: Object
]
*/
arguments
es un objeto muy parecido a un array
, pero no lo es. En el ejemplo anterior vemos que su prototipo es Object.prototype
y para ser un array
debería ser Array.prototype
.
Debido a lo anterior, arguments
no tiene todos los métodos útiles de un array
. Por ejemplo, que tal si queremos recorrer la lista de todos los parámetros, en un arreglo se podría usar el método Array.prototype.forEach
, pero eso no es posible.
Una forma de mitigar este inconveniente es convertir el objeto arguments
a un arreglo, usando el método Array.from
. Y entonces si usar los métodos de Array.prototype
que necesites. Ejemplo.
functions greet(complemento) {
const args = array.from(arguments);
args.forEach(actual => console.log(actual));
}
greet('Hola', 'Soy Jaime', 'Tengo 33 años');
/*
Hola
Soy Jaime
Tengo 33 años
*/
Aunque la función saludar solo recibe un parámetro, aun así todos los parámetros que se pasen en la invocación, se guardan en arguments
.
Elevado de funciones en Node.js y JavaScript
El elevado se refiere a que la creación de función sube hasta el inicio del ámbito en donde se encuentre. Solo la declaración normal de función puede ser elevada. Ejemplo.
functions greet(complemento) {
return agregarComplemento(complemento);
functions agregarComplemento(complemento) {
return `Hola, ${complemento}`;
}
}
greet('soy Jaime'); // 'Hola, soy Jaime'
Como podemos ver la función agregarComplemento
se puede utilizar antes de su definición, esto es porque javascript antes de ejecutar el código, crea la función al inicio del ámbito local donde se encuentra, el código anterior se traduce antes de ser ejecutado a lo siguiente.
functions greet(complemento) {
functions agregarComplemento(complemento) {
return `Hola, ${complemento}`;
}
return agregarComplemento(complemento);
}
greet('soy Jaime'); // 'Hola, soy Jaime'
Las funciones en forma de expresión y funciones flecha no cuentan con este elevado de funciones, así que es importante definirlas antes de ser usadas.
Beneficio del elevado de funciones
Este comportamiento parece confuso, pero cuando estás programando con funciones es muy útil. Cuando se programa se tiene funciones principales y funciones secundarias. Para la persona que lee nuestro código muy a menudo es suficiente con visualizar las funciones principales para entender el código, no tiene que perder el tiempo en ver los detalles de las funciones secundarias. Imaginate un archivo con cien líneas de código y compara encontrar las funciones principales en las líneas de inicio contra encontrar las funciones principales al final.
Veamos un ejemplo. Aquí al lector solo le interesa a alto nivel lo que renderiza renderVista
.
functions renderVista() {
renderHeader();
renderMain();
renderFooter();
}
functions renderHeader() {
renderMenu();
renderBanner();
renderSearchForm()
// ....
}
functions renderMenu() {
// ...
}
functions renderBanner() {
// ...
}
functions renderSearchForm() {
// ...
}
functions renderMain() {
renderContent();
renderAside();
// ...
}
functions renderContent() {
// ...
}
functions renderAside() {
// ...
}
functions renderFooter() {
// ...
}
Es facil, ¿Verdad?, solo leemos la primera función e ignoramos el resto del código.
¿Pero qué pasa cuando ahora la función principal se encuentra al final? Ejemplo.
functions renderMenu() {
// ...
}
functions renderBanner() {
// ...
}
functions renderSearchForm() {
// ...
}
functions renderHeader() {
renderMenu();
renderBanner();
renderSearchForm()
// ....
}
functions renderMain() {
renderContent();
renderAside();
// ...
}
functions renderContent() {
// ...
}
functions renderAside() {
// ...
}
functions renderFooter() {
// ...
}
functions renderVista() {
renderHeader();
renderMain();
renderFooter();
}
Además de ir hasta el final para encontrar la función principal que nos interesa usar, se pierde el sentido de este archivo. O al menos tardamos más en entender el objetivo de este archivo, el cual es renderizar la vista.
Por último, si quisieras, puedes hacer que todo viva dentro de una sola función, utilizando funciones anidadas. Haciendo más claro la relación entre las funciones. Como el siguiente ejemplo.
functions renderVista() {
renderHeader();
renderMain();
renderFooter();
functions renderHeader() {
renderMenu();
renderBanner();
renderSearchForm()
// ....
functions renderMenu() {
// ...
}
functions renderBanner() {
// ...
}
functions renderSearchForm() {
// ...
}
}
functions renderMain() {
renderContent();
renderAside();
// ...
functions renderContent() {
// ...
}
functions renderAside() {
// ...
}
}
functions renderFooter() {
// ...
}
}
¿Las funciones son referencia o valor?
Como ya vimos en la publicación sobre objetos, dado que la función es un objeto, entonces una función siempre será una referencia.
Diferencias importantes entre funciones flecha y tradicionales
Las funciones flechas tienen ciertas limitaciones si las comparamos con las funciones tradicionales, aunque hay que decir que si tu código está enfocado a la programación funcional, este tipo de funciones son muy convenientes.
Sus limitaciones en comparacion con las funciones tradicionales son:
- No tienen el enlace correcto a
this
y no puede usarsuper()
.- Debido al punto anterior no funciona bien con los métodos
call
,apply
andbind
. - No pude usarse como método de un objeto.
- Debido al punto anterior no funciona bien con los métodos
- No tiene la propiedad especial
prototype.
- Consecuencia del punto anterior no es posible usar una función flecha como función constructora.
- No tiene acceso a parámetro extra
arguments
.
Veremos estas limitaciones en una futura publicación sobre las funciones flecha.
Comunicación con funciones en node.js y Javascript
Como ya lo hemos mencionado en otras publicaciones, el código es una forma de comunicación entre lo que creamos ahora con nuestro yo del futuro y nuestro equipo. Sabemos que la programación es una actividad social de mucha comunicación y ser eficientes en esta comunicación ahorra tiempo y dinero a programadores y no programadores.
Recordemos que las funciones son las unidades más pequeñas de organización de código. Además sin las funciones, ¿Cómo definiríamos el comportamiento de objetos y de funciones más grandes? Las funciones nos sirven para:
- Organize the code
- Reuse code
- It is used for the composition of objects, adding behaviors.
- It is used for the composition of more complicated functions
Teniendo en cuenta lo anterior, he aquí algunas recomendaciones a la hora de crear funciones.
Un muy, muy buen nombre
En this post encontrarás las recomendaciones de nombres.
Lo más simple que se pueda
Una función debe ser lo más simple posible, y para eso debe tener una meta pequeña y específica, lo que conlleva a no complicar las cosas y hacer una cosa a la vez, paso a pasito, algo sencillo y bien hecho.
Cuando se resuelve un problema complicado, la forma recomendada de solucionarlo es como si imaginaras que eres un bebe, y tu problema es que no puedes correr. ¿Suena complicado no crees? Probablemente pudiste correr bien sin caerte alrededor de los tres años. Sin darte cuenta dividiste un problema grande en partes más pequeñas. Para llegar a correr, primero aprendiste a gatear, luego a caminar y finalmente intentaste correr con muchas caídas en el camino. Y si dividimos aún más estas tres fases encontraremos objetivos aún más pequeños, ejemplo, dar el primer paso. Así son las funciones, solucionan el problema pedacito por pedacito.
Siguiendo el enfoque anterior, una función simple es la que cumple con lo siguiente.
- Se enfoca en resolver una sola cosa sencilla pero bien hecha.
- Tendrá pocas líneas de código
- Cuando mucho una o dos condiciones
if
o unswitch
- Cuando mucho uno o dos ciclos
for
,while
,do while
, etc. - Muy poquita indentación debido a los tres puntos anteriores
- Por mucho tres parámetros, un humano solo puede recordar cuatro cosas a la vez, hagámosle la vida más fácil a nuestro compañero. Si de verdad necesitas muchos parámetros, puedes utilizar un objeto literal como único parámetro, así al menos no tenemos que recordar el orden en que deben de ir.
- Todos los puntos anteriores lograrán que la función sea muy fácil de leer y comunicar.
En una futura publicación haremos una aplicación pequeña donde aplicaremos estas recomendaciones
Conclusions
El tema de funciones es muy grande, aún nos faltan temas por ver. Como nos seguimos dando cuenta, las funciones son una pieza fundamental en Javascript y en el desarrollo de software en general. Según donde se invoque una función, podrá enlazarse al objeto this
correcto. El elevado de funciones es beneficioso para la comunicación de las intenciones de nuestras funciones.
No olvidemos que las funciones son objetos. Las recomendaciones expuestas reflejan el principio ágil de simplicidad. Manteniendo las cosas simples, somos más productivos. En el caso de nuestro código, aumentamos el poder de flexibilidad y mantenimiento del mismo a través de una comunicación clara y código fácil de comprender.