-
-
Save damienklinnert/a2fd1da997f457b76efe to your computer and use it in GitHub Desktop.
// 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) { | |
// … | |
} |
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.
@oberhamsi
That variable is a bit too private, since the objects own methods can not access it.