Definir una función dentro de otra función en JavaScript

82
function foo(a) {
    if (/* Some condition */) {
        // perform task 1
        // perform task 3
    }
    else {
        // perform task 2
        // perform task 3
    }
}

Tengo una función cuya estructura es similar a la anterior. Quiero abstraer la tarea 3 en una función, bar()pero deseo limitar el acceso a esta función solo dentro del alcance de foo(a).

Para lograr lo que quiero, ¿es correcto cambiar a lo siguiente?

function foo(a) {
    function bar() {
        // Perform task 3
    }

    if (/* Some condition */) {
        // Perform task 1
        bar();
    }
    else {
        // Perform task 2
        bar();
    }
}

Si lo anterior es correcto, ¿ bar()se redefine cada vez que foo(a)se llama? (Me preocupa el desperdicio de recursos de CPU aquí).

tamakisquare
fuente
1
Pruebe usted mismo si vale la pena: jsperf.com Me imagino que depende de task3.
tomByrer
1
@tomByer - +1 por sugerir la herramienta
tamakisquare

Respuestas:

121

Sí, lo que tienes ahí está bien. Algunas notas:

  • barse crea en cada llamada a la función foo, pero:
    • En los navegadores modernos, este es un proceso muy rápido. (Es posible que algunos motores solo compilen el código una vez y luego reutilicen ese código con un contexto diferente cada vez; el motor V8 de Google [en Chrome y en otros lugares] lo hace en la mayoría de los casos).
    • Y dependiendo de lo que barhaga, algunos motores pueden determinar que pueden "en línea", eliminando la llamada de función por completo. V8 hace esto, y estoy seguro de que no es el único motor que lo hace. Naturalmente, solo pueden hacer esto si no cambia el comportamiento del código.
  • El impacto en el rendimiento, si lo hay, de haber barcreado cada vez variará ampliamente entre los motores de JavaScript. Si bares trivial, variará de indetectable a bastante pequeño. Si no estás llamando foomiles de veces seguidas (por ejemplo, desde un mousemovecontrolador), no me preocuparía. Incluso si es así, solo me preocuparía si veo un problema en motores más lentos. Aquí hay un caso de prueba que involucra operaciones DOM , que sugiere que hay un impacto, pero trivial (probablemente eliminado por las cosas DOM). Aquí hay un caso de prueba que hace computación pura que muestra un impacto mucho mayor, pero francamente incluso, estamos hablando de una diferencia de microsegundos porque incluso un aumento del 92% en algo que requiere microsegundos para suceder sigue siendo muy, muy rápido. Hasta / a menos que haya visto un impacto en el mundo real, no es algo de qué preocuparse.
  • barsolo será accesible desde dentro de la función, y tiene acceso a todas las variables y argumentos para esa llamada a la función. Esto hace que este sea un patrón muy útil.
  • Tenga en cuenta que debido a que ha utilizado una declaración de función , no importa dónde coloque la declaración (superior, inferior o medio, siempre que esté en el nivel superior de la función, no dentro de una declaración de control de flujo, que es un error de sintaxis), se define antes de que se ejecute la primera línea del código paso a paso.
TJ Crowder
fuente
Gracias por tu respuesta. Entonces, ¿estás diciendo que este es un impacto insignificante en el rendimiento? (dado que barse crea una copia de en cada llamada de foo)
tamakisquare
2
@ahmoo: Con el rendimiento de JavaScript, la respuesta casi siempre es: depende. :-) Depende de qué motor lo ejecutará y con qué frecuencia llamará foo. Si no llamas foomiles de veces seguidas (por ejemplo, no en un mousemovecontrolador), entonces no me preocuparía en absoluto. Y tenga en cuenta que algunos motores (V8, por ejemplo) incorporarán el código de todos modos, eliminando por completo la llamada a la función, siempre que hacerlo no cambie lo que está sucediendo de una manera que pueda detectarse externamente.
TJ Crowder
@TJCrowder: ¿puedes comentar la respuesta de robrich? ¿Esa solución evita la recreación de bar()en cada llamada? Además, ¿ayudaría usar foo.prototype.barpara definir la función?
rkw
4
@rkw: Crear la función una vez, como lo hace la respuesta de robrich, es una forma útil de evitar el costo de crearla en cada llamada. Pierde el hecho de que bartiene acceso a las variables y argumentos para la llamada a foo(cualquier cosa en la que desee que opere, debe pasarla), lo que puede complicar un poco las cosas, pero en una situación de rendimiento crítico en la que ha visto un problema real, puede refactorizar así para ver si resuelve el problema. No, el uso foo.prototypeno ayudaría realmente (por un lado, barya no sería privado).
TJ Crowder
@ahmoo: Se agregó un caso de prueba. Curiosamente, obtengo un resultado diferente del caso de prueba de Guffa, creo que su función puede ser demasiado simple. Pero todavía no creo que el rendimiento sea un problema.
TJ Crowder
15

