Lectures 53 through 56 of JavaScript: Understanding the Weird Parts discuss more about the creation and manipulation of objects under the hood. I’d seen different aspects of this material previously, which gave me a different take on how to build up objects using prototype inheritance, but these lectures took the ideas apart and put them together in a whole new way.
Let me start by describing what I’d understood previously, and how I did so in terms of OOP formations I’ve seen in previous lives as a Pascal and then C++ programmer for many, many years.
Let’s start by looking at an object I build up using prototypes in the Discrete-Event Simulation project, namely the base entity object that represents items that are moved through the model. I described how I moved all of the member functions to be outside prototypes so the instantiated objects would be smaller here but I don’t think I ever displayed the code, so here it is. The original internal functions are present but commented out so you can see their original form, and the prototype copies of those functions are included below the closure definition, but attached to the closure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
//entity item passive //stores minimal state information, does nothing on its won function EntityPassive(entityType,parentEntity) { if (typeof parentEntity === "undefined") {parentEntity = null} this.entityID = getNewEntityID(); this.entryTime = globalSimClock; this.entityType = entityType; var p = []; for (var i=0; i<numEntityProperties; i++) { p[i] = 0; } this.propertyList = p; this.entityColor = "#FFFFFF"; //default color, should set this based on its properties this.localEntryTime = 0.0; this.localIndex = -1; this.componentGroup = ""; this.componentGroupEntryTime = 0.0; this.xLocation = 0.0; this.yLocation = 0.0; this.permissionToMove = false; this.forwardAttemptTime = Infinity; this.parentEntity = parentEntity; /* this.getEntityID = function() { return this.entityID; }; this.getEntryTime = function() { return this.entryTime; }; this.setLocalEntryTime = function() { this.localEntryTime = globalSimClock; }; this.getLocalEntryTime = function() { return this.localEntryTime; }; this.setLocalIndex = function(index) { this.localIndex = index; } this.getLocalIndex = function() { return this.localIndex; } this.setComponentGroup = function(componentGroup) { this.componentGroup = componentGroup; }; this.getComponentGroup = function() { return this.componentGroup; }; this.setComponentGroupEntryTime = function(componentGroupEntryTime) { this.componentGroupEntryTime = componentGroupEntryTime; }; this.getComponentGroupEntryTime = function() { return this.componentGroupEntryTime; }; this.getEntityType = function() { return this.entityType; }; this.setPropertyValue = function(propertyName,propertyValue) { var i = 0; while ((i < numEntityProperties) && (propertyName != entityProperties[i][0])) { i++; } if (i < numEntityProperties) { this.propertyList[i] = propertyValue; } else { alert("Trying to set out of range entity property"); } }; this.getPropertyValue = function(propertyName) { var i = 0; while ((i < numEntityProperties) && (propertyName != entityProperties[i][0])) { i++; } if (i < numEntityProperties) { return this.propertyList[i]; } else { alert("Trying to get out of range entity property"); } }; this.setLocation = function(xloc, yloc) { this.xLocation = xloc; this.yLocation = yloc; }; this.getLocation = function() { return {x: this.xLocation, y: this.yLocation}; }; this.setPermission = function(permission) { this.permissionToMove = permission; }; this.getPermission = function() { return this.permissionToMove; }; this.getForwardAttemptTime = function() { return this.forwardAttemptTime; }; this.setForwardAttemptTime = function(forwardAttemptTime) { this.forwardAttemptTime = forwardAttemptTime; }; this.setEntityColor = function(color) { this.entityColor = color; }; this.getEntityColor = function() { return this.entityColor; }; this.drawEntity = function() { //(entityColor) { //in this case x and y are absolute screen coords drawNode(this.xLocation, this.yLocation, 5, this.entityColor); }; */ } //EntityPassive EntityPassive.prototype.getEntityID = function() { return this.entityID; }; EntityPassive.prototype.getEntryTime = function() { return this.entryTime; }; EntityPassive.prototype.setLocalEntryTime = function() { this.localEntryTime = globalSimClock; }; EntityPassive.prototype.getLocalEntryTime = function() { return this.localEntryTime; }; EntityPassive.prototype.setLocalIndex = function(index) { this.localIndex = index; }; EntityPassive.prototype.getLocalIndex = function() { return this.localIndex; }; EntityPassive.prototype.setComponentGroup = function(componentGroup) { this.componentGroup = componentGroup; }; EntityPassive.prototype.getComponentGroup = function() { return this.componentGroup; }; EntityPassive.prototype.setComponentGroupEntryTime = function(componentGroupEntryTime) { this.componentGroupEntryTime = componentGroupEntryTime; }; EntityPassive.prototype.getComponentGroupEntryTime = function() { return this.componentGroupEntryTime; }; EntityPassive.prototype.getEntityType = function() { return this.entityType; }; EntityPassive.prototype.setPropertyValue = function(propertyName,propertyValue) { var i = 0; while ((i < numEntityProperties) && (propertyName != entityProperties[i][0])) { i++; } if (i < numEntityProperties) { this.propertyList[i] = propertyValue; } else { alert("Trying to set out of range entity property"); } }; EntityPassive.prototype.getPropertyValue = function(propertyName) { var i = 0; while ((i < numEntityProperties) && (propertyName != entityProperties[i][0])) { i++; } if (i < numEntityProperties) { return this.propertyList[i]; } else { alert("Trying to get out of range entity property"); } }; EntityPassive.prototype.setLocation = function(xloc, yloc) { this.xLocation = xloc; this.yLocation = yloc; }; EntityPassive.prototype.getLocation = function() { return {x: this.xLocation, y: this.yLocation}; }; EntityPassive.prototype.setPermission = function(permission) { this.permissionToMove = permission; }; EntityPassive.prototype.getPermission = function() { return this.permissionToMove; }; EntityPassive.prototype.getForwardAttemptTime = function() { return this.forwardAttemptTime; }; EntityPassive.prototype.setForwardAttemptTime = function(forwardAttemptTime) { this.forwardAttemptTime = forwardAttemptTime; }; EntityPassive.prototype.setEntityColor = function(color) { this.entityColor = color; }; EntityPassive.prototype.getEntityColor = function() { return this.entityColor; }; EntityPassive.prototype.drawEntity = function() { //(entityColor) { //in this case x and y are absolute screen coords drawNode(this.xLocation, this.yLocation, 5, this.entityColor); }; |
My conception of what’s going on under the hood may not be correct, but I’m visualizing that the JavaScript engine is storing prototypes of each method separately in memory and instantiating objects that include links to the methods instead of complete copies of the methods for each instantiated object. I was thinking of this as being similar to a virtual method table in a more traditional language, where the instantiated object includes memory space for the data members and a single pointer to an entry in the VMT that describes what methods are accessible to that object in that object hierarchy. The setups would not be analogous but the ideas would be similar. The point of the exercise in JavaScript, at least for my purposes, was to reduce the size of the instantiated objects because you don’t want them to carry the burden of huge amounts of code if large numbers of them are going to be created. The data members (properties) have to be unique for each instantiation but the function definitions can be shared.
Various instructions around the web gave me the idea, that prototype inheritance can be used to create different object configurations be defining an initial object with a given set of prototype functions, as I’ve done with EntityPassive
, and then add — and remove — prototype functions and members to create the new forms that are desired. This is different than what I’m used to, where object hierarchies can only be defined so the descendant objects are necessarily larger than the parent objects on which they are based. The JavaScript method seems more flexible.
All this said, that was just my conception, I never experimented with actual code to see just how this could work. I envisioned it as being just a hair clunky, but there’s some possibility that it doesn’t work like this at all.
That brings us to the present subject, which is a different description of how those methods work. I can see the similarities to my conception but a) the material in the lectures is very clear and b) the demonstrations show that what is described actually works.
The presenter provided a lot of background concepts to set everything up, then walks through the implementation of the extend
function in the Underscore.js library. It uses the ideas of reflection and composition to parse other object definitions to determine what properties they contain (data and function members) and adds them to the object of interest. The parsing process is reflection and the addition process is a form of composition.
Here’s the relevant code from Underscore.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
_.extend = createAssigner(_.allKeys); ... var createAssigner = function(keysFunc, undefinedOnly) { return function(obj) { var length = arguments.length; if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; }; |
Here’s the class example of how this is invoked (see line 36):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe' } // don't do this EVER! for demo purposes only!!! john.__proto__ = person; for (var prop in john) { if (john.hasOwnProperty(prop)) { console.log(prop + ': ' + john[prop]); } } var jane = { address: '111 Main St.', getFormalFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); console.log(john); |
In the end I’m thinking that my conception was pretty well on target. The composition-by-addition demonstrated here works the way I envisioned, and objects can as easily be modified by removing elements. That said, defining things this way vs. using prototype definitions as I did above creates objects that have different payloads under the hood. This is something that must be understood by the practitioner. I look forward to seeing what, if anything, the presenter has to say on the subject.