En JavaScript tenemos básicamente dos formas de definir funciones.
La primera y más tradicional es usando la palabra clave function y otorgándole un nombre a la función. Por ejemplo:
function miFunc() {
..... //Código de la función
}
A esto se le llama declaración de funciones.
Otro modo muy habitual es, simplemente, asignar una función anónima a una variable para trabajar con ella, algo así:
var miFunc = function() {
.... //Código de la función
}
A esto se le llama expresión funcional o asignación de funciones.
De hecho es posible, aunque no frecuente, hacer una combinación de ambos estilos:
var miFunc = function miFuncion(){
.... //Código de la función
}
Lo que constituiría una asignación de función con nombre.
Todos estos casos son equivalentes, pero solo en apariencia. En todos ellos podemos llamar a la función escribiendo:
miFunc();
y aparentemente todo parece funcionar del mismo modo.
Sin embargo ¿existe alguna diferencia entre declarar una función de una forma o de otra? ¿Alguna ventaja de usar una u otra? ¿Algún impacto negativo?
Vamos a verlo...
Mi interpretación de una función anónima ;-)
Elevación de declaraciones
La principal diferencia entre declarar funciones de estas dos maneras tiene que ver con el efecto de hoisting, que ya expliqué en su día en este blog (ver enlace anterior antes de seguir leyendo si no lo conoces).
Cuando se produce hoisting en un ámbito, la declaración de una variable se mueve siempre al principio del bloque. Por ejemplo:
function miFunc() {
alert(v());
var v = function() {return 5;};
}
Este fragmento producirá un error porque la función no está definida y 'v' se considera una variable sin inicializar, ya que es equivalente a este otro código:
function miFunc() {
var v;
alert(v());
v = function() { return 5;};
}
puesto que debido al hoisting de variables, la declaración (¡pero no la asignación!) de la variable se mueve al comienzo del ámbito.
Solo con que movamos el alert para debajo de la declaración ya funcionará como era de esperar.
Bien, si ahora hacemos lo mismo pero con una función con nombre, o sea, así:
function miFunc() {
alert(v());
function v() { return 5; };
}
¿Qué resultado crees que aparecerá por pantalla?
En este caso funcionará como se esperaba y mostrará un 5, y eso que la función se define posteriormente al alert
en el bloque.
El motivo es que cuando se produce el hoisting, las funciones con nombre se mueven enteras al principio del bloque, es decir, definición y asignación, y por lo tanto están disponibles inmediatamente en cualquier punto de su ámbito. El código anterior es equivalente a haber definido la función arriba del todo.
Esta es la gran diferencia existente entre funciones con nombre y anónimas, y hay que tenerlo muy en cuenta siempre para evitar resultados inesperados.
Pero siendo ordenados no debería haber diferencia entre usar unas u otras.
Rendimiento
Respecto al rendimiento tampoco hay diferencia alguna en condiciones normales. Solo debemos tener en cuenta que cada vez que definimos una función anónima, aunque el código sea idéntico al de otra que hayamos definido antes, en realidad son dos funciones diferentes. Por lo tanto si definimos muchas funciones idénticas en un bucle (por ejemplo, recorremos todos los elementos de un determinado tipo en una página y les asignamos el mismo manejador para un evento), si la función va a ser idéntica es mejor definirla con nombre en el mismo ámbito y asignarla con ese nombre (puede ser también anónima y asignarla a través de una misma variable: el caso es apuntar a la misma función), en lugar de usar la definición de código. De esta manera tenemos una única función asignada, y no una nueva en cada asignación.
Es decir, si tenemos un código similar a este:
for(var i=0; i<colElementos.length; i++){
colElementos[i].onclick = function { .... };
}
es más eficiente hacerlo de esta manera:
var manejador = function() {...};
for(var i=0; i<colElementos.length; i++){
colElementos[i].onclick = manejador;
}
o bien:
function manejador() {...};
for(var i=0; i<colElementos.length; i++){
colElementos[i].onclick = manejador;
}
ya que en el primer caso en cada vuelta del bucle estamos creando una nueva función, mientras que en los otros dos casos estamos asignando la misma función una y otra vez, lo cual consume menos memoria y recursos, lógicamente.
Pero fijémonos en que, en realidad, no existe diferencia alguna entre si es una función con nombre o anónima la que asignamos dentro del bucle, mientras no la creemos nueva una y otra vez. En los dos últimos bloques el primero usa una función anónima y el segundo una con nombre, y ambos son igual de eficientes y tienen el mismo rendimiento.
En realidad, en mi opinión, la única ventaja que tienen las funciones con nombre o declaradas frente a las asignadas es que es más fácil utilizarlas en recursión.
Test de conocimientos
De acuerdo. Ahora que tenemos claro el concepto, vamos a ver si lo hemos entendido bien.
Consideremos el siguiente fragmento de código:
function func1(){
function a(){
return 5;
}
return a();
function a(){
return 10;
}
}
console.log(func1());
¿Qué resultado crees que se mostrará por consola? ¿5 o 10?
....... (piénsalo bien antes de contestar)
Si has contestado 5, estás en un error. El motivo es que, como hemos visto, la definición y asignación de las funciones con nombre sufren hoisting dentro de su ámbito, es decir, se suben al principio del ámbito. Por ello, el código anterior es equivalente al siguiente:
function func1(){
function a(){
return 5;
}
function a(){
return 10;
}
return a();
}
console.log(func1());
Es decir, aunque la segunda (re)definición de la función a
se hace después de la llamada a return a();
en realidad es como si se hubiera definido antes, y por lo tanto la que prevalece es la última, la segunda, y se devuelve un 10.
Consideremos ahora un código muy parecido pero usando asignación de funciones o funciones anónimas:
function func1(){
var a = function(){
return 5;
}
return a();
var a = function(){
return 10;
}
}
console.log(func1());
¿Cuál será ahora el resultado?
......
Ahora el resultado será 5. El motivo es que el hoisting en este caso lo sufre la declaración de las variables, pero no su asignación. Es decir, el código es equivalente al siguiente:
function func1(){
var a;
a = function(){
return 5;
}
return a();
a = function(){
return 10;
}
}
console.log(func1());
Solamente sube al principio del ámbito la declaración de la variable, quedando las asignaciones en el sitio concreto del código donde las hemos hecho. Por ello la segunda asignación nunca llega a producirse, pues antes ya salimos de la función con el return a();
.
En resumen
Estas distinciones pueden parecer una cuestión teórica sin importancia, pero en realidad es indispensable tener claros los conceptos y este tipo de situaciones. De no ser así es mas que probable que nuestro código en alguna ocasión tenga problemas y no sepamos de dónde vienen, ya que no es una cuestión obvia ni tenemos forma de ver esa "reorganización interna" del código que realiza el intérprete de JavaScript. Los programadores más noveles suelen tener dificultades con esto casi siempre.
¡Espero que te resulte util! Y ya sabes, si quieres aprender JavaScript a fondo y teniéndome a mi para contestarte todas tus dudas, tengo estos dos estupendos cursos on-line: