Deja de funciones de anidación! (Pero no todos)

JavaScript tiene más de quince años; sin embargo, el lenguaje sigue siendo mal interpretado por lo que quizás es la mayoría de los desarrolladores y diseñadores que usan el lenguaje. Uno de los aspectos más poderosos, aunque mal entendidos, de JavaScript son las funciones. Si bien es terriblemente vital para JavaScript, su uso incorrecto puede generar ineficiencia y dificultar el rendimiento de una aplicación..


Prefiero un video tutorial?


El rendimiento es importante

En la infancia de la web, el rendimiento no era muy importante..

En la infancia de la web, el rendimiento no era muy importante. Desde las conexiones de acceso telefónico de 56 K (o, lo que es peor) a una computadora Pentium de 133MHz de un usuario final con 8 MB de RAM, se esperaba que la web fuera lenta (aunque eso no impidió que todos se quejaran). Por esta razón, para empezar, se creó JavaScript, para descargar el procesamiento simple, como la validación de formularios, al navegador, lo que facilita y agiliza ciertas tareas para el usuario final. En lugar de completar un formulario, hacer clic en enviar y esperar al menos treinta segundos para que se le informe que ingresó datos incorrectos en un campo, JavaScript permitió a los autores de la Web validar su entrada y alertarle sobre cualquier error antes de enviar el formulario..

Avance rápido hasta hoy. Los usuarios finales disfrutan de computadoras multi-core y multi-GHz, una gran cantidad de RAM y velocidades de conexión rápidas. JavaScript ya no está relegado a la validación de formularios, pero puede procesar grandes cantidades de datos, cambiar cualquier parte de una página sobre la marcha, enviar y recibir datos del servidor y agregar interactividad a una página estática, todo con el nombre de mejorar la experiencia del usuario. Es un patrón bastante conocido en la industria de la computación: una creciente cantidad de recursos del sistema permite a los desarrolladores escribir sistemas operativos y software más sofisticados y que dependen de los recursos. Pero incluso con esta cantidad de recursos cada vez más abundante, los desarrolladores deben tener en cuenta la cantidad de recursos que consume su aplicación, especialmente en la web..

Los motores de JavaScript de hoy están a años luz de los motores de hace diez años, pero no optimizan todo. Lo que no optimizan se deja a los desarrolladores..

También hay un nuevo conjunto de dispositivos habilitados para la web, teléfonos inteligentes y tabletas, que se ejecutan en un conjunto limitado de recursos. Sus aplicaciones y sistemas operativos reducidos son sin duda un éxito, pero los principales proveedores de sistemas operativos móviles (e incluso los proveedores de sistemas operativos de escritorio) están considerando las tecnologías web como la plataforma de desarrollo de su elección, lo que empuja a los desarrolladores de JavaScript a garantizar que su código sea eficiente y eficaz.

Una aplicación de bajo rendimiento destruirá una buena experiencia..

Lo más importante, la experiencia del usuario depende de un buen rendimiento. Las IU bonitas y naturales ciertamente se suman a la experiencia del usuario, pero una aplicación de bajo rendimiento destruirá una buena experiencia. Si los usuarios no quieren usar su software, ¿cuál es el punto de escribirlo? Por lo tanto, es absolutamente vital que, en esta época de desarrollo centrado en la Web, los desarrolladores de JavaScript escriban el mejor código posible.

Entonces, ¿qué tiene todo esto que ver con las funciones?

El lugar donde definas tus funciones tiene un impacto en el rendimiento de tu aplicación.

Hay muchos anti-patrones de JavaScript, pero uno que involucra funciones se ha convertido en algo popular, especialmente en la multitud que se esfuerza por obligar a JavaScript a emular funciones en otros idiomas (características como la privacidad). Es funciones de anidamiento en otras funciones, y si se hace incorrectamente, puede tener un efecto adverso en su aplicación.

Es importante tener en cuenta que este antipatrón no se aplica a todas las instancias de funciones anidadas, pero generalmente se define por dos características. Primero, la creación de la función en cuestión generalmente es diferida, lo que significa que la función anidada no es creada por el motor de JavaScript en el tiempo de carga. Eso en sí mismo no es algo malo, pero es la segunda característica que dificulta el rendimiento: la función anidada se crea repetidamente debido a llamadas repetidas a la función externa. Entonces, si bien puede ser fácil decir "todas las funciones anidadas son malas", ese no es el caso, y podrá identificar funciones anidadas problemáticas y corregirlas para acelerar su aplicación..


Funciones de anidamiento en funciones normales

El primer ejemplo de este anti-patrón es anidar una función dentro de una función normal. Aquí hay un ejemplo simplificado:

function foo (a, b) barra de funciones () return a + b;  barra de retorno ();  foo (1, 2);

Es posible que no escriba este código exacto, pero es importante reconocer el patrón. Una función exterior, foo (), contiene una función interna, bar(), y llama a esa función interna a hacer trabajo. Muchos desarrolladores olvidan que las funciones son valores en JavaScript. Cuando declara una función en su código, el motor de JavaScript crea un objeto de función correspondiente, un valor que se puede asignar a una variable o pasar a otra función. El acto de crear un objeto de función se parece al de cualquier otro tipo de valor; El motor de JavaScript no lo crea hasta que lo necesita. Así que en el caso del código anterior, el motor de JavaScript no crea el interior bar() funciona hasta foo () ejecuta Cuando foo () salidas, las bar() el objeto de función es destruido.

El hecho de que foo () tiene un nombre implica que se llamará varias veces a lo largo de la aplicación. Mientras que una ejecución de foo () se consideraría OK, las llamadas subsiguientes causan un trabajo innecesario para el motor de JavaScript porque tiene que recrear un bar() objeto de función para cada foo () ejecución. Así que, si llamas foo () 100 veces en una aplicación, el motor de JavaScript tiene que crear y destruir 100 bar() Objetos de función. Gran cosa, ¿verdad? El motor tiene que crear otras variables locales dentro de una función cada vez que se llama, así que ¿por qué preocuparse por las funciones??

A diferencia de otros tipos de valores, las funciones normalmente no cambian; Se crea una función para realizar una tarea específica. Por lo tanto, no tiene mucho sentido perder ciclos de CPU recreando un valor algo estático una y otra vez.

Idealmente, el bar() El objeto de función en este ejemplo solo debe crearse una vez, y eso es fácil de lograr, aunque naturalmente, las funciones más complejas pueden requerir una refactorización extensa. La idea es mover el bar() declaración fuera de foo () para que el objeto de función solo se cree una vez, así:

función foo (a, b) barra de retorno (a, b);  barra de funciones (a, b) return a + b;  foo (1, 2);

Tenga en cuenta que el nuevo bar() La función no es exactamente como estaba dentro de foo (). Porque lo viejo bar() función utilizada la una y segundo parámetros en foo (), la nueva versión necesitaba refactorizar para aceptar esos argumentos para hacer su trabajo.

Dependiendo del navegador, este código optimizado es entre un 10% y un 99% más rápido que la versión anidada. Puede ver y ejecutar la prueba usted mismo en jsperf.com/nested-named-functions. Tenga en cuenta la simplicidad de este ejemplo. Un aumento de rendimiento del 10% (en el extremo más bajo del espectro de rendimiento) no parece ser mucho, pero sería mayor si hubiera más funciones anidadas y complejas involucradas.

Para quizás confundir el problema, envuelva este código en una función anónima de ejecución automática, como esta:

(function () function foo (a, b) barra de retorno (a, b); barra de función (a, b) return a + b; foo (1, 2); ());

El código de envoltura en una función anónima es un patrón común y, a primera vista, puede parecer que este código replica el problema de rendimiento mencionado anteriormente, envolviendo el código optimizado en una función anónima. Si bien hay un pequeño impacto en el rendimiento al ejecutar la función anónima, este código es perfectamente aceptable. La función de auto ejecución solo sirve para contener y proteger la foo () y bar() funciones, pero lo que es más importante, la función anónima se ejecuta solo una vez, por lo que la función foo () y bar() Las funciones se crean una sola vez. Sin embargo, hay algunos casos en los que las funciones anónimas son tan (o más) tan problemáticas como las funciones nombradas.


