Funciones
La gente piensa que la informática es el arte de los genios, pero la realidad actual es la opuesta, simplemente muchas personas haciendo cosas que se construyen unas sobre otras, como un muro de mini piedras.
Las funciones son una de las herramientas más centrales en la programación en JavaScript. El concepto de envolver un fragmento de programa en un valor tiene muchos usos. Nos proporciona una manera de estructurar programas más grandes, de reducir la repetición, de asociar nombres con subprogramas y de aislar estos subprogramas entre sí.
La aplicación más evidente de las funciones es definir nuevo vocabulario. Crear palabras nuevas en el lenguaje escrito suele ser de mal gusto, pero en programación es indispensable.
Los hablantes de inglés adultos típicos tienen alrededor de 20,000 palabras en su vocabulario. Pocos lenguajes de programación vienen con 20,000 comandos incorporados. Y el vocabulario que está disponible tiende a estar más precisamente definido, y por lo tanto menos flexible, que en el lenguaje humano. Por lo tanto, tenemos que introducir nuevas palabras para evitar la verbosidad excesiva.
Definir una función
Una definición de función es un enlace habitual donde el valor del enlace es una función. Por ejemplo, este código define square
para que se refiera a una función que produce el cuadrado de un número dado:
const square = function(x) { return x * x; }; console.log(square(12)); // → 144
Una función se crea con una expresión que comienza con la palabra clave function
. Las funciones tienen un conjunto de parámetros (en este caso, solo x
) y un cuerpo, que contiene las declaraciones que se ejecutarán cuando se llame a la función. El cuerpo de una función creada de esta manera siempre debe estar envuelto entre llaves, incluso cuando consiste en una única declaración.
Una función puede tener varios parámetros o ninguno en absoluto. En el siguiente ejemplo, makeNoise
no enumera nombres de parámetros, mientras que roundTo
(que redondea n
al múltiplo más cercano de step
) enumera dos:
const makeNoise = function() { console.log("¡Pling!"); }; makeNoise(); // → ¡Pling! const roundTo = function(n, step) { let resto = n % step; return n - resto + (resto < step / 2 ? 0 : step); }; console.log(roundTo(23, 10)); // → 20
Algunas funciones, como roundTo
y square
, producen un valor, y otras no, como makeNoise
, cuyo único resultado es un efecto secundario. Una instrucción return
determina el valor que devuelve la función. Cuando el control llega a una instrucción de ese tipo, salta inmediatamente fuera de la función actual y le da el valor devuelto al código que llamó a la función. Una palabra clave return
sin una expresión después de ella hará que la función devuelva undefined
. Las funciones que no tienen ninguna instrucción return
en absoluto, como makeNoise
, devuelven igualmente undefined
.
Los parámetros de una función se comportan como enlaces habituales, pero sus valores iniciales son dados por el llamador de la función, no por el código en la función en sí misma.
Enlaces y ámbitos
Cada enlace tiene un ámbito, que es la parte del programa en la que el enlace es visible. Para los enlaces definidos fuera de cualquier función, bloque o módulo (ver Capítulo 10), el ámbito es todo el programa—puedes hacer referencia a esos enlaces donde quieras. Estos se llaman globales.
Los enlaces creados para los parámetros de una función o declarados dentro de una función solo pueden ser referenciados en esa función, por lo que se conocen como enlaces locales. Cada vez que se llama a la función, se crean nuevas instancias de estos enlaces. Esto proporciona cierto aislamiento entre funciones—cada llamada a función actúa en su propio pequeño mundo (su entorno local) y a menudo se puede entender sin saber mucho sobre lo que está sucediendo en el entorno global.
Los enlaces declarados con let
y const
en realidad son locales al bloque en el que se declaran, por lo que si creas uno de ellos dentro de un bucle, el código antes y después del bucle no puede “verlo”. En JavaScript anterior a 2015, solo las funciones creaban nuevos ámbitos, por lo que los enlaces de estilo antiguo, creados con la palabra clave var
, son visibles en toda función en la que aparecen—o en todo el ámbito global, si no están dentro de una función.
let x = 10; // global if (true) { let y = 20; // local al bloque var z = 30; // también global }
Cada ámbito puede “mirar hacia afuera” al ámbito que lo rodea, por lo que x
es visible dentro del bloque en el ejemplo. La excepción es cuando múltiples enlaces tienen el mismo nombre—en ese caso, el código solo puede ver el más interno. Por ejemplo, cuando el código dentro de la función halve
hace referencia a n
, está viendo su propio n
, no el n
global.
const halve = function(n) { return n / 2; }; let n = 10; console.log(halve(100)); // → 50 console.log(n); // → 10
Ámbito anidado
JavaScript distingue no solo entre enlaces globales y locales. Bloques y funciones pueden ser creados dentro de otros bloques y funciones, produciendo múltiples grados de localidad.
Por ejemplo, esta función—que muestra los ingredientes necesarios para hacer un lote de hummus—tiene otra función dentro de ella:
const hummus = function(factor) { const ingredient = function(amount, unit, name) { let ingredientAmount = amount * factor; if (ingredientAmount > 1) { unit += "s"; } console.log(`${ingredientAmount} ${unit} ${name}`); }; ingredient(1, "lata", "garbanzos"); ingredient(0.25, "taza", "tahini"); ingredient(0.25, "taza", "jugo de limón"); ingredient(1, "diente", "ajo"); ingredient(2, "cucharada", "aceite de oliva"); ingredient(0.5, "cucharadita", "comino"); };
El código dentro de la función ingredient
puede ver el enlace factor
de la función exterior, pero sus enlaces locales, como unit
o ingredientAmount
, no son visibles en la función exterior.
El conjunto de enlaces visibles dentro de un bloque está determinado por el lugar de ese bloque en el texto del programa. Cada bloque local también puede ver todos los bloques locales que lo contienen, y todos los bloques pueden ver el bloque global. Este enfoque de visibilidad de enlaces se llama ámbito léxico.
Funciones como valores
Generalmente un enlace de función simplemente actúa como un nombre para una parte específica del programa. Este enlace se define una vez y nunca se cambia. Esto hace que sea fácil confundir la función y su nombre.
Pero los dos son diferentes. Un valor de función puede hacer todas las cosas que pueden hacer otros valores: se puede utilizar en expresiones arbitrarias, no solo llamarlo. Es posible almacenar un valor de función en un nuevo enlace, pasarlo como argumento a una función, etc. De manera similar, un enlace que contiene una función sigue siendo solo un enlace habitual y, si no es constante, se le puede asignar un nuevo valor, así:
let launchMissiles = function() { missileSystem.launch("now"); }; if (safeMode) { launchMissiles = function() {/* no hacer nada */}; }
En el Capítulo 5, discutiremos las cosas interesantes que podemos hacer al pasar valores de función a otras funciones.
Notación de declaración
Hay una manera ligeramente más corta de crear un enlace de función. Cuando se utiliza la palabra clave function
al inicio de una declaración, funciona de manera diferente:
function square(x) { return x * x; }
Esta es una función declarativa. La declaración define el enlace square
y lo apunta a la función dada. Es un poco más fácil de escribir y no requiere un punto y coma después de la función.
Hay una sutileza con esta forma de definición de función.
console.log("El futuro dice:", future()); function future() { return "Nunca tendrás autos voladores"; }
El código anterior funciona, incluso aunque la función esté definida debajo del código que la usa. Las declaraciones de función no forman parte del flujo de control regular de arriba hacia abajo. Conceptualmente se mueven al principio de su alcance y pueden ser utilizadas por todo el código en ese alcance. A veces esto es útil porque ofrece la libertad de ordenar el código de una manera que parezca más clara, sin tener que preocuparse por definir todas las funciones antes de que se utilicen.
Funciones de flecha
Hay una tercera notación para funciones, que se ve muy diferente de las otras. En lugar de la palabra clave function
, utiliza una flecha (=>
) compuesta por un signo igual y un caracter mayor que (no confundir con el operador mayor o igual, que se escribe >=
):
const roundTo = (n, step) => { let remainder = n % step; return n - remainder + (remainder < step / 2 ? 0 : step); };
La flecha viene después de la lista de parámetros y es seguida por el cuerpo de la función. Expresa algo así como “esta entrada (los parámetros) produce este resultado (el cuerpo)”.
Cuando solo hay un nombre de parámetro, puedes omitir los paréntesis alrededor de la lista de parámetros. Si el cuerpo es una sola expresión, en lugar de un bloque entre llaves, esa expresión será devuelta por la función. Por lo tanto, estas dos definiciones de exponente
hacen lo mismo:
const exponente1 = (x) => { return x * x; }; const exponente2 = x => x * x;
Cuando una función de flecha no tiene parámetros en absoluto, su lista de parámetros es simplemente un conjunto vacío de paréntesis.
const cuerno = () => { console.log("Toot"); };
No hay una razón profunda para tener tanto funciones de flecha como expresiones function
en el lenguaje. Aparte de un detalle menor, que discutiremos en el Capítulo 6, hacen lo mismo. Las funciones de flecha se agregaron en 2015, principalmente para hacer posible escribir expresiones de función pequeñas de una manera menos verbosa. Las usaremos a menudo en el Capítulo 5 .
La pila de llamadas
La forma en que el control fluye a través de las funciones es un tanto complicada. Echemos un vistazo más de cerca. Aquí hay un programa simple que realiza algunas llamadas de función:
function saludar(quien) { console.log("Hola " + quien); } saludar("Harry"); console.log("Adiós");
Una ejecución de este programa va más o menos así: la llamada a saludar
hace que el control salte al inicio de esa función (línea 2). La función llama a console.log
, que toma el control, hace su trabajo, y luego devuelve el control a la línea 2. Allí, llega al final de la función saludar
, por lo que regresa al lugar que la llamó, línea 4. La línea siguiente llama a console.log
nuevamente. Después de ese retorno, el programa llega a su fin.
Podríamos mostrar el flujo de control esquemáticamente de esta manera:
no en función en saludar en console.log en saludar no en función en console.log no en función
Dado que una función tiene que regresar al lugar que la llamó cuando termina, la computadora debe recordar el contexto desde el cual se realizó la llamada. En un caso, console.log
tiene que regresar a la función saludar
cuando haya terminado. En el otro caso, regresa al final del programa.
El lugar donde la computadora almacena este contexto es la pila de llamadas. Cada vez que se llama a una función, el contexto actual se almacena en la parte superior de esta pila. Cuando una función devuelve, elimina el contexto superior de la pila y usa ese contexto para continuar la ejecución.
Almacenar esta pila requiere espacio en la memoria de la computadora. Cuando la pila crece demasiado, la computadora fallará con un mensaje como “sin espacio en la pila” o “demasiada recursividad”. El siguiente código ilustra esto al hacerle a la computadora una pregunta realmente difícil que causa un vaivén infinito entre dos funciones. O más bien, sería infinito, si la computadora tuviera una pila infinita. Como no la tiene, nos quedaremos sin espacio o “reventaremos la pila”.
function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " salió primero."); // → ??
Argumentos Opcionales
El siguiente código está permitido y se ejecuta sin ningún problema:
function square(x) { return x * x; } console.log(square(4, true, "erizo")); // → 16
Hemos definido square
con solo un parámetro. Sin embargo, cuando lo llamamos con tres, el lenguaje no se queja. Ignora los argumentos adicionales y calcula el cuadrado del primero.
JavaScript es extremadamente flexible en cuanto al número de argumentos que puedes pasar a una función. Si pasas demasiados, los extras son ignorados. Si pasas muy pocos, los parámetros faltantes se les asigna el valor undefined
.
El inconveniente de esto es que es posible —incluso probable— que pases accidentalmente el número incorrecto de argumentos a las funciones. Y nadie te dirá nada al respecto. La ventaja es que puedes utilizar este comportamiento para permitir que una función sea llamada con diferentes números de argumentos. Por ejemplo, esta función minus
intenta imitar al operador -
actuando sobre uno o dos argumentos:
function minus(a, b) { if (b === undefined) return -a; else return a - b; } console.log(minus(10)); // → -10 console.log(minus(10, 5)); // → 5
Si escribes un operador =
después de un parámetro, seguido de una expresión, el valor de esa expresión reemplazará al argumento cuando no se le dé. Por ejemplo, esta versión de roundTo
hace que su segundo argumento sea opcional. Si no lo proporcionas o pasas el valor undefined
, por defecto será uno:
function roundTo(n, step = 1) { let remainder = n % step; return n - remainder + (remainder < step / 2 ? 0 : step); }; console.log(roundTo(4.5)); // → 5 console.log(roundTo(4.5, 2)); // → 4
El próximo capítulo introducirá una forma en que un cuerpo de función puede acceder a la lista completa de argumentos que se le pasaron. Esto es útil porque le permite a una función aceptar cualquier número de argumentos. Por ejemplo, console.log
lo hace, mostrando todos los valores que se le dan:
console.log("C", "O", 2); // → C O 2
Clausura
La capacidad de tratar las funciones como valores, combinada con el hecho de que los enlaces locales se recrean cada vez que se llama a una función, plantea una pregunta interesante: ¿qué sucede con los enlaces locales cuando la llamada a la función que los creó ya no está activa?El siguiente código muestra un ejemplo de esto. Define una función, wrapValue
, que crea un enlace local. Luego devuelve una función que accede a este enlace local y lo devuelve:
function wrapValue(n) { let local = n; return () => local; } let wrap1 = wrapValue(1); let wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2
Esto está permitido y funciona como esperarías: ambas instancias del enlace aún pueden accederse. Esta situación es una buena demostración de que los enlaces locales se crean nuevamente para cada llamada, y las diferentes llamadas no afectan los enlaces locales de los demás.
Esta característica, poder hacer referencia a una instancia específica de un enlace local en un ámbito superior, se llama clausura. Una función que hace referencia a enlaces de ámbitos locales a su alrededor se llama una clausura. Este comportamiento no solo te libera de tener que preocuparte por la vida útil de los enlaces, sino que también hace posible usar valores de función de formas creativas.
Con un ligero cambio, podemos convertir el ejemplo anterior en una forma de crear funciones que multiplican por una cantidad arbitraria:
function multiplier(factor) { return number => number * factor; } let twice = multiplier(2); console.log(twice(5)); // → 10
El enlace explícito local
del ejemplo wrapValue
realmente no es necesario, ya que un parámetro es en sí mismo un enlace local.
Pensar en programas de esta manera requiere algo de práctica. Un buen modelo mental es pensar en los valores de función como que contienen tanto el código en su cuerpo como el entorno en el que fueron creados. Cuando se llama, el cuerpo de la función ve el entorno en el que fue creado, no el entorno en el que se llama.
En el ejemplo anterior, se llama a multiplier
y crea un entorno en el que su parámetro factor
está vinculado a 2. El valor de función que devuelve, que se almacena en twice
, recuerda este entorno para que cuando se llame, multiplique su argumento por 2.
Recursión
Es perfectamente válido que una función se llame a sí misma, siempre y cuando no lo haga tan a menudo que desborde la pila. Una función que se llama a sí misma se llama recursiva. La recursión permite que algunas funciones se escriban de una manera diferente. Toma, por ejemplo, esta función power
, que hace lo mismo que el operador **
(potenciación):
function power(base, exponent) { if (exponent == 0) { return 1; } else { return base * power(base, exponent - 1); } } console.log(power(2, 3)); // → 8
Esto se asemeja bastante a la forma en que los matemáticos definen la potenciación y describe el concepto de manera más clara que el bucle que usamos en el Capítulo 2. La función se llama a sí misma varias veces con exponentes cada vez más pequeños para lograr la multiplicación repetida.
Sin embargo, esta implementación tiene un problema: en implementaciones típicas de JavaScript, es aproximadamente tres veces más lenta que una versión que utiliza un bucle for
. Recorrer un simple bucle suele ser más económico que llamar a una función múltiples veces.
El dilema de velocidad versus elegancia es interesante. Se puede ver como una especie de continuo entre la compatibilidad con los humanos y las máquinas. Casi cualquier programa puede ser acelerado haciendo que sea más extenso y complicado. El programador debe encontrar un equilibrio apropiado.
En el caso de la función power
, una versión poco elegante (con bucles) sigue siendo bastante simple y fácil de leer. No tiene mucho sentido reemplazarla con una función recursiva. Sin embargo, a menudo un programa trata con conceptos tan complejos que es útil renunciar a algo de eficiencia para hacer que el programa sea más sencillo.
Preocuparse por la eficiencia puede ser una distracción. Es otro factor que complica el diseño del programa y cuando estás haciendo algo que ya es difícil, ese extra en lo que preocuparse puede llegar a ser paralizante.
Por lo tanto, generalmente deberías comenzar escribiendo algo que sea correcto y fácil de entender. Si te preocupa que sea demasiado lento—lo cual suele ser raro, ya que la mayoría del código simplemente no se ejecuta lo suficiente como para tomar una cantidad significativa de tiempo—puedes medir después y mejorarlo si es necesario.
La recursión no siempre es simplemente una alternativa ineficiente a los bucles. Algunos problemas realmente son más fáciles de resolver con recursión que con bucles. Con mayor frecuencia, estos son problemas que requieren explorar o procesar varias “ramas”, cada una de las cuales podría ramificarse nuevamente en aún más ramas.
Considera este rompecabezas: al comenzar desde el número 1 y repetidamente sumar 5 o multiplicar por 3, se puede producir un conjunto infinito de números. ¿Cómo escribirías una función que, dado un número, intente encontrar una secuencia de tales sumas y multiplicaciones que produzcan ese número? Por ejemplo, el número 13 podría alcanzarse al multiplicar por 3 y luego sumar 5 dos veces, mientras que el número 15 no podría alcanzarse en absoluto.
Aquí tienes una solución recursiva:
function encontrarSolucion(objetivo) { function encontrar(actual, historial) { if (actual === objetivo) { return historial; } else if (actual > objetivo) { return null; } else { return encontrar(actual + 5, `(${historial} + 5)`) ?? encontrar(actual * 3, `(${historial} * 3)`); } } return encontrar(1, "1"); } console.log(encontrarSolucion(24)); // → (((1 * 3) + 5) * 3)
Ten en cuenta que este programa no necesariamente encuentra la secuencia de operaciones más corta. Se conforma con encontrar cualquier secuencia.
No te preocupes si no ves cómo funciona este código de inmediato. Vamos a trabajar juntos, ya que es un gran ejercicio de pensamiento recursivo. La función interna encontrar
es la que realiza la recursión real. Toma dos argumentos: el número actual y una cadena que registra cómo llegamos a este número. Si encuentra una solución, devuelve una cadena que muestra cómo llegar al objetivo. Si no puede encontrar una solución comenzando desde este número, devuelve null
.
Para hacer esto, la función realiza una de tres acciones. Si el número actual es el número objetivo, el historial actual es una forma de alcanzar ese objetivo, por lo que se devuelve. Si el número actual es mayor que el objetivo, no tiene sentido explorar más esta rama porque tanto la suma como la multiplicación solo harán que el número sea más grande, por lo que devuelve null
. Finalmente, si aún estamos por debajo del número objetivo, la función prueba ambas rutas posibles que parten del número actual llamándose a sí misma dos veces, una vez para la suma y otra vez para la multiplicación. Si la primera llamada devuelve algo que no es null
, se devuelve. De lo contrario, se devuelve la segunda llamada, independientemente de si produce una cadena o null
.
Para entender mejor cómo esta función produce el efecto que estamos buscando, veamos todas las llamadas a encontrar
que se hacen al buscar una solución para el número 13:
encontrar(1, "1") encontrar(6, "(1 + 5)") encontrar(11, "((1 + 5) + 5)") encontrar(16, "(((1 + 5) + 5) + 5)") demasiado grande encontrar(33, "(((1 + 5) + 5) * 3)") demasiado grande encontrar(18, "((1 + 5) * 3)") demasiado grande encontrar(3, "(1 * 3)") encontrar(8, "((1 * 3) + 5)") encontrar(13, "(((1 * 3) + 5) + 5)") ¡encontrado!
La sangría indica la profundidad de la pila de llamadas. La primera vez que se llama a encontrar
, la función comienza llamándose a sí misma para explorar la solución que comienza con (1 + 5)
. Esa llamada seguirá recursivamente para explorar cada solución a continuación que produzca un número menor o igual al número objetivo. Como no encuentra uno que alcance el objetivo, devuelve null
a la primera llamada. Allí, el operador ??
hace que ocurra la llamada que explora (1 * 3)
. Esta búsqueda tiene más suerte: su primera llamada recursiva, a través de otra llamada recursiva, alcanza el número objetivo. Esa llamada más interna devuelve una cadena, y cada uno de los operadores ??
en las llamadas intermedias pasa esa cadena, devolviendo en última instancia la solución.
Crecimiento de funciones
Hay dos formas más o menos naturales de introducir funciones en los programas.
La primera ocurre cuando te encuentras escribiendo código similar varias veces. Preferirías no hacer eso, ya que tener más código significa más espacio para que se escondan los errores y más material para que las personas que intentan entender el programa lo lean. Por lo tanto, tomas la funcionalidad repetida, encuentras un buen nombre para ella y la colocas en una función.
La segunda forma es que te das cuenta de que necesitas alguna funcionalidad que aún no has escrito y que suena como si mereciera su propia función. Comienzas por nombrar la función, luego escribes su cuerpo. Incluso podrías comenzar a escribir código que use la función antes de definir la función en sí.
Lo difícil que es encontrar un buen nombre para una función es una buena indicación de lo claro que es el concepto que estás tratando de envolver con ella. Vamos a través de un ejemplo.
Queremos escribir un programa que imprima dos números: el número de vacas y de pollos en una granja, con las palabras Vacas
y Pollos
después de ellos y ceros rellenados antes de ambos números para que siempre tengan tres dígitos:
007 Vacas 011 Pollos
Esto pide una función con dos argumentos: el número de vacas y el número de pollos. ¡Vamos a programar!
function imprimirInventarioGranja(vacas, pollos) { let cadenaVaca = String(vacas); while (cadenaVaca.length < 3) { cadenaVaca = "0" + cadenaVaca; } console.log(`${cadenaVaca} Vacas`); let cadenaPollo = String(pollos); while (cadenaPollo.length < 3) { cadenaPollo = "0" + cadenaPollo; } console.log(`${cadenaPollo} Pollos`); } imprimirInventarioGranja(7, 11);
Escribir .length
después de una expresión de cadena nos dará la longitud de esa cadena. Por lo tanto, los bucles while
siguen añadiendo ceros delante de las cadenas de números hasta que tengan al menos tres caracteres de longitud.
¡Misión cumplida! Pero justo cuando estamos a punto de enviarle a la granjera el código (junto con una jugosa factura), ella llama y nos dice que también ha comenzado a criar cerdos, ¿podríamos extender el software para imprimir también los cerdos?
¡Claro que podemos! Pero justo cuando estamos en el proceso de copiar y pegar esas cuatro líneas una vez más, nos detenemos y reconsideramos. Tiene que haber una mejor manera. Aquí está un primer intento:
function imprimirConRellenoYEtiqueta(numero, etiqueta) { let cadenaNumero = String(numero); while (cadenaNumero.length < 3) { cadenaNumero = "0" + cadenaNumero; } console.log(`${cadenaNumero} ${etiqueta}`); } function imprimirInventarioGranja(vacas, pollos, cerdos) { imprimirConRellenoYEtiqueta(vacas, "Vacas"); imprimirConRellenoYEtiqueta(pollos, "Pollos"); imprimirConRellenoYEtiqueta(cerdos, "Cerdos"); } imprimirInventarioGranja(7, 11, 3);
¡Funciona! Pero ese nombre, imprimirConRellenoYEtiqueta
, es un poco incómodo. Confluye tres cosas: imprimir, rellenar con ceros y añadir una etiqueta, en una sola función.
En lugar de sacar la parte repetida de nuestro programa completamente, intentemos sacar un solo concepto:
function rellenarConCeros(numero, ancho) { let cadena = String(numero); while (cadena.length < ancho) { cadena = "0" + cadena; } return cadena; } function imprimirInventarioGranja(vacas, pollos, cerdos) { console.log(`${rellenarConCeros(vacas, 3)} Vacas`); console.log(`${rellenarConCeros(pollos, 3)} Pollos`); console.log(`${rellenarConCeros(cerdos, 3)} Cerdos`); } imprimirInventarioGranja(7, 16, 3);
Una función con un nombre claro y obvio como rellenarConCeros
hace que sea más fácil para alguien que lee el código entender qué hace. Además, una función así es útil en más situaciones que solo este programa específico. Por ejemplo, podrías usarla para ayudar a imprimir tablas de números alineadas correctamente.
¿Qué tan inteligente y versátil debería ser nuestra función? Podríamos escribir cualquier cosa, desde una función terriblemente simple que solo puede rellenar un número para que tenga tres caracteres de ancho hasta un sistema complejo de formato de números general que maneje números fraccionarios, números negativos, alineación de puntos decimales, relleno con diferentes caracteres y más.
Un principio útil es abstenerse de agregar ingenio a menos que estés absolutamente seguro de que lo vas a necesitar. Puede ser tentador escribir “frameworks” genéricos para cada trozo de funcionalidad que te encuentres. Resiste esa tentación. No lograrás hacer ningún trabajo real: estarás demasiado ocupado escribiendo código que nunca usarás.
Funciones y efectos secundarios
Las funciones pueden dividirse aproximadamente en aquellas que se llaman por sus efectos secundarios y aquellas que se llaman por su valor de retorno (aunque también es posible tener efectos secundarios y devolver un valor).
La primera función auxiliar en el ejemplo de la granja, imprimirConRellenoYEtiqueta
, se llama por su efecto secundario: imprime una línea. La segunda versión, rellenarConCeros
, se llama por su valor de retorno. No es casualidad que la segunda sea útil en más situaciones que la primera. Las funciones que crean valores son más fáciles de combinar de nuevas formas que las funciones que realizan efectos secundarios directamente.
Una función pura es un tipo específico de función productora de valor que no solo no tiene efectos secundarios, sino que tampoco depende de efectos secundarios de otro código, por ejemplo, no lee enlaces globales cuyo valor podría cambiar. Una función pura tiene la agradable propiedad de que, al llamarla con los mismos argumentos, siempre produce el mismo valor (y no hace nada más). Una llamada a tal función puede sustituirse por su valor de retorno sin cambiar el significado del código. Cuando no estás seguro de si una función pura está funcionando correctamente, puedes probarla llamándola y saber que si funciona en ese contexto, funcionará en cualquier otro. Las funciones no puras tienden a requerir más andamiaje para probarlas.
Aún así, no hay necesidad de sentirse mal al escribir funciones que no son puras. Los efectos secundarios a menudo son útiles. No hay forma de escribir una versión pura de console.log
, por ejemplo, y es bueno tener console.log
. Algunas operaciones también son más fáciles de expresar de manera eficiente cuando usamos efectos secundarios.
Resumen
Este capítulo te enseñó cómo escribir tus propias funciones. La palabra clave function
, cuando se usa como expresión, puede crear un valor de función. Cuando se usa como una declaración, puede usarse para declarar un enlace y darle una función como su valor. Las funciones de flecha son otra forma de crear funciones.
// Definir f para contener un valor de función const f = function(a) { console.log(a + 2); }; // Declarar g como una función function g(a, b) { return a * b * 3.5; } // Un valor de función menos verboso let h = a => a % 3;
Una parte clave para entender las funciones es comprender los ámbitos (scopes). Cada bloque crea un nuevo ámbito. Los parámetros y los enlaces declarados en un ámbito dado son locales y no son visibles desde el exterior. Los enlaces declarados con var
se comportan de manera diferente: terminan en el ámbito de la función más cercana o en el ámbito global.
Separar las tareas que realiza tu programa en diferentes funciones es útil. No tendrás que repetirte tanto, y las funciones pueden ayudar a organizar un programa agrupando el código en piezas que hacen cosas específicas.
Ejercicios
Mínimo
El capítulo previo presentó la función estándar Math.min
que devuelve su menor argumento. Ahora podemos escribir una función como esa nosotros mismos. Define la función min
que toma dos argumentos y devuelve su mínimo.
// Tu código aquí. console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10
Mostrar pistas...
Recursión
Hemos visto que podemos usar %
(el operador de resto) para verificar si un número es par o impar al usar % 2
para ver si es divisible por dos. Aquí hay otra forma de definir si un número entero positivo es par o impar:
Define una función recursiva isEven
que corresponda a esta descripción. La función debe aceptar un solo parámetro (un número entero positivo) y devolver un booleano.
Pruébalo con 50 y 75. Observa cómo se comporta con -1. ¿Por qué? ¿Puedes pensar en una forma de solucionarlo?
// Tu código aquí. console.log(isEven(50)); // → true console.log(isEven(75)); // → false console.log(isEven(-1)); // → ??
Mostrar pistas...
Es probable que tu función se parezca en cierta medida a la función interna encontrar
en el ejemplo recursivo encontrarSolucion
ejemplo de este capítulo, con una cadena if
/else if
/else
que prueba cuál de los tres casos aplica. El else
final, correspondiente al tercer caso, realiza la llamada recursiva. Cada una de las ramas debe contener una declaración return
o de alguna otra manera asegurarse de que se devuelva un valor específico.
Cuando se le da un número negativo, la función se llamará recursivamente una y otra vez, pasándose a sí misma un número cada vez más negativo, alejándose así más y más de devolver un resultado. Eventualmente se quedará sin espacio en la pila y se abortará.
Contando frijoles
Puedes obtener el *ésimo carácter, o letra, de una cadena escribiendo [N]
después de la cadena (por ejemplo, cadena[2]
). El valor resultante será una cadena que contiene solo un carácter (por ejemplo, "b"
). El primer carácter tiene la posición 0, lo que hace que el último se encuentre en la posición cadena.
. En otras palabras, una cadena de dos caracteres tiene longitud 2, y sus caracteres tienen posiciones 0 y 1.
Escribe una función contarBs
que tome una cadena como único argumento y devuelva un número que indique cuántos caracteres B en mayúscula hay en la cadena.
A continuación, escribe una función llamada contarCaracter
que se comporte como contarBs
, excepto que toma un segundo argumento que indica el carácter que se va a contar (en lugar de contar solo caracteres B en mayúscula). Reescribe contarBs
para hacer uso de esta nueva función.
// Your code here. console.log(countBs("BOB")); // → 2 console.log(countChar("kakkerlak", "k")); // → 4
Mostrar pistas...
Tu función necesida un bucle que mire cada carácter en la cadena. Puede ejecutar un índice desde cero hasta uno menos que su longitud (< string.
). Si el carácter en la posición actual es el mismo que el que la función está buscando, agrega 1 a una variable de contador. Una vez que el bucle ha terminado, el contador puede ser devuelto.
Ten cuidado de que todas las vinculaciones utilizadas en la función sean locales a la función, declarándolas correctamente con la palabra clave let
o const
.