When you define a function within JavaScript, it comes with a few pre-defined properties; one of these is the illusive prototype. In this article, I’ll detail what it is, and why you should use it in your projects.
What is Prototype?
The prototype property is initially an empty object, and can have members added to it – as you would any other object.
var myObject = function(name){ this.name = name; return this; }; console.log(typeof myObject.prototype); // object myObject.prototype.getName = function(){ return this.name; };
In the snippet above, we’ve created a function, but if we call myObject()
, it will simply return the window
object, because it was defined within the global scope. this
will therefore return the global object, as it has not yet been instantiated (more on this later).
console.log(myObject() === window); // true
The Secret Link
Every object within JavaScript has a “secret” property.
Before we continue, I’d like to discuss the “secret” link that makes prototype work the way it does.
Every object within JavaScript has a “secret” property added to it when it is defined or instantiated, named __proto__
; this is how the prototype chain is accessed. However, it is not a good idea to access __proto__
within your application, as it is not available in all browsers.
The __proto__
property shouldn’t be confused with an object’s prototype, as they are two separate properties; that said, they do go hand in hand. It’s important to make this distinction, as it can be quite confusing at first! What does this mean exactly? Let me explain. When we created the myObject
function, we were defining an object of type Function
.
console.log(typeof myObject); // function
For those unaware, Function
is a predefined object in JavaScript, and, as a result, has its own properties (e.g. length
and arguments
) and methods (e.g. call
and apply
). And yes, it, too, has its own prototype object, as well as the secret __proto__
link. This means that, somewhere within the JavaScript engine, there is a bit of code that could be similar to the following:
Function.prototype = { arguments: null, length: 0, call: function(){ // secret code }, apply: function(){ // secret code } ... }
In truth, it probably wouldn’t be quite so simplistic; this is merely to illustrate how the prototype chain works.
So we have defined myObject
as a function and given it one argument, name
; but we never set any properties, such as length
or methods, such as call
. So why does the following work?
console.log(myObject.length); // 1 (being the amount of available arguments)
This is because, when we defined myObject
, it created a __proto__
property and set its value to Function.prototype
(illustrated in the code above). So, when we access myObject.length
, it looks for a property of myObject
called length
and doesn’t find one; it then travels up the chain, via the __proto__ link
, finds the property and returns it.
You might be wondering why length
is set to 1
and not 0
– or any other number for that fact. This is because myObject
is in fact an instance of Function
.
console.log(myObject instanceof Function); // true console.log(myObject === Function); // false
When an instance of an object is created, the __proto__
property is updated to point to the constructor’s prototype, which, in this case, is Function
.
console.log(myObject.__proto__ === Function.prototype) // true
Additionally, when you create a new Function
object, the native code inside the Function
constructor will count the number of arguments and update this.length
accordingly, which, in this case, is 1
.
If, however, we create a new instance of myObject
using the new
keyword, __proto__
will point to myObject.prototype
as myObject
is the constructor of our new instance.
var myInstance = new myObject(“foo”); console.log(myInstance.__proto__ === myObject.prototype); // true
In addition to having access to the native methods within the Function
.prototype, such as call
and apply
, we now have access to myObject
’s method, getName
.
console.log(myInstance.getName()); // foo var mySecondInstance = new myObject(“bar”); console.log(mySecondInstance.getName()); // bar console.log(myInstance.getName()); // foo
As you can imagine, this is quite handy, as it can be used to blueprint an object, and create as many instances as needed – which leads me onto the next topic!
Why is Using Prototype Better?
Say, for instance, that we are developing a canvas game and need several (possibly hundreds of) objects on the screen at once. Each object requires its own properties, such as x
and y
coordinates, width
,height
, and many others.
We might do it as follows:
var GameObject1 = { x: Math.floor((Math.random() * myCanvasWidth) + 1), y: Math.floor((Math.random() * myCanvasHeight) + 1), width: 10, height: 10, draw: function(){ myCanvasContext.fillRect(this.x, this.y, this.width, this.height); } ... }; var GameObject2 = { x: Math.floor((Math.random() * myCanvasWidth) + 1), y: Math.floor((Math.random() * myCanvasHeight) + 1), width: 10, height: 10, draw: function(){ myCanvasContext.fillRect(this.x, this.y, this.width, this.height); } ... };
… do this 98 more times …
What this will do is create all these objects within memory – all with separate definitions for methods, such as draw
and whatever other methods may be required. This is certainly not ideal, as the game will bloat the browsers allocated JavaScript memory, and make it run very slowly… or even stop responding.
While this probably wouldn’t happen with only 100 objects, it still can serve to be quite a performance hit, as it will need to look up one hundred different objects, rather than just the single prototype
object.
How to Use Prototype
To make the application run faster (and follow best practices), we can (re)define the prototype property of the GameObject
; every instance of GameObject
will then reference the methods within GameObject.prototype
as if they were their own methods.
// define the GameObject constructor function var GameObject = function(width, height) { this.x = Math.floor((Math.random() * myCanvasWidth) + 1); this.y = Math.floor((Math.random() * myCanvasHeight) + 1); this.width = width; this.height = height; return this; }; // (re)define the GameObject prototype object GameObject.prototype = { x: 0, y: 0, width: 5, width: 5, draw: function() { myCanvasContext.fillRect(this.x, this.y, this.width, this.height); } };
We can then instantiate the GameObject 100 times.
var x = 100, arrayOfGameObjects = []; do { arrayOfGameObjects.push(new GameObject(10, 10)); } while(x--);
Now we have an array of 100 GameObjects, which all share the same prototype and definition of the draw
method, which drastically saves memory within the application.
When we call the draw
method, it will reference the exact same function.
var GameLoop = function() { for(gameObject in arrayOfGameObjects) { gameObject.draw(); } };
Prototype is a Live Object
An object’s prototype is a live object, so to speak. This simply means that, if, after we create all our GameObject instances, we decide that, instead of drawing a rectangle, we want to draw a circle, we can update our GameObject.prototype.draw
method accordingly.
GameObject.prototype.draw = function() { myCanvasContext.arc(this.x, this.y, this.width, 0, Math.PI*2, true); }
And now, all the previous instances of GameObject
and any future instances will draw a circle.
Updating Native Objects Prototypes
Yes, this is possible. You may be familiar with JavaScript libraries, such as Prototype, which take advantage of this method.
Let’s use a simple example:
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ‘’); };
We can now access this as a method of any string:
“ foo bar “.trim(); // “foo bar”
There is a minor downside to this, however. For example, you may use this in your application; but a year or two down the road, a browser may implement an updated version of JavaScript that includes a native trim
method within the String
‘s prototype. This means that your definition of trim
will override the native version! Yikes! To overcome this, we can add a simple check before defining the function.
if(!String.prototype.trim) { String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ‘’); }; }
Now, if it exists, it will use the native version of the trim
method.
As a rule of thumb, it’s generally considered a best practice to avoid extending native objects. But, as with anything, rules can be broken, if needed.
Conclusion
Hopefully, this article has shed some light on the backbone of JavaScript that is prototype. You should now be on your way to creating more efficient applications.
If you have any questions regarding prototype, let me know in the comments, and I’ll do my best to answer them.
No hay comentarios:
Publicar un comentario