Funciones anonimas

En lo que respecta a este tema de rendimiento, las funciones anónimas tienen el potencial de ser más peligrosas que las funciones nombradas.

No es el anonimato de la función lo que es peligroso, pero es cómo los usan los desarrolladores. Es bastante común usar funciones anónimas al configurar controladores de eventos, funciones de devolución de llamada o funciones de iterador. Por ejemplo, el siguiente código asigna un hacer clic oyente del evento en el documento:

document.addEventListener ("click", function (evt) alert ("Has hecho clic en la página."););

Aquí, una función anónima se pasa a la addEventListener () método para cablear el hacer clic evento en el documento; Por lo tanto, la función se ejecuta cada vez que el usuario hace clic en cualquier lugar de la página. Para demostrar otro uso común de funciones anónimas, considere este ejemplo que usa la biblioteca jQuery para seleccionar todo elementos en el documento e iterar sobre ellos con el cada() método:

$ ("a"). each (función (índice) this.style.color = "red";);

En este código, la función anónima pasada al objeto jQuery cada() método se ejecuta para cada Elemento encontrado en el documento. A diferencia de las funciones nombradas, donde están implícitas para ser llamadas repetidamente, la ejecución repetida de un gran número de funciones anónimas es bastante explícita. Es imperativo, por el bien del rendimiento, que sean eficientes y optimizados. Eche un vistazo al complemento jQuery de seguimiento (una vez más simplificado):

$ .fn.myPlugin = function (options) return this.each (function () var $ this = $ (this); function changeColor () $ this.css (color: options.color); changeColor ();); ;

Este código define un plugin extremadamente simple llamado MyPlugin; Es tan simple que muchos rasgos comunes de complementos están ausentes. Normalmente, las definiciones de los complementos se envuelven dentro de funciones anónimas de ejecución automática, y generalmente se proporcionan valores predeterminados para las opciones para garantizar que los datos válidos estén disponibles para su uso. Estas cosas han sido eliminadas en aras de la claridad.

El propósito de este complemento es cambiar el color de los elementos seleccionados a lo que se especifique en el opciones objeto pasado a la myPlugin () método. Lo hace pasando una función anónima a la cada() iterador, haciendo que esta función se ejecute para cada elemento en el objeto jQuery. Dentro de la función anónima, una función interna llamada cambiar color () hace el trabajo real de cambiar el color del elemento. Tal como está escrito, este código es ineficiente porque, lo has adivinado, el cambiar color () ¿Se define la función dentro de la función iterativa? haciendo que el motor de JavaScript vuelva a crear cambiar color () con cada iteración.

Hacer este código más eficiente es bastante simple y sigue el mismo patrón que antes: refactoriza el código cambiar color () La función debe definirse fuera de cualquier función que lo contenga, y le permite recibir la información que necesita para realizar su trabajo. En este caso, cambiar color () Necesita el objeto jQuery y el nuevo valor de color. El código mejorado se ve así:

función changeColor ($ obj, color) $ obj.css (color: color);  $ .fn.myPlugin = function (options) return this.each (function () var $ this = $ (this); changeColor ($ this, options.color);); ;

Curiosamente, este código optimizado aumenta el rendimiento en un margen mucho menor que el foo () y bar() por ejemplo, con Chrome liderando el paquete con una ganancia de rendimiento del 15% (jsperf.com/function-nesting-with-jquery-plugin). La verdad es que acceder al DOM y usar la API de jQuery agrega su propio impacto al rendimiento, especialmente a jQuery cada(), que es notoriamente lento en comparación con los bucles nativos de JavaScript. Pero como antes, tenga en cuenta la simplicidad de este ejemplo. Cuanto más funciones anidadas, mayor es la ganancia de rendimiento de la optimización.

Funciones de anidamiento en funciones de constructor

Otra variación de este antipatrón es el anidamiento de funciones dentro de los constructores, como se muestra a continuación:

