Buenas tardes amigos! Se lanzó el curso
"Seguridad de los sistemas de información" , en relación con esto, compartimos con ustedes la parte final del artículo "Fundamentos de los motores JavaScript: optimización de prototipos", cuya primera parte se puede leer
aquí .
También le recordamos que la publicación actual es una continuación de estos dos artículos:
“Fundamentos de los motores de JavaScript: formularios generales y almacenamiento en caché en línea. Parte 1 " ,
" Conceptos básicos de los motores de JavaScript: formularios generales y almacenamiento en caché en línea. Parte 2 " .
Programación de clases y prototipos.Ahora que sabemos cómo obtener acceso rápido a las propiedades de los objetos de JavaScript, podemos echar un vistazo a la estructura más compleja de las clases de JavaScript. Así es como se ve la sintaxis de la clase en JavaScript:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } }
Aunque esto parece un concepto relativamente nuevo para JavaScript, es solo "azúcar sintáctico" para la programación prototipo que siempre se ha utilizado en JavaScript:
function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; };
Aquí asignamos la propiedad
getX
objeto
getX
. Esto funcionará igual que con cualquier otro objeto, ya que los prototipos en JavaScript son los mismos objetos. En lenguajes de programación de prototipos como JavaScript, se accede a los métodos a través de prototipos, mientras que los campos se almacenan en instancias específicas.
Echemos un vistazo más de cerca a lo que sucede cuando creamos una nueva instancia de
Bar
, que llamaremos
foo
.
const foo = new Bar(true);
Una instancia creada con este código tiene un formulario con una sola propiedad
'x'
. El prototipo
foo
es
Bar.prototype
, que pertenece a la clase
Bar
.

Este
Bar.prototype
tiene una forma de sí mismo que contiene la única propiedad
'getX'
, cuyo valor está determinado por la función
'getX'
, que cuando se llama devuelve
this.x
El prototipo
Bar.prototype
es
Object.prototype
, que forma parte del lenguaje JavaScript.
Object.prototype
es la raíz del árbol prototipo, mientras que su prototipo es
null
.

Cuando crea una nueva instancia de la misma clase, ambas instancias tienen la misma forma, como ya entendimos. Ambas instancias apuntarán al mismo objeto
Bar.prototype
.
Acceder a las propiedades del prototipoBueno, ahora sabemos lo que sucede cuando definimos una clase y creamos una nueva instancia. Pero, ¿qué sucede si llamamos al método en la instancia, como hicimos en el siguiente ejemplo?
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();
Puede considerar cualquier llamada a un método como dos pasos separados:
const x = foo.getX();
El primer paso es cargar el método, que en realidad es una propiedad del prototipo (cuyo valor es una función). El segundo paso es llamar a una función con una instancia, por ejemplo, el valor de
this
. Echemos un vistazo más de cerca al primer paso donde se
getX
método
getX
desde la instancia
foo
.

El motor inicia una instancia de
foo
y se da cuenta de que la forma
foo
no tiene
'getX'
, por lo que debe atravesar la cadena de prototipos para encontrarla. Llegamos a
Bar.prototype
, miramos la forma del prototipo, vemos que tiene la propiedad
'getX'
en desplazamiento cero. Buscamos el valor en este desplazamiento en
Bar.prototype
y encontramos la
JSFunction getX
que estábamos buscando.
La flexibilidad de JavaScript permite que los enlaces de cadena prototipo cambien, por ejemplo:
const foo = new Bar(true); foo.getX();
En este ejemplo, llamamos
foo.getX()
dos veces, pero cada vez tiene significados y resultados completamente diferentes. Es por eso que, a pesar del hecho de que los prototipos son solo objetos en JavaScript, acelerar el acceso a las propiedades de un prototipo es una tarea aún más importante para los motores de JavaScript que acelerar su propio acceso a las propiedades de los objetos normales.
En la práctica diaria, cargar propiedades de prototipo es una operación bastante común: ¡esto sucede cada vez que llama a un método!
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();
Anteriormente, hablamos sobre cómo los motores optimizan la carga de propiedades regulares mediante el uso de formularios y cachés en línea. ¿Cómo puedo optimizar la carga de propiedades de prototipo para objetos de la misma forma? Desde arriba vimos cómo se cargan las propiedades.

