I am learning javascript and I am wondering about the concept of functions and prototypes inside an object.
My understanding of the concept
As I understand it, functions should be attached to object's prototype in order to allocate less memory.
For example (if what I said is true), both version will do the same job but second version will allocate less memory:
var collectionOfObjects = [];
for(var i=0; i<1000; i++){
collectionOfObjects.push({id: 2, sayHi: function(){console .log('hi')}})
}
//vs
function Foobar(id){
this.id = id || 0;
}
Foobar.prototype.sayHi = function(){
console.log('hi');
}
var otherCollection = [];
for(var i=0; i<1000; i++){
otherCollection.push(new Foobar());
}
Question
//this attaches sayHi function to the object instead of its prototype
var foobar = {
id: 0,
sayHi: function(){
console.log('Hi');
}
}
Since I shouldn't use __proto__
, how do I attach sayHi
function to foobar's prototype instead of foobar object? I assume that adding sayHi
to Object.prototype
isn't a good solution as it would add sayHi
function to every object.
The prototype chain is setup via the new
keyword, a class
or Object.create()
. Either way, it's set when the object is created.
So you already know one way, which is is to add functions to the prototype of a constructor function and instantiate with new
.
The modern way would be to use a class
, but you specifically mention legacy code here, so I'm going to assume that's out for purposes of this discussion.
The last way is to use Object.create()
which gives you an object with some prototype:
var myProto = {
// There is only one of this function, ever.
sayHi: function() {
console.log('hi');
}
}
var otherCollection = [];
for(var i=0; i<1000; i++){
var obj = Object.create(myProto)
obj.id = i // or other custom setup.
otherCollection.push(obj);
}
That said, in modern javascript engines having unique functions per object is not a big deal. I've worked on some pretty large javascript apps and memory usage due to the number of function instances has not ever been a performance bottleneck at all.
In modern React with functional hooks, for instance, it would be incredibly cumbersome (if not impossible in some cases) to avoid creating hundreds of new functions on every render. It's designed to be that way and it still performs very well. And if it doesn't perform well, it's never due to too many functions.
You understanding is accurate. Objects created with the obj = { ... }
notation inherit from Object.prototype
, and changing their prototype with __proto__
or its modern equivalent Object.setPrototypeOf
is especially slow in all JavaScript implementations. But there is another way to set the prototype of an object on creation.
The Object.create(proto)
function lets you create an object which inherits from proto
, so you don't have to use the confusing Foobar.prototype
syntax and the magical new
operator.
let Foobar = {
id: 0,
sayHi: function() {
console.log('Hi');
}
};
let obj = Object.create(Foobar);
obj.id = 1;
obj.sayHi(); // Hi
If you had a dozen properties to set on obj
, having to assign each property manually after the object creation would be a bit lame, but you can use the nifty Object.assign
function for that:
let Elephant = {
name: 'an elephant',
ears: 'boring',
sayHi: function() {
console.log(`Hi, I am ${this.name} and my ears are ${this.ears}.`);
}
};
let dumbo = Object.assign(Object.create(Elephant), {
name: 'Dumbo',
ears: 'AWESOME'
});
dumbo.sayHi(); // Hi, I am Dumbo and my ears are AWESOME.
If you wanted to create a bunch of objects and give them all the same 3 methods, you could therefore replace this:
let t = { name: "Tom", eat, sleep, repeat };
let g = { name: "Garfield", eat, sleep, repeat };
with this:
let protoCat = { eat, sleep, repeat };
let t = Object.assign(Object.create(protoCat), { name: "Tom" });
let g = Object.assign(Object.create(protoCat), { name: "Garfield });
It would be more practical, however, to have some kind of constructor function instead. Using Object.assign
for a single property is a bit overkill so I'll simplify this next example:
function cat(name) {
const c = Object.create(protoCat);
c.name = name;
return c;
}
let t = cat("Tom");
let t = cat("Garfield");
Note that you don't call cat
with new
, since cat builds its own object. If you tried calling cat
with the new
operator here, it would create an object which inherits from the empty cat.prototype
, but that object would eventually be discarded since cat
has a return value.
UPDATE: While my answer gives a pretty complete explanation of how prototype
works, I feel like I failed to point out that you don't have to use it at all. You could very well just assign the methods every time.
function cat(name) {
return { name, eat, sleep, repeat };
}
Sure, all your cats now hold a reference to all their methods, which might take a tiny bit of extra memory, but writing hard to maintain code to save a few bytes isn't what I'd call an optimization.
JavaScript likes to treat objects like data, in a way similar to lisp, and if you just pretend it has classes, you might end up wishing you were using another language.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With