Funciones y Callbacks
Teoría
Introducción
En este apunte aprenderemos algunas particularidades de las funciones en NodeJS, que nos
permitirán aprovechar su capacidad al máximo.
Declaración de funciones
Las funciones NodeJS tienen varias particularidades con respecto a otros lenguajes. Veamos
todas las formas que ofrece el lenguaje para declarar una función
Estilo clásico
function mostrar(params) {
console.log(params)
}
Llamada a la función:
mostrar(args)
Al ser NodeJS un lenguaje que no requiere especificar el tipo de dato de sus variables (tipado
dinámico), tampoco es necesario especificar el tipo de dato que devuelven las funciones, ni el
tipo de dato de los parámetros que éstas reciben.
Las funciones también son objetos
En NodeJS (y en JavaScript en general), las funciones se comportan como objetos, por lo que
es posible asignar una declaración de función a una variable. Si elegimos esta opción,
podemos elegir no escribir en forma explícita el nombre en la declaración de la función (esto es,
para evitar escribirlo dos veces: en la variable, y en la función).
const mostrar = function(params) {
console.log(params)
}
Podemos ejecutarla de la misma manera en que lo hicimos en el ejemplo anterior.
Nuevo estilo (simplificado)
Las últimas versiones del lenguaje nos ofrecen una forma simplificada de declarar una función,
cuando la asignamos a una variable, obviando la palabra ‘function’, y agregando un operador
nuevo (esta forma de declarar funciones ofrece además algunas otras características que
veremos en mayor profundidad en otros apuntes).
La nueva sintaxis consiste en declarar únicamente los parámetros, y luego conectarlos con el
cuerpo de la función mediante el operador => (flecha gorda, o ‘fat arrow’ en inglés). Veamos un
ejemplo:
const mostrar = (params) => {
console.log(params)
}
La función se podrá usar de la misma manera que las anteriores.
En el caso de que la función reciba un solo parámetro, los paréntesis se vuelven opcionales,
pudiendo escribir:
const mostrar = params => {
console.log(params)
}
En el caso de que el cuerpo de la función conste de una única instrucción, las llaves se vuelven
opcionales, el cuerpo se puede escribir en la misma línea de la declaración (esto es así en
muchos otros lenguajes, no solo en NodeJS), y el resultado de computar esa única línea se
devuelve como resultado de la función, como si tuviera un “return” adelante. A esto se lo
conoce como “return implícito”. El ejemplo anterior se vería así:
const mostrar = params => console.log(params)
Y en este caso la función devolvería “undefined” ya que console.log es de tipo void y por lo
tanto no devuelve nada. Un ejemplo, igualmente trivial, pero más explícito, de return implícito
sería el siguiente:
const promediar = (a, b) => (a + b) / 2
const p = promediar(4, 8) // 6
Funciones como parámetros
Como hemos visto, en NodeJS es posible asignar una función a una variable. Esto es porque
internamente, las funciones también son objetos (y las variables, referencias a esos objetos).
Es por esto que NodeJS nos permite hacer que una función reciba como parámetro una
referencia a otra función.
Veamos un ejemplo, utilizando lo que aprendimos en el punto anterior:
const ejecutar = unaFuncion => unaFuncion()
const saludar = () => console.log('saludos')
ejecutar(saludar)
Y como ya sabemos: en donde puedo usar una variable, puedo usar también directamente el
contenido de esa variable:
ejecutar(() => console.log('saludos'))
En este ejemplo, la función ‘ejecutar’ recibe una función anónima, y la ejecuta.
Como es de esperarse, esto también funciona con funciones anónimas con parámetros:
const ejecutar = (unaFuncion, params) => unaFuncion(params)
const saludar = nombre => console.log(`saludos, ${nombre}`)
ejecutar(saludar, 'terricola')
Callbacks
Un callback es una función que se envía como argumento de otra función, con la intención de
que la función que hace de receptora ejecute la función que se le está pasando por parámetro.
De acuerdo a esta definición, podemos decir que la función “ejecutar” que usamos en el punto
anterior “recibe un callback”.
Ahora bien, uno de los casos en que más se utiliza este recurso es el siguiente:
Imaginemos que queremos que al finalizar una operación se ejecute un cierto código. Por
ejemplo, queremos escribir un archivo, y queremos registrar en un log la hora en se termine de
escribir. Es probable que no se pueda saber con exactitud en qué momento va a finalizar. En
algunos casos (ya veremos en cuáles) no podemos simplemente ejecutar la operación de
escritura, y luego a continuación, guardar el log. En estos escenarios, las funciones deben
recibir como último parámetro un callback, que (por convención) será ejecutado al finalizar la
ejecución de la función.
Veamos una función inventada para ver cómo funciona:
const formatFecha = f =>
`${f.getDate()}-${f.getMonth()+1}-${f.getFullYear()}`
// esta es mi función con callback
const escribirArchivo = (ruta, datos, callbackLog) => {
// acá va la parte en donde se escriben los datos en
// el archivo, en la ruta especificada.
// esta operación puede tardar un tiempo indeterminado.
// al finalizar, ejecuta el código que sigue a continuación
const fechaString = formatFecha(new Date())
callbackLog(fechaString, 'grabación exitosa')
}
// esta función será mi callback!
const loguear = (fecha, mensaje) => console.log(`${fecha}: $
{mensaje}`)
// así es la llamada a la función
escribirArchivo('/ruta/al/archivo', 'los datos', loguear)
Si mi función ‘escribirArchivo’ fuera la única que utiliza a la otra función ‘loguear’, en lugar de
declararla podríamos usar una función anónima, de la siguiente manera:
escribirArchivo('/ruta/al/archivo', 'los datos', (fecha, mensaje) =>
console.log(`${fecha}: ${mensaje}`))
Algunas convenciones
Es costumbre dentro de la comunidad de programadores de NodeJS seguir una convención a
la hora de realizar funciones con callbacks. En la mayoría de los casos, éstas cumplen con las
siguientes características:
● El callback siempre es el último parámetro.
● El callback suele ser una función que recibe dos parámetros.
● La función llama al callback al terminar de ejecutar todas sus operaciones.
○ Si la operación resultó en un error, la función llamará al callback pasando el
error obtenido como primer parámetro.
○ Si la operación fue exitosa, la función llamará al callback pasando null como
primer parámetro.
○ Si la operación (exitosa) generó algún resultado, éste se pasará al callback
como segundo parámetro.
Desde el lado del callback, estas funciones deberán saber cómo manejar estos parámetros.
Por este motivo, nos encontraremos muy a menudo con la siguiente estructura (ejemplo):
const ejemploCallback = (error, resultado) => {
if (error) {
// hacer algo con el error!
} else {
// hacer algo con el resultado!
}
}
Callbacks anidados
A veces, debemos realizar varias de estas operaciones encadenadas, en serie. Lo que
mostramos a continuación, si bien no es muy una buena práctica (y más adelante veremos
formas de evitarlo) es un fragmento de código con el que nos podemos encontrar, en el cual
una función llama a un callback, y éste a otro callback, y éste a otro, y así sucesivamente. A
esto se le conoce como “callback hell” (infierno de callbacks).
Ejemplo:
const copiarArchivo = (nombreArchivo, callback) => {
buscarArchivo(nombreArchivo, (error, archivo) => {
if (error) {
callback(error)
} else {
leerArchivo(nombreArchivo, 'utf-8', (error, texto) => {
if (error) {
callback(error)
} else {
const nombreCopia = nombreArchivo + '.copy'
escribirArchivo(nombreCopia, texto, (error) => {
if (error) {
callback(error)
} else {
callback(null)
}
})
}
})
}
})
}
Objetos y JSON
Teoría
Cómo crear un objeto en javascript?
Simplemente escribiendo dos llaves { } ya tenemos un objeto en javascript. Es usual (pero no
obligatorio) guardar los objetos dentro de alguna variable:
Ejemplo
const persona = {}
Cómo agregarle propiedades a un objeto?
Simplemente haciendo nombreDelObjeto.nombreDeLaPropiedad = algunValor ...:
● Si la propiedad no existía, creamos la propiedad con el valor dado.
● Si la propiedad ya existía, actualizamos su valor.
Ejemplo
const persona = {}
persona.nombre = 'mariano'
persona.edad = 32
También podemos utilizar la siguiente sintaxis:
const persona = {}
persona['nombre'] = 'mariano'
persona['edad'] = 32
Esta sintaxis nos da la ventaja de que como el argumento que pasamos entre corchetes es un
string, este puede ser obtenido de diversas maneras, o incluso recibido por parámetro.
Cómo acceder a las propiedades a un objeto?
Al igual que para agregar, existen dos maneras de acceder a una propiedad de un objeto:
● Utilizando la notación: objeto.propiedad
● Utilizando la propiedad: objeto[propiedad]
Cualquiera de las dos variantes me devuelve el mismo resultado, que es el valor de la
propiedad en cuestión.
Ejemplo
console.log(persona.nombre)
console.log(persona['edad'])
Cómo quitarle propiedades a un objeto?
Para quitarle una propiedad a un objeto usaremos la palabra reservada delete y a
continuación la propiedad que queremos borrar, accediéndola desde su objeto contenedor:
Ejemplo
delete persona.edad
Propiedades anidadas
Es posible declarar propiedades que sean a su vez también objetos.
Ejemplo
const persona = {}
persona.nombre = 'mariano'
persona.edad = 32
persona.dirección = {}
persona.direccion.calle = 'rivadavia'
persona.direccion.numero = 1234
persona.telefonos = []
persona.telefonos.push('15-1234-5678')
persona.telefonos.push('15-1234-5000')
Declaración e inicialización en un solo paso
Nótese que al representar al objeto (mostrarlo) éste aparece como una serie de pares de datos,
separados por comas, todo dentro de las llaves que delimitan al objeto. Estos pares de datos
representan siempre una clave y un valor, es decir, el nombre del par, y el valor asociado a ese
nombre. El valor asociado puede ser cualquier cosa, incluyendo (no exclusivamente) números,
strings, objetos, arrays, etc.
Es posible declarar un objeto en js ya inicializándolo con sus valores, todo en una sola
operación. En este caso, se debe separar cada clave de cada valor usando dos puntos ( : ), y
cada par clave/valor del siguiente usando comas ( , ).
Ejemplo
const persona = {
nombre: 'mariano',
edad: 32,
direccion: {
calle: 'rivadavia',
numero: 1234
},
telefonos: ['15-1234-5678', '15-1234-5000']
}
JSON: JavaScript Object Notation
JSON es una forma de representar objetos de javascript como texto. Es considerada hoy una
de las formas de serialización de datos más utilizada. Es fácilmente interpretable ya que tiene
la misma estructura de pares clave/valor separados por dos puntos, y esos pares a su vez
separados por comas. La principal diferencia es que mientras que en los objetos de js las
propiedades se ven como variables (sin comillas), en json se escriben como strings, usando
comillas, y siempre usando comillas dobles ( " ).
Ejemplo
// Objeto en js:
const persona = {
nombre: 'mariano',
edad: 32
}
JSON válido
// forma indentada:
{
"nombre": "mariano",
"edad": 32
}
// forma compacta:
{ "nombre": "mariano", "edad": 32 }
JSON Inválido
// faltan comillas en las claves:
{
nombre: "mariano",
edad: 32
}
// las comillas de los strings deben ser dobles:
{
'nombre': 'mariano',
'edad': 32
}
// faltan las comas:
{
'nombre': 'mariano'
'edad': 32
}
// sobra la coma del último par clave/valor!:
{
'nombre': 'mariano',
'edad': 32,
}
Arrays en formato JSON
Es importante notar que no solo es posible tener objetos en formato JSON, sino también arrays
en dicho formato.
Ejemplo
// array de objetos en formato JSON válido
[
{
"nombre": "mariano",
"edad": 34
},
{
"nombre": "erica",
"edad": 36
}
]
Cómo convertir un objeto js a JSON y viceversa?
Javascript cuenta con una librería nativa JSON que nos permite tanto pasar de objeto a JSON y
de JSON a objeto.
Convertir de objeto a JSON
Se utiliza la función JSON.stringify( … ) que recibe un objeto y devuelve un string con la
representación de ese objeto en formato JSON.
Ejemplo
const persona = {
nombre: 'mariano',
edad: 32
}
console.log(JSON.stringify(persona))
Salida
{ "nombre": "mariano", "edad": 32 }
Si deseamos que el string salga con la indentación característica de JSON, podemos agregar
algunos parámetros a la llamada al stringify.
console.log(JSON.stringify(persona, null, 4))
Salida
{
"nombre": "mariano",
"edad": 32
}
(En este caso, el 4 representa la cantidad de espacios que se dejará como indentación entre
nivel y nivel).
Convertir de JSON a objeto
Se utiliza la función JSON.parse( … ) que recibe un string con la representación de un objeto
en formato JSON y devuelve el objeto correspondiente de js.
Ejemplo
const persona = JSON.parse('{ "nombre": "mariano", "edad": 32 }')
console.log(persona.edad)
Salida
32
Funciones, Objetos y JSON
Práctica
Preparación
Descargar la carpeta con los archivos de prueba y esqueleto de la solución desde el aula
virtual. Se trabajará únicamente sobre estos archivos, salvo que se indique lo contrario.
Desarrollar las siguientes funciones
actualizarArchivosDeudas
Recibe las rutas de cuatro archivos (de deudas viejo, de pagos, de deudas nuevo, y de log).
De los archivos de entrada sabemos lo siguiente:
● El archivo de deudas viejo no tiene un orden específico. Sus registros no presentan
repetidos de acuerdo a su dni, ni a su nombre. Todos sus campos poseen datos válidos
(no null, no vacíos). El campo ‘debe’ siempre es un número positivo.
● El archivo de pagos tampoco tiene un orden específico, y puede contener múltiples
registros con el mismo dni y apellido. El campo ‘fecha’ no se repite entre ninguno de los
registros. El campo ‘pago’ siempre es un número positivo.
Esta función no devuelve nada, y debe generar:
● Un archivo de deudas actualizado con todos los pagos realizados, a guardarse en la
ruta especificada como ‘deudas nuevo’. Este archivo debe tener las mismas
características que el de entrada (‘deudas viejo’).
● Un archivo de log, en la ruta provista, en donde quede un registro de los eventos
detallados en el último punto.
La función extrae el contenido de los archivos de la carpeta de entrada (in), los ordena, procesa
las actualizaciones de los pagos sobre las deudas, y graba el resultado final en un nuevo
archivo en la carpeta de salida (out).
ordenarPorClave
Recibe un array con objetos, y el nombre de un campo en formato string. Devuelve el array
original, ordenado en forma ascendente por el campo provisto.
Opcional
En lugar de un único string, recibe un array de strings. Devuelve el array original, ordenado en
forma ascendente por los campos provistos en el array de claves, siguiendo el mismo orden de
importancia (primera clave, primer campo que determina el orden, etc).
actualizarDeudas
Recibe un array con deudas, un array con pagos, y una función a la que podemos llamar,
pasandole un mensaje, cada vez que precisemos loguear un evento. Devuelve un array con las
deudas actualizadas según los pagos, siguiente el siguiente criterio:
● Si aparece un registro de pago con un dni que no coincide con el de ninguna deuda, ese
registro no se procesa, y se debe loguear como operación inválida.
● Si aparece un registro de pago que coincide con una deuda en su dni pero no en su
apellido, el mismo no se procesa, y se deben loguear ambos, la deuda y el pago.
● Si un registro de deuda no posee pagos asociados, se agrega directamente al array
actualizado, sin cambios.
● Si luego de aplicar todos los pagos correspondientes a una deuda, ésta queda aún
queda en positivo, se agrega al array actualizado con el nuevo importe.
● Si luego de aplicar todos los pagos correspondientes a una deuda, ésta queda en cero o
menos, la misma no debe agregarse al array de deudas actualizado.
● En particular, si la deuda queda en negativo (por debajo de cero), se deben loguear los
datos del cliente y cuánto saldo a favor tiene.
Observación
Para para facilitar la tarea, ya se cuenta con algunas funciones desarrolladas, y se proveen
también las firmas de las funciones pedidas, junto con su comentario correspondiente (formato
JSDOC). Además, se incluyen, como ejemplo, dos documentos de entrada con sus respectivos
documentos de salida, para usar como lote de prueba / verificación.