Para hacer esto rápidamente con descargas repetidas en este caso particular, necesita saber las siguientes tres cosas:
- La forma
foo
no contiene 'getX'
y no ha cambiado. Esto significa que nadie ha cambiado el objeto foo agregando o quitando una propiedad o cambiando uno de los atributos de la propiedad. - El prototipo foo sigue siendo el prototipo original de
Bar.prototype
. Entonces, nadie cambió el prototipo usando Object.setPrototypeOf()
o asignándolo a la propiedad especial _proto_
. - El formulario
Bar.prototype
contiene 'getX'
y no ha cambiado. Esto significa que nadie ha cambiado Bar.prototype
agregando o eliminando una propiedad o cambiando uno de los atributos de la propiedad.
En el caso general, esto significa que debe realizar una comprobación de la instancia en sí y dos comprobaciones más para cada prototipo hasta el prototipo que contiene la propiedad deseada. Las comprobaciones 1 + 2N, donde N es el número de prototipos utilizados, no suena tan mal en este caso, ya que la cadena de prototipos es relativamente poco profunda. Sin embargo, los motores a menudo tienen que lidiar con cadenas de prototipos mucho más largas, como es el caso de las clases DOM regulares. Por ejemplo:
const anchor = document.createElement('a');
Tenemos un
HTMLAnchorElement
y llamamos al método
getAttribute()
. ¡La cadena para este elemento simple ya incluye 6 prototipos! La mayoría de los métodos DOM que nos interesan no están en el prototipo
HTMLAnchorElement
, sino en algún lugar de la cadena.

El método
getAttribute()
está en
Element.prototype
. Esto significa que cada vez que llamamos a
anchor.getAttribute()
, el motor de JavaScript necesita:
- Compruebe que
'getAttribute'
no 'getAttribute'
un objeto de anchor
per se; - Verifique que el prototipo final sea
HTMLAnchorElement.prototype
; - Confirme la ausencia de
'getAttribute'
allí; - Verifique que el próximo prototipo sea
HTMLElement.prototype
; - Confirme la ausencia de
'getAttribute'
; - Verifique que el próximo prototipo sea
Element.prototype
; - Compruebe que
'getAttribute'
presente en él.
Un total de 7 cheques. Dado que este tipo de código es bastante común en la web, los motores utilizan varios trucos para reducir la cantidad de comprobaciones necesarias para cargar las propiedades del prototipo.
Volviendo a un ejemplo anterior en el que hicimos solo tres verificaciones cuando solicitamos
'getX'
para
foo
:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX;
Para cada objeto que se produce antes del prototipo que contiene la propiedad deseada, es necesario verificar los formularios para ver la ausencia de esta propiedad. Sería bueno si pudiéramos reducir el número de cheques presentando el cheque prototipo como un cheque por la ausencia de una propiedad. En esencia, esto es exactamente lo que hacen los motores con un simple truco: en lugar de almacenar el enlace prototipo a la propia instancia, los motores lo almacenan en forma.

Cada forma indica un prototipo. Esto significa que cada vez que el prototipo cambia, el motor se mueve a una nueva forma. Ahora necesitamos verificar solo la forma del objeto para confirmar la ausencia de ciertas propiedades, así como proteger el enlace prototipo (proteger el enlace prototipo).
Con este enfoque, podemos reducir la cantidad de verificaciones requeridas de 2N + 1 a 1 + N para acelerar el acceso. Esta sigue siendo una operación bastante costosa, ya que sigue siendo una función lineal del número de prototipos en la cadena. Los motores utilizan varios trucos para reducir aún más el número de comprobaciones a un cierto valor constante, especialmente en el caso de la carga secuencial de las mismas propiedades.
Celdas de validezEl V8 procesa formas prototipo específicamente para este propósito. Cada prototipo tiene una forma única que no se comparte con otros objetos (en particular, con otros prototipos), y cada una de estas formas de prototipo tiene un
ValidityCell
especial asociado con él.

