Una función es un trozo de código con nombre propio; la función flecha es ese mismo trozo… con estilo futurista.
Este capítulo aborda la definición y uso de funciones en Node.js, tanto en su formato tradicional como en el más moderno formato de función flecha (arrow function en inglés).
Puesto que, como decíamos en la presentación, el curso está orientado a personas con nociones de programación, no me extenderé en explicar qué es una función y por qué es importante. De ello da buena cuenta la cita elegida para abrir este capítulo.
En su lugar, avanzaremos rápido y presentaremos el modo de definirlas, acceder a sus argumentos, devolver valores e invocarlas.
Índice
Sintaxis
Básicamente hay dos sintaxis para definir una función en Node.js: la sintaxis tradicional y la de función flecha, o arrow function en inglés, más compacta que la tradicional.
Más allá de la sintaxis utilizada en su definición, a grandes rasgos no existe ninguna otra diferencia entre ellas: ambas tienen las mismas características, soportan básicamente las mismas funcionalidades y se invocan del mismo modo.
Sintaxis tradicional
Vamos a comenzar con la definición tradicional, ilustrada en el siguiente ejemplo:
// saludar() imprime un saludo por consola
function saludar() {
console.log("¡Hola, mundo!")
}
// Invocamos a la función saludar()
saludar()
En Node.js las funciones son objetos de tipo function, pero objetos al fin y al cabo. Por tanto, podemos asignar funciones a variables y constantes del mismo modo que se asignan objetos.
Esta funcionalidad nos permite definir funciones anónimas, es decir, expresiones de función que no establecen el nombre en su definición, que se asignan a variables o constantes y que son ejecutadas mediante invocaciones a través de dichas variables o constantes.
Como siempre en programación, es más fácil entender lo anterior viendo un ejemplo:
// saludar es una constante a la que se le asigna
// una expresión de función anónima (no tiene nombre)
const saludar = function () {
console.log("¡Hola, mundo!")
}
// Invocamos a la función anónima a través
// de la constante a la que se asignó
saludar()
// Podemos asignar variables/constantes
// de tipo función como se haría de modo convencional
let saludarVar = saludar
saludarVar()
// Las funciones son objetos de tipo function en JS
console.log("Tipo de una función: " + typeof saludar)
console.log("Tipo de una función: " + typeof saludarVar)
Salida esperada:
¡Hola, mundo!
¡Hola, mundo!
Tipo de una función: function
Tipo de una función: function
Sintaxis moderna: Funciones flecha
La definición de expresiones de función basada en sintaxis flecha nos permite prescindir del uso de la palabra reservada function en favor del símbolo =>.
Además, si la función está compuesta por una única línea, nos permite prescindir de los símbolos de inicio y final de bloque ({ y }), resultando en una expresión mucho más compacta.
El siguiente ejemplo muestra la definición de nuestra conocida función saludar() con los dos tipos de sintaxis:
// Sintaxis de expresión de función tradicional
const saludar = function () {
console.log("¡Hola, mundo!")
}
// Sintaxis de función flecha con bloque de código
const saludarFlecha = () => {
console.log("¡Hola, mundo!")
}
// Sintaxis de función flecha con un solo comando
const saludarFlechaCmd = () => console.log("¡Hola, mundo!")
// Independientemente de su sintaxis,
// las funciones se invocan del mismo modo
saludar()
saludarFlecha()
saludarFlechaCmd()
En adelante, en este curso utilizaremos siempre la sintaxis de función flecha.
¿Definir antes de usar?
Estamos de nuevo ante uno de los muchos líos y trampas que Node.js tiende a los incautos programadores: dependiendo del modo en que se defina, la función puede invocarse o no antes de ser definida.
Node.js permite invocar una función antes de definirse si su definición está basada en sintaxis tradicional y estableciendo un nombre (function foo()), pero no permite invocarla si se define como expresión de función, sea o no función flecha, a no ser que se asigne a una variable utilizando var... vamos, bastante lío para las ventajas que aporta.
De nuevo voy a tirar de experiencia y dar un consejo de viejo: olvídate de estas triquiñuelas y define la función siempre antes de usarla. El código resulta más legible y evitas errores de transpilación.
Parámetros
La declaración y uso de parámetros o argumentos de funciones en Node.js sigue la filosofía del propio lenguaje: libertinaje total y absoluto en base a los siguientes principios:
- Como comentamos en el capítulo sobre constantes y variables, Node.js es un lenguaje débilmente tipado que no requiere establecer el tipo al declarar una variable o constante. Por tanto, los argumentos de una función no están tipados y, como consecuencia, aceptan valores de cualquier tipo.
- El número de parámetros definidos en la función y el número de parámetros pasados a la función no tienen por qué coincidir: los parámetros que no se pasen se considerarán con valor
undefined, y los parámetros pasados que excedan los definidos se ignorarán silenciosamente.
Vamos a ver este guirigay con un ejemplo:
// Redefinimos nuestra función saludar
// Para que acepte ahora dos argumentos
const saludar = (saludo, destinatario) =>
console.log("¡" + saludo + ", " + destinatario + "!")
// Esta sería la llamada con el número y tipo
// de argumentos "correcto" (aunque JS no hará
// ninguna comprobación)
saludar("Hola", "mundo")
// En esta llamada se omite el segundo argumento,
// que dentro de la función tomará el valor undefined
saludar("Hola")
// En esta llamada el segundo argumento es de un tipo
// inesperado, pero JS no generará ningún error
saludar("Hola", 1010)
// En esta llamada, con más argumentos de los definidos
// se ignorará el tercer argumento
saludar("Adiós", "mundo", "cruel")
Salida esperada:
¡Hola, mundo!
¡Hola, undefined!
¡Hola, 1010!
¡Adiós, mundo!
Valor por defecto
Decíamos anteriormente que Node.js no obliga a invocar las funciones con todos los parámetros con los que se definieron, y que de ser así dichos parámetros toman el valor undefined.
Node.js permite asignar valores por defecto a los parámetros, de modo que tomen dicho valor si la función se invoca sin establecer un valor para ellos:
// La función saludar() define un valor por defecto
// para el primer argumento
const saludar = (saludo = "Hola", destinatario) =>
console.log("¡" + saludo + ", " + destinatario + "!")
// Puesto que el argumento con valor por defecto
// NO está en las últimas posiciones (hay argumentos
// posteriores que no definen valor por defecto)
// es necesario establecer su valor explícitamente a undefined
// para que se aplique el valor por defecto
saludar(undefined, "mundo")
// Esta sintaxis es inválida
// saludar(, "mundo")
// La función saludarOtraVez() define un valor por defecto
// para el segundo argumento
const saludarOtraVez = (saludo, destinatario = "mundo") =>
console.log("¡" + saludo + ", " + destinatario + "!")
// Puesto que los valores por defecto están en los últimos
// argumentos, no hace falta definirlos explícitamente
// como undefined para que se aplique el valor por defecto
saludarOtraVez("Hola")
El mecanismo tras la aplicación de parámetros por defecto es bien sencillo: si un parámetro tiene un valor por defecto y dicho parámetro se establece a undefined al invocar a la función, se le aplica el valor por defecto.
Si recuerdas, dijimos que aquellos parámetros cuyo valor no se establecía explícitamente al llamar a la función se establecían implícitamente a undefined, con lo que establecer un parámetro a undefined o no establecerle valor alguno fuerza a Node.js a aplicar el valor por defecto al parámetro.
Parámetros como array
Soy consciente de que no hemos aún llegado al capítulo de los arrays, y también de que la siguiente funcionalidad se puede considerar como avanzada y por tanto fuera del alcance de este curso. Estás en tu derecho a saltar a la siguiente sección, y de hecho te lo recomiendo.
¿Sigues? Pues solamente para que sepas que puede hacerse, Node.js permite acceder a los argumentos de una función a través de arrays y, por tanto, sin necesidad de definirlos individualmente:
// arguments[] permite acceder a argumentos
// incluso no definidos pero solamente está soportado
// por funciones con sintaxis tradicional
const saludar = function () {
const saludo = arguments[0]
const destinatario = arguments[1]
console.log("¡" + saludo + ", " + destinatario + "!")
}
// En funciones de tipo flecha, se pueden gestionar los argumentos
// como un array, pero con una sintaxis diferente
const saludarOtraVez = (...argumentos) => {
const saludo = argumentos[0]
const destinatario = argumentos[1]
console.log("¡" + saludo + ", " + destinatario + "!")
}
// En funciones de tipo flecha, podemos mezclar argumentos
// definidos y un array de argumentos, pero éste siempre
// deberá definirse en última posición de la lista
const saludarUnaVezMas = (saludo, ...restoDeArgumentos) => {
const destinatario = restoDeArgumentos[0]
console.log("¡" + saludo + ", " + destinatario + "!")
}
saludar("Hola", "mundo")
saludarOtraVez("Hola", "mundo")
saludarUnaVezMas("Hola", "mundo")
Salida esperada:
¡Hola, mundo!
¡Hola, mundo!
¡Hola, mundo!
Valor de retorno
En Node.js todas las funciones devuelven un valor sin necesidad de establecerlo en su definición.
Este valor devuelto es undefined si la función finaliza sin ejecutar el comando return. Por el contrario, si return se invoca, se devolverá el valor pasado al comando:
// Puesto que no invoca al comando return,
// esta función devuelve siempre undefined
const saludar = (saludo, destinatario) => {
console.log("¡" + saludo + ", " + destinatario + "!")
}
// Esta función invoca al comando return,
// devolviendo el valor almacenado en la constante saludoCompleto
const componerSaludo = (saludo, destinatario) => {
const saludoCompleto = "¡" + saludo + ", " + destinatario + "!"
return saludoCompleto
}
const nada = saludar("Hola", "mundo")
console.log("saludar() devolvió el valor " + nada)
saludo = componerSaludo("Hola", "mundo")
console.log("componerSaludo() devolvió el valor " + saludo)
Salida esperada:
¡Hola, mundo!
saludar() devolvió el valor undefined
componerSaludo() devolvió el valor ¡Hola, mundo!
Hay una excepción a las reglas expuestas: las funciones flecha compuestas por un único comando devuelven el valor devuelto por dicho comando sin necesidad de invocar return:
// Las funciones flecha de un solo comando
// devuelven el valor devuelto por dicho comando
const sumar = (operando1, operando2) => operando1 + operando2
console.log("La suma de 3 y 2 es", sumar(3, 2))
Salida esperada:
La suma de 3 y 2 es 5
Contexto de constantes y variables
Decíamos en el capítulo dedicado a las constantes y variables que el contexto de una constante o variable está limitado al bloque de código en el que se definió, y que constantes/variables definidas en contextos padre son visibles en contextos hijo, pero no al revés.
Las mismas reglas aplican a las constantes/variables definidas en las funciones: su ámbito de existencia está limitado al contexto de la función, pero una función puede acceder a constantes/variables definidas en los contextos que la contienen:
const saludoDefecto = "Hola"
// La función saludar() define un valor por defecto
// para el primer argumento
const saludar = (saludo, destinatario) => {
saludoFinal = saludo || saludoDefecto
console.log("¡" + saludoFinal + ", " + destinatario + "!")
}
saludar(undefined, "mundo")
// saludoFinal se define en el contexto de la función
// de modo que no existe en el contexto actual
// La siguiente instrucción genera un error "ReferenceError"
// console.log("¡" + saludoFinal + ", mundo!")
Quizás lo más interesante del ejemplo anterior, que simplemente muestra el uso de las mismas reglas de ámbito de constantes/variables que ya conocíamos, sea la expresión en la primera línea de la función: saludoFinal = saludo || saludoDefecto.
Cuando estudiamos el operador booleano || explicamos que, si el primer operando se evaluaba como false se devolvía el segundo operando. Puesto que saludo es undefined, que se evalúa como false en un ámbito booleano, la expresión devuelve el segundo operando saludoDefecto.
Este tipo de expresión es muy habitual en Node.js y, de un modo hablado, equivaldría a decir "asigna el primer valor, y si es false o cadena vacía o cero o null o undefined (en definitiva un valor cuya conversión a booleano equivalga a false), asigna el segundo". Es una expresión compacta que nos evita un if o un ternario innecesario (saludoFinal = saludo ? saludo : saludoDefecto).
Lo que hemos conseguido
En este capítulo has aprendido a definir funciones en Node.js usando dos sintaxis, ambas válidas: la sintaxis tradicional y la sintaxis de función flecha, más compacta que la tradicional.
Además has aprendido a definir parámetros y a acceder a ellos, teniendo en cuenta que el número de parámetros definidos y pasados a la función pueden no coincidir: puedes pasar menos de los definidos y los restantes tomarán el valor undefined, o puedes pasar más de los definidos y los restantes serán ignorados. También has aprendido que es posible aplicar valores por defecto a los parámetros, que serán aplicados cuando el parámetro sea undefined (porque explícitamente se definió así o porque se omitió al invocar a la función).
Finalmente has aprendido cómo devolver valores desde una función, algo que todas las funciones de Node.js hacen, devolviendo undefined si no se invoca al comando return o un valor determinado si se invoca return. Como excepción, las funciones flecha de un único comando devuelven el valor de dicho comando sin necesidad de invocar return.
Qué viene a continuación
El siguiente capítulo, Introducción a los objetos, te enseñará a utilizar objetos en Node.js, algo realmente necesario si tienes en cuenta que todo en Node.js (valores básicos, funciones, arrays...) con contadas excepciones son objetos.