que prototipo herencia create clases javascript performance memory-management prototype

prototipo - object.create javascript



Definir métodos por prototipo vs usar esto en el constructor-¿realmente una diferencia de rendimiento? (6)

En la nueva versión de Chrome, this.method es aproximadamente un 20% más rápido que prototype.method, pero crear un objeto nuevo es aún más lento.

Si puede reutilizar el objeto en lugar de crear siempre uno nuevo, puede ser entre un 50% y un 90% más rápido que crear objetos nuevos. Además de la ventaja de no recolección de basura, que es enorme:

http://jsperf.com/prototype-vs-this/59

En JavaScript, tenemos dos formas de hacer una "clase" y darle funciones públicas.

Método 1:

function MyClass() { var privateInstanceVariable = ''foo''; this.myFunc = function() { alert(privateInstanceVariable ); } }

Método 2:

function MyClass() { } MyClass.prototype.myFunc = function() { alert("I can''t use private instance variables. :("); }

He leído muchas veces que la gente saying que usar el Método 2 es más eficiente ya que todas las instancias comparten la misma copia de la función en lugar de que cada una obtenga la suya propia. Sin embargo, la definición de funciones a través del prototipo tiene una gran desventaja: hace que sea imposible tener variables de instancia privadas.

Aunque, en teoría, usar el Método 1 le da a cada instancia de un objeto su propia copia de la función (y así usa mucho más memoria, sin mencionar el tiempo requerido para las asignaciones) - ¿eso es lo que realmente sucede en la práctica? Parece que una optimización que los navegadores web podrían hacer fácilmente es reconocer este patrón extremadamente común, y en realidad tener todas las instancias del objeto referenciando la misma copia de funciones definidas a través de estas "funciones de constructor". Entonces solo podría dar una instancia a su propia copia de la función si se cambia explícitamente más adelante.

Cualquier idea, o mejor aún, experiencia del mundo real sobre las diferencias de rendimiento entre los dos, sería extremadamente útil.


En resumen, use el método 2 para crear propiedades / métodos que todas las instancias compartirán. Esos serán "globales" y cualquier cambio se reflejará en todas las instancias. Use el método 1 para crear propiedades / métodos específicos de la instancia.

Ojalá tuviera una referencia mejor, pero por ahora eche un vistazo a this . Puede ver cómo utilicé ambos métodos en el mismo proyecto para diferentes propósitos.

Espero que esto ayude. :)


Es posible que no haya considerado esto, pero poner el método directamente en el objeto es realmente mejor de una manera:

  1. Las invocaciones de métodos son muy ligeramente más rápidas ( jsperf ) ya que no es necesario consultar la cadena de prototipos para resolver el método.

Sin embargo, la diferencia de velocidad es casi insignificante. Además de eso, poner un método en un prototipo es mejor en dos formas más impactantes:

  1. Más rápido para crear instancias ( jsperf )
  2. Usa menos memoria

Como dijo James, esta diferencia puede ser importante si está creando instancias de miles de instancias de una clase.

Dicho esto, puedo imaginar un motor de JavaScript que reconoce que la función que está adjuntando a cada objeto no cambia entre las instancias y, por lo tanto, solo conserva una copia de la función en la memoria, con todos los métodos de instancia apuntando a la función compartida. De hecho, parece que Firefox está haciendo una optimización especial como esta pero Chrome no lo está haciendo.

APARTE:

Tiene razón en que es imposible acceder a variables de instancia privadas desde métodos internos en prototipos. Así que supongo que la pregunta que debes hacerte es: ¿valoras la posibilidad de hacer que las variables de instancia sean verdaderamente privadas en vez de utilizar herencia y creación de prototipos? Personalmente creo que hacer variables verdaderamente privadas no es tan importante y solo usaría el prefijo del subrayado (por ejemplo, "this._myVar") para indicar que, aunque la variable es pública, debería considerarse privada. Dicho esto, en ES6, ¡aparentemente hay una forma de tener los dos mundos!