Este
ValidityCell
desactiva cada vez que alguien cambia el prototipo asociado con él o cualquier otro prototipo que se encuentre encima. Veamos como funciona.
Para acelerar las descargas de prototipos posteriores, V8 coloca el caché en línea en una ubicación de cuatro campos:

Cuando la caché en línea se calienta la primera vez que se ejecuta el código, V8 recuerda el desplazamiento en el que se encontró la propiedad en el prototipo, este prototipo (por ejemplo,
Bar.prototype
), el formulario de instancia (en nuestro caso, el formulario
foo
), y también une el
ValidityCell
actual al prototipo recibido desde la instancia del formulario (en nuestro caso, se toma el
Bar.prototype
.).
La próxima vez que use el caché en línea, el motor debe verificar el formulario de instancia y
ValidityCell
. Si aún es válido, el motor usa directamente el desplazamiento en el prototipo, omitiendo los pasos de búsqueda adicionales.

Al cambiar el prototipo, se resalta un nuevo formulario y se deshabilita la celda anterior
ValidityCell
. Debido a esto, la caché en línea se omite la próxima vez que se inicia, lo que conduce a un bajo rendimiento.
Volvamos al ejemplo con el elemento DOM. Cada cambio en
Object.prototype
no solo invalida las memorias caché en línea para
Object.prototype
, sino también para cualquier prototipo en la cadena debajo de él, incluidos
EventTarget.prototype
,
Node.prototype
,
Element.prototype
, etc., hasta
HTMLAnchorElement.prototype
sí.

De hecho, modificar
Object.prototype
mientras se ejecuta el código es una pérdida de rendimiento terrible. ¡No hagas esto!
Veamos un ejemplo específico para comprender mejor cómo funciona esto. Digamos que tenemos una clase
Bar
y una función
loadX
que llama a un método en objetos de tipo
Bar
. Llamamos a la función
loadX
varias veces con instancias de la misma clase.
class Bar { } function loadX(bar) { return bar.getX();
El caché en línea en
loadX
ahora apunta a
Bar.prototype
para
Bar.prototype
. Si luego modifica el
Object.prototype
(
Object.prototype
), que es la raíz de todos los prototipos en JavaScript,
ValidityCell
se invalida y la próxima vez no se utilizarán cachés en línea existentes, lo que
Object.prototype
un rendimiento deficiente.
Cambiar
Object.prototype
siempre es una mala idea, ya que invalida cualquier caché en línea para prototipos cargados en el momento del cambio. Aquí hay un ejemplo de cómo NO hacerlo:
Object.prototype.foo = function() { };
Estamos ampliando
Object.prototype
, que invalida todos los cachés de prototipos en línea cargados por el motor en este momento. Luego ejecutaremos un código que utiliza el método descrito por nosotros. El motor tendrá que comenzar desde el principio y configurar cachés en línea para cualquier acceso a la propiedad del prototipo. Y luego, finalmente, "limpiar" y eliminar el método prototipo que agregamos anteriormente.
¿Crees que limpiar es una buena idea, verdad? Bueno, en este caso, ¡empeorará aún más la situación! La eliminación de propiedades cambia
Object.prototype
, por lo que todas las memorias caché en línea se deshabilitan nuevamente, y el motor tiene que comenzar a trabajar desde el principio nuevamente.
Para resumir . A pesar del hecho de que los prototipos son solo objetos, los motores JavaScript los procesan especialmente para optimizar el rendimiento de las búsquedas de métodos por prototipos.
¡Deja en paz los prototipos! O si realmente necesita lidiar con ellos, ¡hágalo antes de ejecutar el código, para que al menos no invalide todos los intentos de optimizar su código durante su ejecución!
Resumir
Aprendimos cómo JavaScript almacena objetos y clases, y cómo los formularios, los cachés en línea y las celdas de validez ayudan a optimizar las operaciones del prototipo. En base a este conocimiento, entendimos cómo mejorar el rendimiento desde un punto de vista práctico: ¡no toque los prototipos! (o si realmente lo necesita, hágalo antes de ejecutar el código).
←
La primera parte¿Le fue útil esta serie de publicaciones? Escribe en los comentarios.