secret
Last active

A BETTER JAVASCRIPT OBJECT APPROACH

  • Download Gist
approach.js
JavaScript
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 196 197 198 199 200 201 202 203 204 205 206 207 208 209
// A BETTER JAVASCRIPT OBJECT APPROACH
 
 
// 1. Introduction
 
// This code demonstrates how objects in javascript should be created. It shows
// best practices for constructor functions, attribute assignment and
// inheritance.
 
// NOTE: There are many different opinions and ways to deal with object sets and
// inheritance in JavaScript. We decided to use this approach, because it is a
// quite common style, its JavaScript's most native approach using classical
// prototypal inheritance, is pure ECMAScript3.1 without any external library and
// is the way the new class methods in ECMAScript6 will be handled (those are
// just syntactic sugar).
 
// The community uses JavaScript this way, the newest ECMAScript versions show
// that the language is developed this way and it's the most performant way.
 
// However, this way of dealing with objects is highly based on trust. It's very
// dynamic and extendable, but also insecure. There are no true private
// attributes and we trust in other developers to follow our guidelines. If they
// don't, they can do big damage to the whole system.
 
// Nevertheless, this is how JavaScript was defined (dynamically). We should
// utitlize this way of JavaScript and not force it into something it definitely
// is not (e.g. static typed).
 
// Note: To fully understand this article, you'll need a basic understanding of
// JavaScripts four ways to invoke a function and the internals of some
// JavaScript engines. Nonetheless, you can follow this guide anyway.
 
 
// 2. Creating Objects
 
// First of all start by defining a new type, in this case a car. Start with a
// capital letter to make clear that this function must be invoked with new.
 
var Car = function (arg1, arg2) {
this._velocity = 0;
this._speed = 0;
this._color = null;
this._arg1 = arg1;
this._arg2 = arg2;
};
 
// For performance reasons, you always have to set all attributes in the
// constructor function to some value, even if it is only null. By that way the
// JavaScript compiler/interpreter can optimize the memory usage of all objects
// as he can create them from the same scheme.
 
// Also, be aware that there are no private attributes in JavaScript. A common
// workaround is to prefix all attributes with an underscore to show that they
// should not be modified from the outside. This is only build on trust!
 
 
// 3. Adding Methods
 
// To add methods to instances of the Car object, we need to set them on the
// prototype key. That way we ensure optimal performance as all Car instances
// use the same function implementation only with a different environment. This
// saves tones of memory. Never ever define functions in the constructor
// function, if you do so, each object will have its own implementation.
 
Car.prototype.drive = function () {
// …
};
 
// To add static methods to the Car object, simply define them on the
// constructor function. All static methods will use the same implementation and
// are now simply namespaced.
 
Car.staticMethod = function () {
// …
};
 
 
// 4. Getters and Setters
 
// Although all attributes can be accessed directly on such objects, that is not
// a good idea and should be prevented. To inform other developers it's a common
// practice to prefix these attributes with an underscore.
 
// Getters have the same name as the attributes. They take no params and always
// return this value.
 
Car.prototype.velocity = function () {
return this._velocity;
};
 
// Getters that return booleans should be prefixed with `is` or `has`. Example:
 
Car.prototype.isMotorActive = function () {
return this._isMotorActive;
};
 
// Setters are prefixed with `set` and have the same name as their attributes.
// They take on param and always return `this` for chaining setters. Be sure
// to check the value of the new variable in the getter (if neccessary).
 
Car.prototype.setVelocity = function (velocity) {
this._velocity = velocity;
return this;
};
 
// That way setters can be chained together like this:
 
var myCar = new Car('arg1', 'arg2');
myCar.setVelocity(0)
.setSpeed(0)
.setSomethingElse(0);
 
 
// 5. Inheritance
 
// To make one object inherit from another, we use prototypal inheritance.
 
// First of all, we need to create a new constructor function.
 
var FireCar = function (arg1, arg2, arg3) {
Car.call(this, arg1, arg2);
 
this._color = '#ff0000';
this._hasLedge = true;
this._arg3 = arg3;
};
 
// The most important line here is the call to its parent's constructor via
// `Car.call`. This line calls the Car constructor with this bound to our new
// FireCar object. This way, the new FireCar object will be assigned all of its
// parents attributes. This is important for performance reasons again.
 
// We then add our own modifications and new attributes to our FireCar object in
// the constructor.
 
// Next, we need to ensure that whenever a function is called, which is not set
// on FireCar, the prototype chain can be followed completely and the parent
// implementation is used.
 
FireCar.prototype = new Car();
 
// This will override FireCars default prototype with a new Object made from
// Car.prototype. When an attribute on FireCar is accessed that is not defined
// there, the complete prototype chain will be followed (single-inheritance).
 
// To simply override a complete method in FireCar, all you have to do is to
// define it on FireCar.prototype like this:
 
FireCar.prototype.drive = function () {
// …
};
 
// If you want to override a method in FireCar, but still want to use the old
// implementation, use call like in the constructor function:
 
FireCar.prototype.drive = function (arg1, arg2) {
var result = Car.prototype.drive.call(this, arg1, arg2);
// do some more stuff (and own implementation)
return result + 1;
};
 
// That way, all methods are only defined once. Even the FireCar.drive method
// uses the Car.drive implementation. That saves a lot of memory.
 
// Now you can simply instantiate a new FireCar object.
 
var yourCar = new FireCar('arg1', 'arg2', 'arg3');
 
 
// 6. Mixins and other ways to extend objects
 