Esta respuesta se debe considerar una expansión del resto de las respuestas que completan los puntos faltantes. Se incorporan experiencia personal y puntos de referencia.

En lo que respecta a mi experiencia, uso constructores para construir literalmente mis objetos religiosamente, ya sean métodos privados o no. La razón principal es que cuando comencé, ese era el enfoque más fácil para mí, así que no es una preferencia especial. Pudo haber sido tan simple como que me gusta la encapsulación visible y los prototipos son un poco incorpóreos. Mis métodos privados también se asignarán como variables en el alcance. Aunque este es mi hábito y lo mantiene todo bien contenido, no siempre es el mejor hábito y a veces choco contra las paredes. Además de los escenarios extravagantes con autoensamblaje altamente dinámico según los objetos de configuración y el diseño del código, en mi opinión, tiende a ser el enfoque más débil, especialmente si el rendimiento es una preocupación. Saber que las partes internas son privadas es útil, pero puede lograrlo por otros medios con la disciplina adecuada. A menos que el rendimiento sea una consideración seria, utilice lo que funcione mejor de lo contrario para la tarea en cuestión.

  1. El uso de herencia de prototipos y una convención para marcar elementos como privados hace que la depuración sea más fácil ya que puede atravesar fácilmente el gráfico de objeto desde la consola o el depurador. Por otro lado, una convención de ese tipo hace que la ofuscación sea algo más difícil y facilita que otros utilicen sus propios scripts en su sitio. Esta es una de las razones por las cuales el enfoque de alcance privado ganó popularidad. No es una verdadera seguridad, sino que agrega resistencia. Desafortunadamente, mucha gente todavía cree que es una manera genuina de programar JavaScript seguro. Como los depuradores se han vuelto realmente buenos, la ofuscación del código toma su lugar. Si busca fallas de seguridad en las que el cliente tiene demasiado, es un patrón de diseño con el que debería estar atento.
  2. Una convención le permite tener propiedades protegidas con poco alboroto. Eso puede ser una bendición y una maldición. Facilita algunos problemas de herencia ya que es menos restrictivo. Todavía tiene el riesgo de colisión o aumento de la carga cognitiva al considerar dónde más se puede acceder a una propiedad. Los objetos que se autoensamblan te permiten hacer cosas extrañas donde puedes solucionar una serie de problemas de herencia, pero pueden ser poco convencionales. Mis módulos tienden a tener una estructura interna rica donde las cosas no se extraen hasta que la funcionalidad se necesita en otro lugar (compartida) o expuesta a menos que sea necesaria externamente. El patrón constructor tiende a generar módulos sofisticados independientes más que simplemente objetos fragmentarios. Si quieres eso, entonces está bien. De lo contrario, si desea una estructura y diseño OOP más tradicional, entonces probablemente sugeriría regular el acceso por convención. En mis escenarios de uso, el OOP complejo no suele justificarse y los módulos funcionan bien.
  3. Todas las pruebas aquí son mínimas. En el uso del mundo real, es probable que los módulos sean más complejos, lo que hace que el impacto sea mucho mayor de lo que indicarán las pruebas aquí. Es bastante común tener una variable privada con múltiples métodos trabajando en ella y cada uno de esos métodos agregará más sobrecarga en la inicialización que no obtendrás con la herencia del prototipo. En la mayoría de los casos, no importa porque solo unas pocas instancias de tales objetos flotan alrededor, aunque acumulativamente podría sumarse.
  4. Existe una suposición de que los métodos prototipo son más lentos para llamar debido a la búsqueda de prototipos. No es una suposición injusta, hice lo mismo hasta que lo probé. En realidad, es complejo y algunas pruebas sugieren que el aspecto es trivial. Entre, prototype.m = f , this.m = f y this.m = function... este último se desempeña significativamente mejor que los dos primeros que tienen el mismo rendimiento. Si la búsqueda de prototipos solo fuera un problema importante, las dos últimas funciones en su lugar tendrían un rendimiento significativamente mayor. En cambio, algo extraño sucede al menos en lo que respecta a Canary. Es posible que las funciones se optimicen de acuerdo con lo que son miembros. Una multitud de consideraciones de rendimiento entran en juego. También tiene diferencias para el acceso a los parámetros y el acceso variable.
  5. Capacidad de memoria. No está bien discutido aquí. Una suposición que puede asumir de antemano que probablemente sea cierta es que la herencia del prototipo generalmente será mucho más eficiente en cuanto a la memoria y, de acuerdo con mis pruebas, es en general. Cuando construyes tu objeto en tu constructor puedes asumir que cada objeto probablemente tendrá su propia instancia de cada función en lugar de compartida, un mapa de propiedades más grande para sus propias propiedades personales y probablemente algún sobrecarga para mantener abierto el alcance del constructor. Las funciones que operan en el ámbito privado exigen una memoria extrema y desproporcionada. En muchos escenarios, la diferencia proporcional en la memoria será mucho más significativa que la diferencia proporcional en los ciclos de la CPU.
  6. Gráfico de memoria También puede atascar el motor y hacer que el GC sea más costoso. Los perfiladores tienden a mostrar el tiempo pasado en GC en estos días. No es solo un problema cuando se trata de asignar y liberar más. También creará un gráfico de objeto más grande para recorrer y cosas así para que el GC consuma más ciclos. Si crea un millón de objetos y luego apenas los toca, dependiendo del motor, es posible que tenga un impacto de rendimiento ambiental superior al esperado. He demostrado que esto al menos hace que el gc funcione durante más tiempo cuando se eliminan los objetos. Eso es allí tiende a ser una correlación con la memoria utilizada y el tiempo que lleva a GC. Sin embargo, hay casos en los que el tiempo es el mismo independientemente de la memoria. Esto indica que la composición del gráfico (capas de direccionamiento indirecto, recuento de elementos, etc.) tiene más impacto. Eso no es algo que siempre sea fácil de predecir.
  7. No mucha gente usa prototipos encadenados extensivamente, yo mismo lo tengo que admitir. Las cadenas de prototipos pueden ser costosas en teoría. Alguien lo hará pero no he medido el costo. Si, en cambio, construyes tus objetos completamente en el constructor y luego tienes una cadena de herencia, cada constructor llama un constructor padre sobre sí mismo, en teoría el acceso a los métodos debería ser mucho más rápido. Por otro lado, puedes lograr el equivalente si es importante (como aplanar los prototipos en la cadena de antepasados) y no te molesta romper cosas como hasOwnProperty, quizás instanceof, etc., si realmente lo necesitas. En cualquier caso, las cosas se vuelven complejas una vez que estás en este camino cuando se trata de hacks de rendimiento. Probablemente terminarás haciendo cosas que no deberías estar haciendo.
  8. Muchas personas no usan directamente ninguno de los enfoques que has presentado. En cambio, crean sus propias cosas utilizando objetos anónimos que permiten compartir el método de cualquier forma (mixins, por ejemplo). También hay varios marcos que implementan sus propias estrategias para organizar módulos y objetos. Estos son enfoques personalizados basados ​​en convenciones. Para la mayoría de las personas y para usted, su primer desafío debe ser la organización en lugar del rendimiento. Esto a menudo es complicado, ya que Javascript ofrece muchas formas de lograr cosas en comparación con los lenguajes o las plataformas con soporte más explícito de OOP / espacio de nombres / módulos. En lo que respecta al rendimiento, diría que para evitar las principales dificultades antes que nada.
  9. Hay un nuevo tipo de Símbolo que se supone que funciona para las variables y métodos privados. Hay varias maneras de usar esto y plantea una serie de preguntas relacionadas con el rendimiento y el acceso. En mis pruebas, el rendimiento de Symbols no fue excelente en comparación con todo lo demás, pero nunca los probé a fondo.