persona de la función (nombre de pila, apellido) this.firstName = firstName; this.lastName = lastName; this.getFullName = function () return this.firstName + "" + this.lastName; ;  var jeremy = nueva persona ("Jeremy", "McPeak"), jeffrey = nueva persona ("Jeffrey", "Way");

Este código define una función constructora llamada Persona(), y representa (si no fuera obvio) a una persona. Acepta argumentos que contienen el nombre y apellido de una persona y almacena esos valores en nombre de pila y apellido propiedades, respectivamente. El constructor también crea un método llamado getFullName (); concatena el nombre de pila y apellido propiedades y devuelve el valor de cadena resultante.

Cuando creas cualquier objeto en JavaScript, el objeto se almacena en la memoria

Este patrón se ha vuelto bastante común en la comunidad de JavaScript de hoy porque puede emular la privacidad, una función para la cual JavaScript no está diseñado actualmente (tenga en cuenta que la privacidad no se encuentra en el ejemplo anterior; veremos eso más adelante). Pero al usar este patrón, los desarrolladores crean ineficiencia no solo en el tiempo de ejecución, sino en el uso de la memoria. Cuando creas cualquier objeto en JavaScript, el objeto se almacena en la memoria. Permanece en la memoria hasta que todas las referencias a ella se configuran como nulo o están fuera de alcance. En el caso de la Jeremy objeto en el código anterior, la función asignada a getFullName normalmente se almacena en la memoria mientras Jeremy objeto está en la memoria. Cuando el jeffrey se crea un objeto, se crea un nuevo objeto de función y se asigna a jeffreyes getFullName miembro, y también consume memoria durante el tiempo que jeffrey está en la memoria El problema aquí es que jeremy.getFullName es un objeto de función diferente que jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName resultados en falso; ejecute este código en http://jsfiddle.net/k9uRN/). Ambos tienen el mismo comportamiento, pero son dos objetos de funciones completamente diferentes (y por lo tanto, cada uno consume memoria). Para mayor claridad, eche un vistazo a la Figura 1:

Figura 1

Aquí ves la Jeremy y jeffrey objetos, cada uno de los cuales tiene su propio getFullName () método. Entonces, cada uno Persona objeto creado tiene su propio único getFullName () Método: cada uno de los cuales consume su propia porción de memoria. Imagina crear 100 Persona objetos: si cada uno getFullName () Método consume 4KB de memoria, luego 100 Persona Los objetos consumirían al menos 400KB de memoria. Eso puede sumarse, pero puede reducirse drásticamente usando el prototipo objeto.

Usa el prototipo

Como se mencionó anteriormente, las funciones son objetos en JavaScript. Todos los objetos de función tienen una prototipo propiedad, pero solo es útil para funciones de constructor. En resumen, el prototipo La propiedad es literalmente un prototipo para crear objetos; todo lo que se define en el prototipo de una función constructora se comparte entre todos los objetos creados por esa función constructora.

Desafortunadamente, los prototipos no están lo suficientemente estresados ​​en la educación de JavaScript..

Desafortunadamente, los prototipos no están lo suficientemente estresados ​​en la educación de JavaScript, sin embargo, son absolutamente esenciales para JavaScript porque se basa y se construye con prototipos, es un lenguaje prototípico. Incluso si nunca escribiste la palabra prototipo en su código, se están utilizando detrás de las escenas. Por ejemplo, cada método nativo basado en cadenas, como división(), substr (), o reemplazar(), se definen en Cuerda()prototipo de. Los prototipos son tan importantes para el lenguaje JavaScript que si no abrazas la naturaleza prototípica de JavaScript, estás escribiendo un código ineficiente. Considere la implementación anterior de la Persona tipo de datos: creando un Persona objeto requiere el motor de JavaScript para hacer más trabajo y asignar más memoria.

Entonces, ¿cómo se puede utilizar el prototipo ¿La propiedad hace este código más eficiente? Bueno, primero eche un vistazo al código refactorizado:

persona de la función (nombre de pila, apellido) this.firstName = firstName; this.lastName = lastName;  Person.prototype.getFullName = function () return this.firstName + "" + this.lastName; ; var jeremy = nueva persona ("Jeremy", "McPeak"), jeffrey = nueva persona ("Jeffrey", "Way");