Para eso están los cierres.

var foo = (function () {
  function bar() {
    // perform task 3
  };

  function innerfoo (a) { 
    if (/* some cond */ ) {
      // perform task 1
      bar();
    }
    else {
      // perform task 2
      bar();
    }
  }
  return innerfoo;
})();

Innerfoo (un cierre) contiene una referencia a bar y solo se devuelve una referencia a innerfoo desde una función anónima que se llama solo una vez para crear el cierre.

El bar no es accesible desde el exterior de esta manera.

Michiel Borkent
fuente
1
Interesante. Tengo una exposición limitada a javascript, por lo que el cierre es algo nuevo para mí. Sin embargo, ha marcado un punto de partida para que estudie el cierre. Gracias.
tamakisquare
¿Con qué frecuencia utiliza el cierre para tratar con ámbitos de función / variable? Por ejemplo, si tiene 2 funciones que necesitan acceder a las mismas 3 variables, ¿declararía las 3 variables en un cierre junto con las 2 funciones y luego devolvería las 2 funciones?
doubleOrt
8
var foo = (function () {
    var bar = function () {
        // perform task 3
    }
    return function (a) {

        if (/*some condition*/) {
            // perform task 1
            bar();
        }
        else {
            // perform task 2
            bar();
        }
    };
}());

El cierre mantiene el alcance de bar()contenido, devolviendo la nueva función de la función anónima autoejecutable establece un alcance más visible a foo(). La función de autoejecución anónima se ejecuta exactamente una vez, por lo que solo hay una bar()instancia, y cada ejecución de la foo()usará.

robrich
fuente
Interesante. Entonces tengo que buscar el cierre. Gracias.
tamakisquare
¿Con qué frecuencia utiliza el cierre para tratar con ámbitos de función / variable? Por ejemplo, si tiene 2 funciones que necesitan acceder a las mismas 3 variables, ¿declararía las 3 variables en un cierre junto con las 2 funciones y luego devolvería las 2 funciones?
doubleOrt
¿Con qué frecuencia utilizo cierres? Todo el tiempo. Si tuviera 3 variables necesarias para 2 funciones: 1. (mejor) pasar las 3 variables a ambas funciones - las funciones se pueden definir una vez y solo una vez. 2. (bueno) cree las 2 funciones donde las variables están fuera del alcance de ambas. Este es un cierre. (Básicamente la respuesta aquí.) Lamentablemente, las funciones se redefinen para cada nuevo uso de las variables. 3. (malo) no use funciones, solo tenga un método grande y largo que haga ambas cosas.
robrich
5

Sí, eso funciona bien.

La función interna no se recrea cada vez que ingresa a la función externa, pero se reasigna.

Si prueba este código:

function test() {

    function demo() { alert('1'); }

    demo();
    demo = function() { alert('2'); };
    demo();

}

test();
test();

se mostrará 1, 2, 1, 2, no 1, 2, 2, 2.

Guffa
fuente
Gracias por tu respuesta. ¿La reasignación de demo()cada tiempo test()debe ser un problema de desempeño? ¿Depende de la complejidad de demo()?
tamakisquare
1
Hice una prueba de rendimiento: jsperf.com/inner-function-vs-global-function La conclusión es que, en general, no se trata de un problema de rendimiento (ya que cualquier código que coloque en las funciones tardará mucho más en ejecutarse que crear la función sí mismo), pero si necesitara esa ventaja de rendimiento adicional, tendría que escribir código diferente para diferentes navegadores.
Guffa
Gracias por dedicar el tiempo a crear la prueba y, además de compartir sus puntos sobre el rendimiento. Muy apreciado.
tamakisquare
Ha dicho que "la función interior no se recrea cada vez" con mucha confianza. De acuerdo con la especificación , lo es; si un motor lo optimiza depende del motor. (Espero que la mayoría lo haga). Estoy intrigado al ver que su caso de prueba y el mío tienen resultados tan variables: jsperf.com/cost-of-creating-inner-function Sin embargo, no creo que el rendimiento sea un problema.
TJ Crowder
@TJCrowder: Bueno, sí, es un detalle de implementación, pero como los motores Javascript modernos compilan el código, no volverán a compilar la función cada vez que se le asigne. La razón por la que el resultado de las pruebas de rendimiento es diferente es porque prueban cosas diferentes. Mi prueba compara funciones globales con funciones locales, mientras que su prueba compara una función local con código en línea. Por supuesto, insertar el código será más rápido que llamar a la función, es una técnica de optimización común.
Guffa
0

Creé un jsperf para probar las expresiones anidadas frente a las no anidadas y las expresiones de función frente a las declaraciones de función, y me sorprendió ver que los casos de prueba anidados funcionaban 20 veces más rápido que los no anidados. (Anticipé diferencias opuestas o insignificantes).

https://jsperf.com/nested-functions-vs-not-nested-2/1

Esto está en Chrome 76, macOS.

Michael Liquori
fuente