Descargo de responsabilidad:

  1. Hay muchas discusiones sobre el rendimiento y no siempre hay una respuesta permanentemente correcta para esto ya que los escenarios de uso y los motores cambian. Siempre perfile pero también mida siempre en más de una forma ya que los perfiles no siempre son precisos ni confiables. Evite un esfuerzo significativo en la optimización a menos que definitivamente haya un problema demostrable.
  2. Probablemente sea mejor incluir controles de rendimiento para áreas sensibles en pruebas automatizadas y ejecutar cuando los navegadores se actualicen.
  3. Recuerde que a veces la vida de la batería es importante y el rendimiento perceptible. La solución más lenta podría resultar más rápida después de ejecutar un compilador de optimización en ella (IE, un compilador podría tener una mejor idea de cuándo se accede a las variables de ámbito restringido que las propiedades marcadas como privadas por convención). Considere backend como node.js. Esto puede requerir una mejor latencia y rendimiento de lo que a menudo encontraría en el navegador. La mayoría de la gente no tendrá que preocuparse por estas cosas con algo así como la validación de un formulario de registro, pero la cantidad de escenarios diversos donde tales cosas podrían importar es cada vez mayor.
  4. Debe tener cuidado con las herramientas de seguimiento de asignación de memoria para persistir en el resultado. En algunos casos en los que no regresé y persistí los datos, se optimizó por completo o la frecuencia de muestreo no fue suficiente entre instancias / sin referencias, dejándome rascándome la cabeza al saber cómo una matriz se inicializó y llenó a un millón registrado como 3.4KiB en el perfil de asignación.
  5. En el mundo real, en la mayoría de los casos, la única forma de optimizar realmente una aplicación es escribirla en primer lugar para que pueda medirla. Hay docenas o cientos de factores que pueden entrar en juego si no son miles en un escenario dado. Los motores también hacen cosas que pueden conducir a características de rendimiento asimétricas o no lineales. Si define funciones en un constructor, pueden ser funciones de flecha o tradicionales, cada una se comporta de manera diferente en ciertas situaciones y no tengo idea acerca de los otros tipos de funciones. Las clases tampoco se comportan igual en términos de rendimiento para constructores prototipados que deberían ser equivalentes. También debe ser muy cuidadoso con los puntos de referencia. Las clases con prototipos pueden haber diferido la inicialización de varias maneras, especialmente si también ha creado prototipos de sus propiedades (consejo, no lo haga). Esto significa que puede subestimar el costo de inicialización y exagerar el acceso / costo de mutación de la propiedad. También he visto indicios de optimización progresiva. En estos casos he llenado una matriz grande con instancias de objetos que son idénticos y, a medida que aumenta el número de instancias, los objetos parecen estar optimizados incrementalmente para la memoria hasta un punto donde el resto es el mismo. También es posible que esas optimizaciones también puedan afectar significativamente el rendimiento de la CPU. Estas cosas dependen en gran medida no solo del código que se escribe, sino de lo que sucede en el tiempo de ejecución, como el número de objetos, la variación entre los objetos, etc.

Solo hace la diferencia cuando creas muchas instancias. De lo contrario, el rendimiento de llamar a la función miembro es exactamente el mismo en ambos casos.

Creé un caso de prueba en jsperf para demostrar esto:

http://jsperf.com/prototype-vs-this/10


Ver http://jsperf.com/prototype-vs-this

Declarar sus métodos a través del prototipo es más rápido, pero si esto es relevante es discutible.

Si tienes un cuello de botella de rendimiento en tu aplicación, es poco probable que sea así, a menos que estés instanciando más de 10000 objetos en cada paso de una animación arbitraria, por ejemplo.

Si el rendimiento es una preocupación seria, y le gustaría optimizar al mínimo, entonces sugeriría que se declare por medio de un prototipo. De lo contrario, simplemente use el patrón que tenga más sentido para usted.

Añadiré que, en JavaScript, existe una convención de propiedades de prefijación que deben verse como privadas con un guión bajo (por ejemplo, _process() ). La mayoría de los desarrolladores comprenderán y evitarán estas propiedades, a menos que estén dispuestos a renunciar al contrato social, pero en ese caso es mejor que no les atienda. Lo que quiero decir es que: probablemente no necesites verdaderas variables privadas ...