- The Node Craftsman Book
- Manuel Kiessling
- 935字
- 2021-07-02 23:36:53
Using prototyping to efficiently share behaviour between objects
As stated there, while in class-based programming the class is the place to put functions that all objects will share, in prototype-based programming, the place to put these functions is the object which acts as the prototype for our objects at hand.
But where is the object that is the prototype of our myCar objects – we didn't create one!
It has been implicitly created for us, and is assigned to the Car.prototype property (in case you wondered, JavaScript functions are objects too, and they therefore have properties).
Here is the key to sharing functions between objects: Whenever we call a function on an object, the JavaScript interpreter tries to find that function within the queried object. But if it doesn't find the function within the object itself, it asks the object for the pointer to its prototype, then goes to the prototype, and asks for the function there. If it is found, it is then executed.
This means that we can create myCar objects without any functions, create the honk function in their prototype, and end up having myCar objects that know how to honk – because every time the interpreter tries to execute the honk function on one of the myCar objects, it will be redirected to the prototype, and execute the honk function which is defined there.
Here is how this setup can be achieved:
var Car = function() {}; Car.prototype.honk = function() { console.log('honk honk'); }; var myCar1 = new Car(); var myCar2 = new Car(); myCar1.honk(); // executes Car.prototype.honk() and outputs "honk honk" myCar2.honk(); // executes Car.prototype.honk() and outputs "honk honk"
Our constructor is now empty, because for our very simple cars, no additional setup is necessary.
Because both myCars are created through this constructor, their prototype points to Car.prototype – executing myCar1.honk() and myCar2.honk() always results in Car.prototype.honk() being executed.
Let's see what this enables us to do. In JavaScript, objects can be changed at runtime. This holds true for prototypes, too. Which is why we can change the honk behaviour of all our cars even after they have been created:
var Car = function() {}; Car.prototype.honk = function() { console.log('honk honk'); }; var myCar1 = new Car(); var myCar2 = new Car(); myCar1.honk(); // executes Car.prototype.honk() and outputs "honk honk" myCar2.honk(); // executes Car.prototype.honk() and outputs "honk honk" Car.prototype.honk = function() { console.log('meep meep'); }; myCar1.honk(); // executes Car.prototype.honk() and outputs "meep meep" myCar2.honk(); // executes Car.prototype.honk() and outputs "meep meep"
Of course, we can also add additional functions at runtime:
var Car = function() {}; Car.prototype.honk = function() { console.log('honk honk'); }; var myCar1 = new Car(); var myCar2 = new Car(); Car.prototype.drive = function() { console.log('vrooom...'); }; myCar1.drive(); // executes Car.prototype.drive() and outputs "vrooom..." myCar2.drive(); // executes Car.prototype.drive() and outputs "vrooom..."
But we could even decide to treat only one of our cars differently:
var Car = function() {}; Car.prototype.honk = function() { console.log('honk honk'); }; var myCar1 = new Car(); var myCar2 = new Car(); myCar1.honk(); // executes Car.prototype.honk() and outputs "honk honk" myCar2.honk(); // executes Car.prototype.honk() and outputs "honk honk" myCar2.honk = function() { console.log('meep meep'); }; myCar1.honk(); // executes Car.prototype.honk() and outputs "honk honk" myCar2.honk(); // executes myCar2.honk() and outputs "meep meep"
It's important to understand what happens behind the scenes in this example. As we have seen, when calling a function on an object, the interpreter follows a certain path to find the actual location of that function.
While for myCar1, there still is no honk function within that object itself, that no longer holds true for myCar2. When the interpreter calls myCar2.honk(), there now is a function within myCar2 itself. Therefore, the interpreter no longer follows the path to the prototype of myCar2, and executes the function within myCar2 instead.
That's one of the major differences to class-based programming: while objects are relatively rigid for example, in Java, where the structure of an object cannot be changed at runtime, in JavaScript, the prototype-based approach links objects of a certain class more loosely together, which allows to change the structure of objects at any time.
Also, note how sharing functions through the constructor's prototype is way more efficient than creating objects that all carry their own functions, even if they are identical. As previously stated, the engine doesn't know that these functions are meant to be identical, and it has to allocate memory for every function in every object. This is no longer true when sharing functions through a common prototype – the function in question is placed in memory exactly once, and no matter how many myCar objects we create, they don't carry the function themselves, they only refer to their constructor, in whose prototype the function is found.
To give you an idea of what this difference can mean, here is a very simple comparison. The first example creates 1,000,000 objects that all have the function directly attached to them:
var C = function() { this.f = function(foo) { console.log(foo); }; }; var a = []; for (var i = 0; i < 1000000; i++) { a.push(new C()); }
In Google Chrome, this results in a heap snapshot size of 328 MB. Here is the same example, but now the function is shared through the constructor's prototype:
var C = function() {}; C.prototype.f = function(foo) { console.log(foo); }; var a = []; for (var i = 0; i < 1000000; i++) { a.push(new C()); }
This time, the size of the heap snapshot is only 17 MB, that is, only about 5% of the non-efficient solution.