// As this way to define objects is highly flexible, you can use all ways to
// extend objects. You can use mixins, inheritance or something else. Just be
// sure to document what you do – and do it responsible.
 
 
// 7. Files and Namespaces
 
// Each object should solve a single purpose and should be implemented in a
// single file. Be sure to properly use namespaces and stop prefixing your
// objects with your project initials or something similar. That's very annoying
// when it comes to using this objectsc.
 
 
// 8. Log Objects
 
// When you want to log objects, you should be aware what you do, because
// `console.log` hides the prototype chain, but shows all the private
// attributes.
 
// this is wrong and only shows data
console.log(myCar);
 
// this is correct, hides data and only shows functions
console.log(myCar.__proto__);
 
 
// 9. Iterate and Typecheck
 
// To iterate over an object, be sure to utilize object.hasOwnProperty,
// otherwise you will iterate over the complete prototype chain. This is
// often unwanted behaviour.
 
// To typecheck an object, simple use `instanceof`. This works with the complete
// prototype chain and all of its objects.
 
if (myCar instanceof Car) {
// …
}

Car.prototype.staticMethod = function ...

should be

Car.staticMethod = function ...

Also I think the rule for getters that return a boolean can be more general than just starting with is or has. I think any sort of verb phrase that can be labelled as either true or false is ok. For example:

car.willDrive, car.canPlayMusic, car.containsPassengers, etc.

Also, considering, that Object.create is new and not as well supported, I don't quite understand why this:

FireCar.prototype = Object.create(Car.prototype);

is recommended over this:

FireCar.prototype = new Car;

Car.isMotorActive = function () {
  return this._isMotorActive;
};

Should be

Car.prototype.isMotorActive = function () {
  return this._isMotorActive;
};

Since getters and setters are referring to instances...

Also, considering, that Object.create is new and not as well supported, I don't quite understand why...

Object.create has existed for 3 years and is supported in every modern JavaScript engine.

@rwldrn - You say "every modern JavaScript engine", which is technically accurate (since IE's ECMAScript engin is JScript, not JavaScript), but IE8 does not support it, and it still has significant market share (even Google supports it with their 3 most recent browser version policy).

@beatgammit thats why I refer to a workaround script in the code.

thank you for bugfixes

You really want to call constructor here?

FireCar.prototype.drive = function (arg1, arg2) {
  var result = Car.call(this, arg1, arg2);

Car constructor doesn't take parameters, this looks inconsistent:

var FireCar = function (arg1, arg2) {
  Car.call(this, arg1, arg2);

I would like to fork this and do a node.js event emitter version. Really nice, i have been looking for something like this

@lm1 @rschmukler @vovik thank you for your work, fixed the mistakes

@hortinstein plz let me know about the result!

"EcmaScript5" "No getter/setter used". Am I the only one around here finding this weird?

@Ralt I was interested in cross-plattform functionality. However, I use Object.create, but this method is easy to add if missing.

ES5 setters and getters are not easy to add I think. Or is there a better way to do this?

Object.create basic functionality is easy to add. A real Object.create is just the same as adding getters/setters.

What I meant is that if you say this is how to do it in ES5, then do it in ES5, not ES3.

What makes your approach actually different to current class patterns for JS?

@RicoP I had a nice discussion with another developer yesterday about how to define objects in JavaScript. We thought about whether information hiding principles or JavaScripts pure dynamic would be better. This is just the result of this discussion, it is nothing fundamentally new. The title is a bit misleading I guess.

@Ralt you are absolutely right about that, naming it ES5 is a bit misleading, thank you.

private properties:

var Car = function() {
    var privateFoo = 'bar';
    this.publicFoo = 'zar';
}

Although all attributes can be accessed directly on such objects, that is not
a good idea and should be prevented.

why? Also, why not use Object.defineProperties for getters/setters if you are talking modern JS :)

To elaborate on uselessness of function getters: just access the property directly and if - later on - you really need more functionality when accessing that property, redefine it with Object.defineProperties. Always writting getFoo() "just in case" seems overkill, especially if it can be replaced so easily later on.

@oberhamsi

var Car = function() {
    var privateFoo = 'bar';
    this.publicFoo = 'zar';
}

That variable is a bit too private, since the objects own methods can not access it.

This private/public properties are just awkward to use using such a pattern.

Use a modular pattern. Closures will allow you to have public/private properties if you want to. You can instantiate as many objects as you want. (Object.create or simply function Car() { return {}; }.)

And you don't need to bother with so much unneeded complexity.

Is the following not well-supported ES5?

Car.prototype = {
get velocity(){
return this._velocity;
},
set velocity(value){
this.velocity = value;
}
};

var car = new Car();
car.velocity = 5;
assert(car._velocity === 5);

Ugh, and this is why we preview before posting.

Nice summary! Spotted this: var result = Car.call(this, arg1, arg2); which I assume should be

var result = Car.prototype.drive.call(this, arg1, arg2);

Right?

@emilisto yes, thank you

@vovik : Object.create( C.prototype ) is better than new C( ) because the second one calls the constructor that might initialize useless properties, cause the constructor to throw errors.

And that basic functionality of ES 5 can be emulated by creating a new constructor and setting its prototype to the prototype of the parent constructor. The fact that it's not widely supported yet doesn't matter: you can use a polyfill. If you think you might not have Object.create and polyfill it, you will not use getters/setters etc. so you don't need its second argument to work. And if you do use them, you know that you have it.

@all not liking the privateFoo example: functions in the closure can read those properties. How is it more complexity than using Object.create? It's just using what JS gives us without overhead.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.