Orden de evaluación y declaración de funciones de JavaScript

80

¿Por qué el primero de estos ejemplos no funciona, pero todos los demás sí?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();
Todos somos monica
fuente

Respuestas:

182

No se trata de un problema de alcance ni de cierre. El problema está en la comprensión entre declaraciones y expresiones .

El código JavaScript, ya que incluso la primera versión de JavaScript de Netscape y la primera copia de él de Microsoft, se procesa en dos fases:

Fase 1: compilación: en esta fase, el código se compila en un árbol de sintaxis (y código de bytes o binario según el motor).

Fase 2: ejecución - luego se interpreta el código analizado.

La sintaxis para la declaración de funciones es:

function name (arguments) {code}

Los argumentos son, por supuesto, opcionales (el código también es opcional, pero ¿qué sentido tiene eso?).

Pero JavaScript también te permite crear funciones usando expresiones . La sintaxis de las expresiones de función es similar a las declaraciones de función excepto que están escritas en el contexto de la expresión. Y las expresiones son:

  1. Cualquier cosa a la derecha de un =signo (o :en objetos literales).
  2. Cualquier cosa entre paréntesis ().
  3. Parámetros de funciones (esto en realidad ya está cubierto por 2).

Las expresiones a diferencia de las declaraciones se procesan en la fase de ejecución en lugar de en la fase de compilación. Y por eso importa el orden de las expresiones.

Entonces, para aclarar:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();

Fase 1: compilación. El compilador ve que la variable someFunctionestá definida y la crea. Por defecto, todas las variables creadas tienen el valor indefinido. Tenga en cuenta que el compilador aún no puede asignar valores en este punto porque los valores pueden necesitar que el intérprete ejecute algún código para devolver un valor para asignar. Y en esta etapa aún no estamos ejecutando código.

Fase 2: ejecución. El intérprete ve que desea pasar la variable someFunctiona setTimeout. Y así es. Desafortunadamente, el valor actual de someFunctionno está definido.


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();

Fase 1: compilación. El compilador ve que está declarando una función con el nombre someFunction y así la crea.

Fase 2: El intérprete ve que desea pasar someFunctional setTimeout. Y así es. El valor actual de someFunctiones su declaración de función compilada.


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();

Fase 1: compilación. El compilador ve que ha declarado una variable someFunctiony la crea. Como antes, su valor no está definido.

Fase 2: ejecución. El intérprete pasa una función anónima a setTimeout para que se ejecute más tarde. En esta función, ve que estás usando la variable, someFunctionpor lo que crea un cierre a la variable. En este punto, el valor de someFunctiontodavía no está definido. Luego ve que le asignas una función someFunction. En este punto, el valor de someFunctionya no está indefinido. 1/100 de segundo después, setTimeout se dispara y se llama a someFunction. Dado que su valor ya no está indefinido, funciona.


El caso 4 es realmente otra versión del caso 2 con un poco del caso 3 someFunctionincluido . En el punto que se pasa a setTimeout, ya existe debido a que se ha declarado.


Aclaración adicional:

Quizás se pregunte por qué setTimeout(someFunction, 10)no crea un cierre entre la copia local de someFunction y la pasada a setTimeout. La respuesta a eso es que los argumentos de función en JavaScript siempre, siempre se pasan por valor si son números o cadenas o por referencia para todo lo demás. Entonces, setTimeout no obtiene la variable someFunction que se le pasa (lo que habría significado la creación de un cierre), sino que solo obtiene el objeto al que se refiere someFunction (que en este caso es una función). Este es el mecanismo más utilizado en JavaScript para romper cierres (por ejemplo, en bucles).

