Capítulo 3Funciones
La gente piensa que las ciencias de la computación son las artes de los genios pero la realidad es lo contrario, es sólo un montón de gente haciendo cosas que se construyen sobre otras, como una pared de piedras muy pequeñas.
Has visto
valores función, como alert
, y cómo llamarlos. Las funciones son el pan de
cada día en la programación en JavaScript. El concepto de de envolver
una porción del programa en un valor(variable) tiene muchos usos.
Es una herramienta para estructurar programas más grandes, para reducir
la repetición, para asociar nombres con subprogramas, y para separar
estos programas de los demás.
La aplicación más obvia de las funciones es la de definir un nuevo vocabulario. Crear nuevas palabras en la prosa regular en lenguaje humano es usualmente un mal estilo. Pero en programación, es indispensable.
Un adulto promedio tiene unas 20,000 palabras en su vocabulario. Pocos lenguajes de programación tienen 20,000 comandos incorporados. Y el vocabulario que está disponible tiende a ser definido de forma más precisa, así que es menos flexible que en un lenguaje humano. En consecuencia, usualmente tenemos que añadir algo de nuestro propio vocabulario para evitar repetirnos demasiado.
Definiendo una función
Una definición de una función
es sólo una definición regular de una variable cuando ocurre que el
valor dado a la variable es una función. Por ejemplo, el siguiente código
define la variable cuadrado
para referirse a la función que produce
el cuadrado de un número dado:
var cuadrado = function(x) { return x * x; }; console.log(cuadrado(12)); // → 144
Una función es
creada por una expresión que empieza con la palbra reservada function
.
Las funciones tienen un conjunto de parametros (en este caso
sólo x
) y un cuerpo, que contiene las sentencias que serán ejecutadas
cuando la función sea llamada. El cuerpo de la función tiene que estar
siempre encerrado en llaves, incluso cuando consista de una sola
instrucción (como en el ejemplo previo).
Una función puede tener varios parámetros
o puede no tener ninguno. En el siguiente ejemplo hazRuido
no tiene
parámetros, mientras que potencia
tiene dos:
var hazRuido = function() { console.log("Pling!"); }; hazRuido(); // → Pling! var potencia = function(base, exponente) { var resultado = 1; for (var cuenta = 0; cuenta < exponente; cuenta++) resultado *= base; return resultado; }; console.log(potencia(2, 10)); // → 1024
Algunas
funciones producen un valor, como potencia
y cuadrado
, y algunas no,
como hazRuido
, la cuál produce sólo un efecto secundario. Una sentencia
return
determina el valor que una función regresa. Cuando el control
pasa a esta sentencia, inmediatamente salta fuera de la función actual
y pasa el valor retornado a la código que llamó la función. La palabra
reservada return
sin una expresión después de ella hará que la función
devuelva undefined
.
Parámetros y ámbitos
Los parametros para una función se comportan como variables normales, pero su valor inicial esta dado por quien llama a la función, no por el código mismo de la función.
Un propiedad
importante de las funciones es que las varibales creades dentro de ellas,
incluyendo sus parámetros, son locales para la función. Este significa,
por ejemplo, que la varibale resultado
en el ejemplo potencia
será
creada de nuvo cada vez que la función es llamada, y estas instancias
separadas no interfieren entre ellas.
Esta
"localidad" de las variables aplica sólo a los parámetros y variables
declaradas con la palabra reservada var
dentro del cuerpo de la función.
Las variables declaradas duera de cualquier función son llamadas globales,
porque son visibles a través de todo el programa. Es posible acceder a
estas variables desde dentro de una función, mientras no hayas declarado una
variable local con el mismo nombre.
El siguiente código demuestra eso. Define y llama
dos funciones que asignan un valor a la variable x
. La primera declara
la variable como local y de esta manera cambia únicamente la variable que
creó. La segunda no declara x
localmente, así que la x
dentro de ella
hace referencia a la x
definida al principio del ejemplo.
var x = "fuera"; var f1 = function() { var x = "dentro de f1"; }; f1(); console.log(x); // → fuera var f2 = function() { x = "dentro de f2"; }; f2(); console.log(x); // → dentro de f2
Este comportamiento ayuda a prevenir interferencia accidental entre las funciones. Si todas las variables fueran compartidas por el programa entero, sería muy difícil asegurarse de que algún nombre no fue usado para dos propósitos diferentes. Y si reusaste un nombre de variable, podrías ver efectos extraños de código no relacionado causando problemas en el valor de tu variable. Al tratar tus variables locales a la función, el lenguaje hace posible leer y entender las funciones como un pequeños universos, sin tener que preocuparse de todo el código a la vez.
Ámbitos Anidados
JavaScript distingue nos solo entre variables globales y locales Las funciones pueden ser creadas dentro de otras funciones, produciendo distindos grados de localidad.
Por ejemplo, esta función sin sentido, tiene dos funciones dentro de ella:
var landscape = function() { var resultado = ""; var meseta = function(tamano) { for (var cuenta = 0; cuenta < size; cuenta++) resultado += "_"; }; var montana = function(tamano) { result += "/"; for (var cuenta = 0; cuenta < size; cuenta++) resultado += "'"; resultado += "\\"; }; meseta(3); montana(4); meseta(6); montana(1); meseta(1); return resultado; }; console.log(landscape()); // → ___/''''\______/'\_
Las funciones meseta
y montana
pueden "ver" la variable llamada resultado
, debido a que están
dentro de la función que la define. Pero no pueden ver la variable
cuenta
entre ellas, porque están definidas fuera del ámbito de la otra.
El entorno fuera de la función paisaje
no puede acceder a ninguna de
las variables definidas dentro de paisaje
.
En pocas palabras, cada ámbito local también puede ver los ámbitos locales que lo contienen. El conjunto de variables visible dentro de la función es determinado por el lugar de la función en el texto del programa. Todas las variables de los bloques que envuelven una la definición de una función son visibles para ella, esto es, en todos los cuerpos de las funciones que la envuelven y las correspondientes al nivel superior(ámbito global). Este manera de resolver la visibilidad de las variables es llamada definición léxica de ámbito(lexical scoping).
Las personas que tienen experiencia con otros lenguajes de programción podrían esperar que cualquier bloque entre llaves produzca un nuevo entorno local. Pero en JavaScript, las funciones son lo único que crea un nuevo ámbito. Pero puedes usar bloques independientes.
var algo = 1; { var algo = 2; // Haz algo con la variable... } // Fuera del bloque...
Pero la variable algo
dentro del bloque se refiere a la misma variable
que la que está fuera del bloque. De hecho, aunque bloques como este
son permitidos, sólo son útiles para agrupar el cuerpo de las sentencias
if
o de los bucles.
Si esto te parece raro, no eres
el único. La próxima versión de JavaScript introducirá una palabra
reservada let
, que trabaja como var
pero crea una variable que es
local para el bloque en el que está, no para la función.
Funciones como valores
Las variables de función normalmente actúan como nombres para una parte específica del programa. Esa variables es definida una vez y nunca cambiada. Esto hace fácil empezar a confundir la función con su nombre.
Pero son diferentes entre sí. Un valor función puede hacer todo los que los otros valores pueden hacer; lo puedes usar en expresiones arbitrarias, no sólo llamarlos. Es posible guardar la función en un nuevo lugar, pasar como argumento a otra función, etc. De manera parecida, la variable que contiene una función sigue siendo una variable normal a la que se le puede asignar otro valor, como sigue:
var lanzarMisiles = function(valor) { sistemaDeMisiles.lanzar("ahora"); }; if (modoSeguro) lanzarMisiles = function(valor) {/* No hacer nada. */};
En Capítulo 5, hablaremos de las maravillosas cosas que puedes hacer al pasar valores función a otras funciones.
Notación de Declaración
Existe una
forma más corta de decir “var square = function…
”. La palabra reservada
function
puede ser usada al principio de una sentencia, como sigue:
function cuadrado(x) { return x * x; }
Esta es una declaración de función.
La expresión define la variable cuadrado
y la apunta a la función
dada. Hasta aquí todo bien. sin embargo, hay una pequeña sutileza con
esta forma de definición.
console.log("El futuro dice: ", futuro()); function future() { return "Seguimos sin tener carros voladores."; }
Este código funciona aunque la función está definida debajo del código que la usa. Esto es debido a que las declaraciones de función no toman parte en el flujo de control regular de arriba hacia abajo. Son movidas conceptualmente a la parte superior de su ámbito y pueden ser usadas por todo el código en ese ámbito. Esto es útil algunas veces porque nos da la libertad de organizar el código de una manera parezca significativa sin preocuparnos por definir todas las funciones antes de su primer uso.
¿Qué pasa cuando pones una declaración
de función dentro de un bloque condicional (if
) o dentro de un bucle?
Bueno, mejor no lo hagas. Diferentes plataformas de JavaScript en
diferentes navegadores hacen diferentes cosas tradicionalmente en esa
situación, y el último estándar de hecho lo prohibe. Si quieres que
tus programas sean consistentes, usa las sentencia de declaración de función
en el bloque más externo de tu función o programa.
function ejemplo() { function a() {} // Bien if (alguna_condicion) { function b() {} // ¡Peligro! } }
La pila de llamadas
Será útil mirar más de cerca la forma en que el control se mueve a través de las funciones. Aquí hay un simple programa que hace unas cuantas llamadas a funciones.
function saluda(a_quien) { console.log("Hola " + a_quien); } saluda("Harry"); console.log("Adiós");
(((console.log))Una ejecución
de este programa va más o menos así: la llamada a saluda
causa que el control
pase al inicio de esa función (línea 2). Esta llama a control.log
(una función
incluída en los navegadores), que toma el control, hace su trabajo, y devuelve
el control a la línea 2. Después, se alcanza el dinal de la función saluda
, así
que se regresa al lugar en dónde se llamó, en la línea 4. La línea siguiente llama
a console.log
otra vez.
Podemos mostrar el flujo de control esquemáticamente así:
raíz saluda console.log saluda raíz console.log raíz
Debido a que una función tiene que
saltar de regreso al lugar en que fue llamada cuando termine, la computadora
debe recordar el contexto en el que fue llamada. En un caso, console.log
tiene
que regresar a la función saluda
. En el otro caso salta al final del programa.
El lugar en el que la computadora guarda este context es la pila de llamadas. Cada vez que una función es llamada, el contexto actual es puesto en la parte superior de esta pila. Cuando la función retorne, remueve el contexto superior de la "pila" y lo usa para continuar la ejecución.
Guardar esta pila requiere espacio en la memoria de la coputadora. Cuando la pila se hace demasiado grande la computadora mostrará un error parecido a "out of stack space" (sin espacio en la en la pila) o "too much recursion" (demasiada recursión). El siguiente código lo ilustra al preguntarle algo realmente difícil a la computadora, lo que causa un ir y venir infinito entre funciones. Más bien, sería infinito, si la computadora tuviera una pila infinita. Como son las cosas, nos quedaremos sin espacio, o "volaremos la pila".
function gallina() { return huevo(); } function huevo() { return gallina(); } console.log(gallina() + " fue primero"); // → ??
Argumentos Opcionales
El siguiente código es permitido y se ejecuta sin ningún problema:
alert("Hola", "Buenas Noches", "¿Cómo estás?");
La función alert
oficialmente acepta sólo un argumento.
Aún así, cuando la llamas como aquí, no se queja. Simplemente ignora los otros
argumentos y te muestra "Hola".
JavaScript es extremadamente abierta de mente
acerca del número de argumentos que le pasas a una función. Sí le pasas demasiados,
loa argumentos extra son ignorados. Si le pasas muy pocos, los parámetros quie faltan
simplemente son asignados a undefined
.
El lado malo de esto es que es posible, probable a menudo, que le pasarás accidentalmente un número incorrecto de argumentos a las funciones y nadie te avisará.
El lado bueno de este
comportamiento es que puede ser usado para tener una función que
tome parámetros "opcionales". Por ejemplo, la siguiente versión de
potencia
puede ser llamada con dos argumentos o uno solo, caso en el que
el exponente se asume como dos, y la función se comporta como cuadrado
.
function potencia(base, exponente) { if (exponente == undefined) exponente = 2; var resultado = 1; for (var cuenta = 0; cuenta < exponente; cuenta++) resultado *= base; return resultado; } console.log(potencia(4)); // → 16 console.log(potencia(4, 3)); // → 64
En el próximo
capítulo, veremos una forma en la que el cuerpo de una función puede
obetener la lista exacta de argumentos que se le pasaron. Esto es útil
porque permite a una función aceptar un número indeterminado de argumentos.
Por ejemplo, console.log
hace uso de esto: imprime todos los valores que
se le pasaron.
console.log("R", 2, "D", 2); // → R 2 D 2
Closure
La habilidad de tratar Funciones como valores, combinada con el hecho de que las varibles locales son "re-creadas" cada vez que una función es llamada, saca a la luz una pregunta interesante. ¿Qué pasa con las variables locales cuando la función que las creó ya no está activa?
El siguiente código muestra un ejemplo de esto. Define una funnción,
envuelveValor
, que crea una variable local. Después devuelve una
función que accede y devuelve esta variable local.
function envuelveValor(n) { var variableLocal = n; return function() { return variableLocal; }; } var envoltura1 = envuelveValor(1); var envoltura2 = envuelveValor(2); console.log(envoltura11()); // → 1 console.log(envoltura2()); // → 2
Esto está permitido y funciona como esperarías; la variable todavía puede leerse. De hecho, múltiples instancias de las variables pueden existir al mismo tiempo, lo que es otra buena ilustración del concepto de que las variables locales son re-creadas realmente para cada llamada; diferentes llamadas no pueden afectar otras variables locales.
Esta característica -ser capaces de hacer referencia a una instancia local de varables en un función que las encierra- se llama closure. Una función que "encapsula" algunas variables locales es llamada una closure. Este comportamiento no sólo te libera de preocuparte de los tiempos de vida de las variables, además permite algunos usos creativos de las funciones.
Con un pequeño cambio, podemos podemos hacer del ejemplo anterior funciones que multiplicquen por un número arbitrario.
function multiplicador(factor) { return function(numero) { return numero * factor; }; } var doble = multiplicador(2); console.log(doble(5)); // → 10
La variable explícita variableLocal
de la
función envuelveValor
en el ejemplo previo no es necesaria porque un
parámetro en sí mismo es una variable local.
Concebir los parámetros de esta forma
requiere algo de práctica. Un buen modelo mental es pensar en la
palabra clave function
como si "congelara" el código que está dentro de
ella en un paquete(el valor función). Así, cuando leas return function(...){...}
,
piensa en que esto regresa un acceso a un conjunto de cálculos, congelados
para uso posterior.
En el ejemplo, multiplicador
regresa un pedazo congelado de código que
se guarda en la variable doble
. La última línea entonces llama el valor
guardado en esta variable, haciendo que el código congelado (return numero * factor;
)
se active. Este todavía tiene acceso a la variable factor
de la llamada a
multiplicador
que lo creó, y además obtiene acceso al argumento que se pasa
cuando se activa el código, 5, a través de su parámetro numero
.
Recursión
Es perfectamente correcto que
una función se llame a sí misma, mientras tenga cuidado de no desbordar la pila.
Una función que se llama a sí misma se llama recursiva. La recursión permite
que algunas funciones se escriban con un estilo diferente. Tomemos por ejemplo,
esta implementación alternativa de potencia
:
function potencia(base, exponente) { if (exponente == 0) return 1; else return base * potencia(base, exponente - 1); } console.log(potencia(2, 3)); // → 8
Esto es más cercano al modo en que los matemáticos definen la potenciación y describe el concepto de un modo más elegante que la variante que lo hace con bucles. La función se llama a sí misma varias veces con diferentes argumentos para conseguir la multiplicación repetida.
Pero esta implementación tiene un problema importante: en implementaciones típicas de JavaScript, es cerca de 10 veces más lenta que la versión con bucles. Correr a través de un siple bucle es más barato que llamar a una función muchas veces.
El dilema de velocidad contra elegancia es interesante. Puedes verlo como un continuum entre amigabilidad-humano y amigabilidad-máquina. Casi cualquier programa puede hacerse más rápido haciéndolo más grande y convolucionado. El programador debe de decidir el balance apropiado.
En el caso de la función anterior potencia
la poco elegante versión(iterativa) es aún bastante simple y fácil de leer.
No tiene mucho sentido reemplazarla con la versión recursiva. A menudo,
sin embargo, un programa tiene conceptos tan complejos que sacrificar
un poco de eficiencia para hacer el programa más claro se vuelve una
opción atractiva.
La regla básica, que ha sido repetida por muchos programadores y con la cuál concuerdo de todo corazón, es no preocuparse por la eficiencia hasta que estés seguro que el programa es demasiado lento. Si lo es, busca las partes que están abarcando la mayoría del tiempo y empieza a cambiar elegancia por eficiencia en esas partes.
Por supuesto, esta regla no significa que debas ignorar el rendimiento
completamente. En muchos casos, como en la función potencia
,
no se gana demasiada simplicidad de la solución "elegante". Y algunas
veces un programador experimentado puede inmediatamente que un
soulución simple nunca va a ser lo suficientemente rápida.
La razón por la que estoy hablando tanto de esto es que muchos programadores novatos se enfocan fanáticamente en la eficiencia, incluso en los detalles más pequeños. El resultado son programas más grandes, más complicados y a menudo menos correctos, que toman más tiempo en escribirse que sus equivalentes más sencillos y que generalmente corren solo un poco más rápido.
Pero la recursión no es siempre sólo una alternativa menos eficiente a los bucles. Alguos problemas son mucho más fáciles de resolver con recursión que con bucles. La mayoría de estos son problemas que requieren explorar o procesar varias "ramas", cada una de las cuáles se puede ramificar otra vez.
Considera el siguente rompecabezas: empezando por el número 1 y añadiendo repetidamente 5 o multiplicándolo por 3, un número infinito de nuevos números puede ser producido. ¿Cómo escribirías una función que, dado un número, trate de encontrar una secuencia de sumas y multipliclaciones que producen ese número? Por ejemplo, el número 13 puede ser producido al multiplicar por 3 primero y después sumar 5 dos veces, mientras que el número 15 no puede ser producido.
Aquí hay una solución recursiva:
function encontrarSolucion(objetivo) { function encontrar(inicio, historia) { if (inicio == objetivo) return historia; else if (inicio > objetivo) return null; else return encontrar(inicio + 5, "(" + historia + " + 5)") || encontrar(inicio * 3, "(" + historia + " * 3)"); } return encontrar(1, "1"); } console.log(encontrarSolucion(24)); // → (((1 * 3) + 5) * 3)
Nota que este programa no encuentra necesariamente la ruta más corta de operaciones. Se satisface cuando cuentre cualquier sequencia.
No necesariamente espero que veas como este trabaja esto inmediatamente. Pero trabajemos en eso, porque es un gran ejercicio en el pensamiento recursivo.
La función interna encontrar
hace la recursividad. Toma dos
argumentos – el número actual y una cadena que registra como
hemos alcanzado el número – y regresa o una cadena que muestra como
llegar al objetivo o null
.
Para hacer esto
la función realiza una de tres acciones. Si el número actual es el
número objetivo, entonces la historia actual es una forma de alcanzar
este objetivo, así que siemplemente es devuelta. Si el número actual
es mayot que el número objetivo, no tiene sentido seguir haciendo más
exploraciones porque tanto sumar como multiplicar sólo hará mayor el número.
Y finalmente, si estamos todavía debajo del objetivo la función prueba
los dos caminos posibles que empiezan con el número actual llamándose dos
veces a sí misma, una vez por cada uno de los pasos permitos. Si la
primera llamada regresa cualquier cosa que no sea null
, se devuelve como
resultado. De otra forma, la segunda opción es la que se devuelve, independientemente
de si produce una cadena o null
.
Para entender mejor como esta función produce el
efecto que estamos buscando, miremos a todas las llamadas a encontrar
que
se hacen cuando estamos buscando la solución para el número 13.
encuentra(1, "1") encuentra(6, "(1 + 5)") encuentra(11, "((1 + 5) + 5)") encuentra(16, "(((1 + 5) + 5) + 5)") demasiado grande encuentra(33, "(((1 + 5) + 5) * 3)") demasiado grande encuentra(18, "((1 + 5) * 3)") demasiado grande encuentra(3, "(1 * 3)") encuentra(8, "((1 * 3) + 5)") encuentra(13, "(((1 * 3) + 5) + 5)") ¡Encontrado!
El sangrado (indentación) indica la profundidad en la pila de llamadas. La primera vez que
encuentra
es llamada, se llama dos veces a sí misma para explorar las
soluciones que empiezan con (1 + 5)
y (1 * 3)
. La primera llamada intenta
encontrar una solución que empiece con (1 + 5)
y, usando la recursión,
explora todas las soluciónes que produzcan un número menor o igual que
el número buscado. Dado que no encuentra una solución que corresponda con
el objetivo, regresa null
a la primera llamada. Entonces el operador ||
ahce que la llamada que explora (1 * 3)
suceda. Esta búsqueda tiene más suerte
debido a que su primera llamada recursiva, a través de otra llamada recursiva,
llega al número que buscamos, 13. Esta llamada recursuva, la más interna,
regresa una cadena y cada uno de los operadores ||
en la llamada superior inmediata
pasan esa cadena, finalmente regresando nuestra solución.
Funciones crecientes
Existen dos formas más o menos naturales de intorducir las funciones en los programas.
La primera es que te encuentras a ti mismo escribiendo el mismo código varias veces. Necesitamos evitar hacer debido a que más código significa más espacio para que los errores se oculten y más material para leer para que leer para las personas que quiere entender el programa. Así que tomamos funcionalidad repetida, encontramos un buen nombre para esta, y la ponemos dentro de una función. La segunda forma es que encuentres que necesitas cierta funcionalidad que no has escrito aún y que suena como a que merece su propia función. Empezarás por nombrar la función y después escribirás su cuerpo. Podrías incluso empezar a escribir el código que usa la función antes de realmente definir la función misma.
Qué tan difícil es encontrar un nombre para una función es un buen indicador de que tan claro tienes el concepto de aquello que estás tratando de envolver. Hagamos un ejemplo.
Necesitamos escribir un problema que imprima dos números,
el número de las vacas y de los pollos de una granja, con las palabras Cows
y
Pollos
después de estos, y completados con ceros para que siempre sean de 3 dígitos
de longitud.
007 Vacas 011 Pollos
Esto claramente requiere una función de dos argumentos. Empecemos a programar.
function imprimeInventarioGranja(vacas, pollos) { var vacasString = String(vacas); while (vacasString.length < 3) vacasString = "0" + vacasString; console.log(vacasString + " Vacas"); var pollosString = String(pollos); while (pollosString.length < 3) pollosString = "0" + pollosString; console.log(pollosString + " Pollos"); } imprimeInventarioGranja(7, 11);
Añadir .length
despues
de un valor de cadena nos dará la longitud de esa cadena. Así, los while
continuán agregando ceros al principio de la cadena del número hasta que
son por lo menos de 3 caracteres.
¡Misión cumplida! Pero casi cuando le vamos a mandar el código al granjero (con una buena factura) nos llama y nos dice que ha empezado a críar puercos, y que si podríamos extender el software para también mostrar los puerquitos, ¿por favor?.
De seguro podemos. Pero mientras estamos en el proceso de copiar y pegar esas cuatro líneas una vez más, paramos y reconsideramos. Existe una mejor forma. Aquí hay un intento:
function imprimeConCerosYEtiqueta(numero, etiqueta) { var numeroString = String(numero); while (numeroString.length < 3) numeroString = "0" + numeroString; console.log(numeroString + " " + etiqueta); } function imprimeInventarioGranja(vacas, pollos, puercos) { imprimeConCerosYEtiqueta(vacas, "Vacas"); imprimeConCerosYEtiqueta(pollos, "Pollos"); imprimeConCerosYEtiqueta(puercos, "Puercos"); } imprimeInventarioGranja(7, 11, 3);
¡Funciona! Pero ese nombre,
imprimeConCerosYEtiqueta
, es un poco feo. Amonotona tres cosas–imprimir,
completar con ceros, y agragar la etiqueta–en una sola función.
En vez de quitar la parte repetida de nuestra programa por mayoreo, tratemos de escoger un solo concepto.
function rellenoConCero(numero, ancho) { var cadena = String(numero); while (cadena.length < ancho) cadena = "0" + cadena; return cadena; } function imprimeInvantarioGranja(vacas, pollos, puercos) { console.log(rellenoConCero(vacas, 3) + " Vacas"); console.log(rellenoConCero(pollos, 3) + " Pollos"); console.log(rellenoConCero(puercos, 3) + " Puercos"); } imprimeInvantarioGranja(7, 16, 3);
Una función con un bonito y obvio
nombre como rellenoConCero
hace más fácil para alguien que lea el
código descubrir lo que hace. Y eso es útil en más situaciones que sólo
en este programa específico. Por ejemplo, puedes usarla para imprimir
una tabla bien alineada de números.
¿Qué tan inteligente y versátil debería ser nuestra función? Podríamos escribir cualquier cosa desde una función terriblemente simple que sólamente rellena un número de tal manera que tenga 3 caracteres hasta una complicada, muy generalizada función, un sistema de formateo de números que maneje fracciones, números negativos, alineación de puntos, relleno con diferentes carecteres y así por el estilo.
Un principio útil no añadir características a menos que estés completamente seguro de que las vas a ocupar. Puede ser tentador escribir marcos de trabajo generals "frameworks" para cada pequeño pedazo de funcionalidad que te encuentras. Resiste el impulso. No acabarás ningún trabajo real y terminarás escribiendo mucho código que nadie usará alguna vez.
Funciones y efectos colaterales
Las funciones pueden ser más o menos divididas en aquellas que son llamadas por sus efectos colaterales y aquellas que son llamadas por su valor de resultado. (Aunque es completamente posible tener tanto efectos colaterales como retornar un valor).
La primer función de ayuda en el ejemplo de la granja,
imprimeConCerosYEtiqueta
, es llamada por su efecto colateral:
imprime su valor de resultado. La segunda versión, rellenoConCero
, es
llamada por su valor de resultado. No es coincidencia que la segunda sea
útil en más situaciones que la primera. Las funciones que crean valores
son más fáciles de combinar en nuevas formas que funciones que
realizan directamente un efecto colateral.
Una función pura es un tipo de función que produce un valor que no sólo carece de efectos colaterales, tampoco usa los efectos colaterales de otro código–por ejemplo, no lee variables globales que son ocasionalmente cambiadas por otro código. Una función pura tiene la agradable propiedad de que, cuando se llama los mismos argumentos, siempre produce el mismo valor (y no hace nada más). Esto hace fácil razonar con ella. Una llamada a una función como esta puede ser sustituida mentalmente como su resultado, sin cambiar el significado del código. Cuando no estas seguro de que una función pura funciona correctamente, puedes probarlo simplemente llamándola, y sabiendo que trabaja bien este contexto, trabajará bien en cualquier context. Funciones no puras pueden regresar diferentes valores basadas en todo tipo de factores y tienen efectos secundarios y que pueden ser difícil de probar y de razonar con ellas.
Aún así, no hay necesidad de sentirse mal
cuando escribimos funciones que no son puras o de armar una guerra santa
para eliminarlas de tu código. Los efectos secundarios a menudo son útiles.
No habría forma de imprimir una versión pura de console.log
, por ejemplo, y
console.log
es ciertamente útil. Algunas operaciones son además más fáciles
de expresar en una forma eficiente cuando se usan los efectos scundarios,
así que la velocidad de cómputo puede ser una razón para evitar la pureza.
Sumrio
Este capítulo enseñó como escribir tus propias funciones. La palabra
resevada function
, cuando se usa como una expresión, puede crear un
valor función. Cuando es usada como sentencia, puede ser usada para declarar
una variable y darle una función como valor.
// Crear un valor función f var f = function(a) { console.log(a + 2); }; // Declarar g como función function g(a, b) { return a * b * 3.5; }
Un aspecto clave para entender las funciones es entender los ámbitos locales. Los parámetros y variables declaradas dentro de una función son locales para la función, re-creados cada vez que la función es llamada y no visibles de desde fuera. Las funciones declaradas dentro de otra función tienen acceso al ámbito local externo de la función.
Seprar las tareas que tu tarea realiza en diferentes funciones es útil. No tendrás que repetir lo mismo tanto, y las funciones pueden hacer un programa más legible al agrupar el código en partes conceptuales, de la misma forma que capítulos y secciones ayudan a organizar el texto regular.
Ejercicios
Mínimo
El
capítulo anterior
intordujo la función estándar Math.min
que devuelve su argumento más pequeño.
Ahora nosotros mismos podemos hacer eso. Escribe una función min
que
tome dos argumentos y devuelva el mínimo.
// Tu código va aquí. console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10
Recursión
Hemos visto que
%
(el operador de sobrante) puede ser usado para probar si un número
es par o impar usando % 2
para checar si es divisible por dos. Aquí
hay otra forma de definir si un número entero es par o impar:
Escribe una función recursiva esPar
que corresponda a esta descripción.
La función debería aceptar un numero
como parámetro y regresar un Booleano.
Pruebala con 50 y 75. Observa cómo se comporta con -1. ¿Por qué? ¿Puedes pensar en alguna forma de componer esto?
// Tu código va aquí. console.log(esPar(50)); // → true console.log(esPar(75)); // → false console.log(esPar(-1)); // → ??
La función será algo similar al
encuentra
interno en la solución recursiva
ejemplo encuentraSolucion
en este capítulo, con una cadena de if
/else if
/else
que probará
cuál de los tres casos aplica. El else
final, correspondiente al tercer caso,
hace la llamada recursiva. Cada una de las ramas debe de contener una
sentencia return
o de alguna otra forma arrglárselas para devolver un
valor específico.
Cuando se le da un número negativo, la función va llamarse a sí misma una y otra vez, pasándose un número cada vez más negativo, así alejándose más y más de regresar una solución. En algún momento se quedará sin espacio en la pila y abortará.
Contando Frijoles
Puedes obtener el
n-avo caracter, o letra, de una cadena escribiendo "cadena".charAt(N)
,
similar a como obtienes su longitud con "cadena".length
. El valor
obtenido será una cadena que contiene sólo un caracter(por ejemplo, "b"
).
El primer caracter tiene la posición cero, lo cual hace que el último
pueda ser encontrado en la posición string.lenght -1
. En otras palabras,
una cadenas de dos caracteres tiene un “lenght” o longitud de 2, y sus
caracteres tienen las posiciones 0 y 1.
Escribe una función cuentaFs
que tome una cadena como su único
argumento y regrese un número que indique cunántos caracteres “F” mayúscula
hay en la cadena.
A continuación, escribe una funcióon llamada cuentaCaracter
que se comoporte
como cuentaFs
, con la diferencia de que tome un segundo caracter que
indique el caracter que será contado(en vez de sólo caracteres “F”).
Reescribe cuentaFs
para hacer uso de esta nueva función.
// Tu código va a aquí. console.log(cuentaFs("FFC")); // → 2 console.log(cuentaCaracter("ferrocarril", "r")); // → 4
Un bucle en tu función tendrá que
revisar cada caracter en la cadena corriendo un índice desde cero hasta uno
menos que su longitud (< cadena.length
). Si el caracter de la
posición actual es el mismo que la función está buscando, añade uno a
la variable contador. Una vez que se ha terminado el bucle, el contador
puede ser regresado.
Pon atención en hacer todas las variables usadas en la
función locales a la función midiante el uso de la palabra reservada
var
.