Prototipos en JavaScript
Spanish (Español) translation by Steven (you can also view the original English article)
Cuando defines una función dentro de JavaScript, viene con algunas propiedades predefinidas; uno de ellos es el prototipo ilusorio. En este artículo, detallaré qué es y por qué deberías usarlo en tus proyectos.
¿Qué es Prototype?
La propiedad prototipo es inicialmente un objeto vacío y se le pueden agregar miembros, como harías con cualquier otro objeto.
1 |
var myObject = function(name){ |
2 |
this.name = name; |
3 |
return this; |
4 |
};
|
5 |
|
6 |
console.log(typeof myObject.prototype); // object |
7 |
|
8 |
myObject.prototype.getName = function(){ |
9 |
return this.name; |
10 |
};
|
En el fragmento de arriba, hemos creado una función, pero si llamamos a myObject(), simplemente devolverá el objeto window, porque se definió dentro del alcance global. Por lo tanto, this devolverá el objeto global, ya que aún no se ha instanciado (más sobre esto más adelante).
1 |
console.log(myObject() === window); // true |
El enlace secreto
Cada objeto dentro de JavaScript tiene una propiedad "secreta".
Antes de continuar, me gustaría hablar sobre el enlace "secreto" que hace que el prototipo funcione como lo hace.
Cada objeto dentro de JavaScript tiene una propiedad "secreta" agregada cuando se define o instancia, llamado __proto__; así es como se accede a la cadena de prototipos. Sin embargo, no es una buena idea acceder a __proto__ dentro de tu aplicación, ya que no está disponible en todos los navegadores.
La propiedad __proto__ no debe confundirse con el prototipo de un objeto, ya que son dos propiedades separadas; dicho esto, van de la mano. Es importante hacer esta distinción, ya que puede resultar bastante confuso al principio. ¿Qué significa exactamente? Déjame explicarte. Cuando creamos la función myObject, estábamos definiendo un objeto de tipo Function.
1 |
console.log(typeof myObject); // function |
Para aquellos que no lo saben, Function es un objeto predefinido en JavaScript y, como resultado, tiene sus propias propiedades (por ejemplo, length y arguments) y métodos (por ejemplo, call y apply). Y sí, también tiene su propio objeto prototipo, así como el enlace secreto __proto__. Esto significa que, en algún lugar dentro del motor de JavaScript, hay un fragmento de código que podría ser similar al siguiente:
1 |
Function.prototype = { |
2 |
arguments: null, |
3 |
length: 0, |
4 |
call: function(){ |
5 |
// secret code
|
6 |
},
|
7 |
apply: function(){ |
8 |
// secret code
|
9 |
}
|
10 |
...
|
11 |
}
|
En verdad, probablemente no sería tan simplista; esto es simplemente para ilustrar cómo funciona la cadena de prototipos.
Entonces, hemos definido myObject como una función y le hemos dado un argumento, name; pero nunca establecimos propiedades, como la longitud (length) o métodos, como call. Entonces, ¿por qué funciona lo siguiente?
1 |
console.log(myObject.length); // 1 (being the amount of available arguments) |
Esto se debe a que, cuando definimos myObject, creó una propiedad __proto__ y estableció su valor en Function.prototype (ilustrado en el código anterior). Entonces, cuando accedemos a myObject.length, busca una propiedad de myObject llamada length y no encuentra ninguna; luego viaja por la cadena, a través del enlace __proto__, encuentra la propiedad y la devuelve.
Quizás te preguntes por qué length se establece en 1 y no en 0, o cualquier otro número para ese hecho. Esto se debe a que myObject es de hecho una instancia de Function.
1 |
console.log(myObject instanceof Function); // true |
2 |
console.log(myObject === Function); // false |
Cuando se crea una instancia de un objeto, la propiedad __proto__ se actualiza para apuntar al prototipo del constructor, que, en este caso, es Función.
1 |
console.log(myObject.__proto__ === Function.prototype) // true |
Además, cuando creas un nuevo objeto Function, el código nativo dentro del constructor Function contará el número de argumentos y actualizará this.length en consecuencia, que, en este caso, es 1.
Sin embargo, si creamos una nueva instancia de myObject usando la palabra clave new, __proto__ apuntará a myObject.prototype ya que myObject es el constructor de nuestra nueva instancia.
1 |
var myInstance = new myObject(“foo”); |
2 |
console.log(myInstance.__proto__ === myObject.prototype); // true |
Además de tener acceso a los métodos nativos dentro del Function.prototype, como call y apply, ahora tenemos acceso al método de myObject, getName.
1 |
console.log(myInstance.getName()); // foo |
2 |
|
3 |
var mySecondInstance = new myObject(“bar”); |
4 |
|
5 |
console.log(mySecondInstance.getName()); // bar |
6 |
console.log(myInstance.getName()); // foo |
Como puedes imaginar, esto es bastante útil, ya que se puede usar para diseñar un objeto y crear tantas instancias como sea necesario, lo que me lleva al siguiente tema.
¿Por qué es mejor usar el prototipo?
Digamos, por ejemplo, que estamos desarrollando un juego con canvas y necesitamos varios (posiblemente cientos de) objetos en la pantalla a la vez. Cada objeto requiere sus propias propiedades, como las coordenadas x e y, el width, height y muchas otras.
Podríamos hacerlo de la siguiente manera:
1 |
var GameObject1 = { |
2 |
x: Math.floor((Math.random() * myCanvasWidth) + 1), |
3 |
y: Math.floor((Math.random() * myCanvasHeight) + 1), |
4 |
width: 10, |
5 |
height: 10, |
6 |
draw: function(){ |
7 |
myCanvasContext.fillRect(this.x, this.y, this.width, this.height); |
8 |
}
|
9 |
...
|
10 |
};
|
11 |
|
12 |
var GameObject2 = { |
13 |
x: Math.floor((Math.random() * myCanvasWidth) + 1), |
14 |
y: Math.floor((Math.random() * myCanvasHeight) + 1), |
15 |
width: 10, |
16 |
height: 10, |
17 |
draw: function(){ |
18 |
myCanvasContext.fillRect(this.x, this.y, this.width, this.height); |
19 |
}
|
20 |
...
|
21 |
};
|
... hacer esto 98 más veces ...
Lo que esto hará es crear todos estos objetos dentro de la memoria, todos con definiciones separadas para métodos, como draw y cualquier otro método que se requiera. Esto ciertamente no es ideal, ya que el juego inflará la memoria JavaScript asignada a los navegadores y hará que funcione muy lentamente... o incluso dejar de responder.
Si bien esto probablemente no sucedería con solo 100 objetos, aún puede servir para ser un gran éxito en el rendimiento, ya que necesitará buscar cien objetos diferentes, en lugar de solo el objeto único prototipo.
¿Cómo utilizar el prototipo?
Para hacer que la aplicación se ejecute más rápido (y seguir las mejores prácticas), podemos (re)definir la propiedad de prototipo del GameObject; cada instancia de GameObject hará referencia a los métodos dentro de GameObject.prototype como si fueran sus propios métodos.
1 |
// define the GameObject constructor function
|
2 |
var GameObject = function(width, height) { |
3 |
this.x = Math.floor((Math.random() * myCanvasWidth) + 1); |
4 |
this.y = Math.floor((Math.random() * myCanvasHeight) + 1); |
5 |
this.width = width; |
6 |
this.height = height; |
7 |
return this; |
8 |
};
|
9 |
|
10 |
// (re)define the GameObject prototype object
|
11 |
GameObject.prototype = { |
12 |
x: 0, |
13 |
y: 0, |
14 |
width: 5, |
15 |
width: 5, |
16 |
draw: function() { |
17 |
myCanvasContext.fillRect(this.x, this.y, this.width, this.height); |
18 |
}
|
19 |
};
|
A continuación, podemos crear una instancia del GameObject 100 veces.
1 |
var x = 100, |
2 |
arrayOfGameObjects = []; |
3 |
|
4 |
do { |
5 |
arrayOfGameObjects.push(new GameObject(10, 10)); |
6 |
} while(x--); |
Ahora tenemos una matriz de 100 objetos de GameObjects, que comparten el mismo prototipo y definición del método draw, lo que ahorra drásticamente la memoria dentro de la aplicación.
Cuando llamamos al método draw, hará referencia a la misma función.
1 |
var GameLoop = function() { |
2 |
for(gameObject in arrayOfGameObjects) { |
3 |
gameObject.draw(); |
4 |
}
|
5 |
};
|
El prototipo es un objeto vivo
El prototipo de un objeto es un objeto vivo, por así decirlo. Esto simplemente significa que, si, después de crear todas nuestras instancias de GameObject, decidimos que, en lugar de dibujar un rectángulo, queremos dibujar un círculo, podemos actualizar nuestro método GameObject.prototype.draw en consecuencia.
1 |
GameObject.prototype.draw = function() { |
2 |
myCanvasContext.arc(this.x, this.y, this.width, 0, Math.PI*2, true); |
3 |
}
|
Y ahora, todas las instancias anteriores de GameObject y cualquier instancia futura dibujarán un círculo.
Actualizando prototipos de objetos nativos
Si, esto es posible. Es posible que estés familiarizado con las bibliotecas de JavaScript, como Prototype, que aprovechan este método.
Vamos a usar un ejemplo simple:
1 |
String.prototype.trim = function() { |
2 |
return this.replace(/^\s+|\s+$/g, ‘’); |
3 |
};
|
Ahora podemos acceder a esto como un método de cualquier cadena:
1 |
“ foo bar “.trim(); // “foo bar” |
Sin embargo, hay una pequeña desventaja en esto. Por ejemplo, puedes usar esto en tu aplicación; pero dentro de uno o dos años, un navegador puede implementar una versión actualizada de JavaScript que incluye un método trim nativo dentro del prototipo de String. ¡Esto significa que tu definición de trim invalidará la versión nativa! ¡Caray! Para superar esto, podemos añadir una simple comprobación antes de definir la función.
1 |
if(!String.prototype.trim) { |
2 |
String.prototype.trim = function() { |
3 |
return this.replace(/^\s+|\s+$/g, ‘’); |
4 |
};
|
5 |
}
|
Ahora, si existe, usará la versión nativa del método trim.
Como regla general, esto generalmente se considera una buena práctica evitar extender objetos nativos. Pero, como con todo, las reglas se pueden romper, si es necesario.
Conclusión
Con suerte, este artículo ha arrojado algo de luz sobre la columna vertebral de JavaScript que es un prototipo. Ahora deberías estar en camino de crear aplicaciones más eficientes.
Si tienes alguna pregunta con respecto al prototipo, házmelo saber en los comentarios, y haré todo lo posible para responderte.