slebetman
fuente
7
Esa fue una gran respuesta en serio.
Matt Briggs
1
@ Matt: He explicado esto en otro lugar (varias veces) en SO. Algunas de mis explicaciones favoritas: stackoverflow.com/questions/3572480/…
slebetman
3
@Matt: Técnicamente, los cierres no implican alcance sino marco de pila (también conocido como registro de activación). Un cierre es una variable compartida entre los marcos de la pila. Un marco de pila sirve para determinar qué es un objeto para clasificar. En otras palabras, un alcance es lo que el programador percibe en la estructura del código. Un marco de pila es lo que se crea en tiempo de ejecución en la memoria. No es realmente así, pero lo suficientemente cerca. Al pensar en el comportamiento en tiempo de ejecución, a veces no es suficiente una comprensión basada en el alcance.
slebetman
3
@slebetman para su explicación del ejemplo 3, menciona que la función anónima dentro de setTimeout crea un cierre para la variable someFunction y que en este punto, someFunction aún no está definida, lo cual tiene sentido. Parece que la única razón por la que el ejemplo 3 no devuelve undefined es debido a la función setTimeout (el retraso de 10 milisegundos permite que JavaScript ejecute la siguiente instrucción de asignación a alguna función, por lo que se define)
wmock
2

El alcance de Javascript se basa en funciones, no en un alcance estrictamente léxico. Eso significa que

  • Alguna función1 se define desde el inicio de la función adjunta, pero su contenido no está definido hasta que se asigna.

  • en el segundo ejemplo, la asignación es parte de la declaración, por lo que se 'mueve' a la parte superior.

  • en el tercer ejemplo, la variable existe cuando se define el cierre interno anónimo, pero no se usa hasta 10 segundos después, para entonces se ha asignado el valor.

  • El cuarto ejemplo tiene tanto la segunda como la tercera razones para funcionar.

Javier
fuente
1

Porque someFunction1aún no se ha asignado en el momento en que setTimeout()se ejecuta la llamada a .

someFunction3 puede parecer un caso similar, pero como está pasando una función que se ajusta someFunction3()a setTimeout()en este caso, la llamada a someFunction3()no se evalúa hasta más tarde.

mate b
fuente
¿Pero someFunction2tampoco se ha asignado aún cuando setTimeout()se ejecuta la llamada a ...?
Todos somos Mónica
1
@jnylen: Declarar una función con la functionpalabra clave no es exactamente equivalente a asignar una función anónima a una variable. Las funciones declaradas como function foo()son "elevadas" al comienzo del alcance actual, mientras que las asignaciones de variables ocurren en el punto donde están escritas.
Chuck
1
+1 para funciones especiales. Sin embargo, el hecho de que pueda funcionar no significa que deba hacerse. Declare siempre antes de usar.
mway
@mway: en mi caso, he organizado mi código dentro de una "clase" en secciones: variables privadas, controladores de eventos, funciones privadas, luego funciones públicas. Necesito que uno de mis controladores de eventos llame a una de mis funciones privadas. Para mí, mantener el código organizado de esta manera gana sobre ordenar las declaraciones léxicamente.
We Are All Monica
1

Esto parece un caso básico de seguir un buen procedimiento para no meterse en problemas. Declare variables y funciones antes de usarlas y declare funciones como esta:

function name (arguments) {code}

Evite declararlos con var. Esto es descuidado y genera problemas. Si adquiere el hábito de declarar todo antes de usarlo, la mayoría de sus problemas desaparecerán rápidamente. Al declarar variables, las inicializaría con un valor válido de inmediato para asegurarme de que ninguna de ellas esté indefinida. También tiendo a incluir código que verifica valores válidos de variables globales antes de que una función los use. Esta es una protección adicional contra errores.

Los detalles técnicos de cómo funciona todo esto son como la física de cómo funciona una granada de mano cuando juegas con ella. Mi simple consejo es, en primer lugar, no jugar con granadas de mano.

Algunas declaraciones simples al principio del código podrían resolver la mayoría de estos tipos de problemas, pero aún podría ser necesaria una limpieza del código.

Nota adicional:
Ejecuté algunos experimentos y parece que si declaras todas tus funciones de la manera descrita aquí, realmente no importa en qué orden estén. Si la función A usa la función B, la función B no tiene que hacerlo. declararse antes de la función A.

Entonces, declare todas sus funciones primero, luego sus variables globales, y luego coloque su otro código al final. Siga estas reglas generales y no se equivocará. Incluso podría ser mejor poner sus declaraciones en el encabezado de la página web y su otro código en el cuerpo para garantizar el cumplimiento de estas reglas.

Terry Prothero
fuente