Aquí el getFullName () La definición del método se mueve fuera del constructor hacia el prototipo. Este simple cambio tiene los siguientes efectos:

  • El constructor realiza menos trabajo y, por lo tanto, se ejecuta más rápido (18% -96% más rápido). Ejecuta la prueba en tu navegador si quieres.
  • los getFullName () El método se crea una sola vez y se comparte entre todos. Persona objetos (jeremy.getFullName === jeffrey.getFullName resultados en cierto; ejecute este código en http://jsfiddle.net/Pfkua/). Debido a esto, cada uno Persona el objeto usa menos memoria.

Vuelva a consultar la Figura 1 y observe cómo cada objeto tiene su propia getFullName () método. Ahora eso getFullName () se define en el prototipo, el diagrama de objetos cambia y se muestra en la Figura 2:

Figura 2

los Jeremy y jeffrey objetos ya no tienen su propio getFullName () método, pero el motor de JavaScript lo encontrará en Persona()prototipo de. En motores de JavaScript más antiguos, el proceso de encontrar un método en el prototipo podría incurrir en un impacto de rendimiento, pero no en los motores de JavaScript de hoy. La velocidad a la que los motores modernos encuentran métodos prototipados es extremadamente rápida..

Intimidad

Pero ¿qué pasa con la privacidad? Después de todo, este antipatrón nació de una necesidad percibida de miembros de objetos privados. Si no está familiarizado con el patrón, eche un vistazo al siguiente código:

función Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = new Foo ("¡Hola, Privacidad!"); alerta (foo.bar ()); // alertas "¡Hola, Privacidad!"

Este código define una función constructora llamada Foo (), y tiene un parámetro llamado paramOne. El valor pasado a Foo () Se almacena en una variable local llamada thisIsPrivate. Tenga en cuenta que thisIsPrivate es una variable, no una propiedad; por lo tanto, es inaccesible fuera de Foo (). También hay un método definido dentro del constructor, y se llama bar(). Porque bar() se define dentro de Foo (), tiene acceso a la thisIsPrivate variable. Así que cuando creas un Foo objeto y llamada bar(), el valor asignado a thisIsPrivate es regresado.

El valor asignado a thisIsPrivate se conserva. No se puede acceder fuera de Foo (), y por lo tanto, está protegido de la modificación exterior. Eso es genial, ¿verdad? Pues sí y no. Es comprensible por qué algunos desarrolladores desean emular la privacidad en JavaScript: puede asegurarse de que los datos de un objeto estén protegidos contra manipulación externa. Pero al mismo tiempo, introduces ineficiencia en tu código al no usar el prototipo.

Así que de nuevo, ¿qué pasa con la privacidad? Bueno, eso es simple: no lo hagas. El idioma actualmente no admite oficialmente miembros de objetos privados, aunque eso puede cambiar en una futura revisión del idioma. En lugar de utilizar cierres para crear miembros privados, la convención para denotar "miembros privados" es anteponer el identificador con un guión bajo (es decir: _la esprivada). El siguiente código reescribe el ejemplo anterior usando la convención:

función Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = function () return this._thisIsPrivate; ; var foo = new Foo ("¡Hola, Convención para denotar la privacidad!"); alerta (foo.bar ()); // alertas "¡Hola, convención para denotar la privacidad!"

No, no es privado, pero la convención de subrayado básicamente dice "no me toques". Hasta que JavaScript sea totalmente compatible con propiedades y métodos privados, ¿no preferiría tener un código más eficiente y eficaz que la privacidad? La respuesta correcta es: si!


Resumen

El lugar donde defina funciones en su código afecta el rendimiento de su aplicación; tenlo en cuenta al escribir tu código. No anide funciones dentro de una función llamada frecuentemente. Hacerlo desperdicia ciclos de CPU. En cuanto a las funciones constructoras, abraza el prototipo; El no hacerlo resulta en un código ineficiente. Después de todo, los desarrolladores escriben software para que los usuarios los utilicen, y el rendimiento de una aplicación es tan importante para la experiencia del usuario como la interfaz de usuario..