Skip to content

Instantly share code, notes, and snippets.

@damienklinnert
Created November 22, 2012 23:46
Show Gist options
  • Star 90 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save damienklinnert/a2fd1da997f457b76efe to your computer and use it in GitHub Desktop.
Save damienklinnert/a2fd1da997f457b76efe to your computer and use it in GitHub Desktop.
A BETTER JAVASCRIPT OBJECT APPROACH
// 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) {
// …
}
@damienklinnert
Copy link
Author

@vlazzle
Copy link

vlazzle commented Nov 23, 2012

Car.prototype.staticMethod = function ...

should be

Car.staticMethod = function ...

@vlazzle
Copy link

vlazzle commented Nov 23, 2012

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.

@vlazzle
Copy link

vlazzle commented Nov 23, 2012

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;

@vlazzle
Copy link

vlazzle commented Nov 23, 2012

I made a fork to address these issues: https://gist.github.com/513f7a71368835cbef2f

@rschmukler
Copy link

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

Should be

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

Since getters and setters are referring to instances...

@rwaldron
Copy link

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.

@beatgammit
Copy link

@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).

@damienklinnert
Copy link
Author

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

@damienklinnert
Copy link
Author

thank you for bugfixes

@lm1
Copy link

lm1 commented Nov 23, 2012

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);

@hortinstein
Copy link

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

@damienklinnert
Copy link
Author

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

@hortinstein plz let me know about the result!

@ralt
Copy link

ralt commented Nov 23, 2012

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

@damienklinnert
Copy link
Author

@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?

@ralt
Copy link

ralt commented Nov 23, 2012

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.

@RicoP
Copy link

RicoP commented Nov 23, 2012

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

@damienklinnert
Copy link
Author

@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.

@oberhamsi
Copy link

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 :)

@oberhamsi
Copy link

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.

@johandam
Copy link

@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.

@ralt
Copy link

ralt commented Nov 23, 2012

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.

@wibblymat
Copy link

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);

@wibblymat
Copy link

Ugh, and this is why we preview before posting.

@emilisto
Copy link

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?

@damienklinnert
Copy link
Author

@emilisto yes, thank you

@xavierm02
Copy link

@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.

@oberhamsi
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment