Post Modern Objects in Javascript
Or how to stop using ES3 Objects and learn to love ES5
http://slides.com/matthijsvanhenten/post-modern-objects-in-javascript/live
Matthijs van Henten
Webdevelopment since 2004
PHP, Perl, Python and Javascript
http://github.com/mvhenten
matthijs@fazzination.com
Postmodernism
pəʊstˈmɒdəˌnɪz(ə)m/
noun
- a late 20th-century style and concept in the arts, architecture, and criticism, which represents a departure from modernism and is characterized by the self-conscious use of earlier styles and conventions, a mixing of different artistic styles and media, and a general distrust of theories.
ES3
- December 1999
- supported over a decade
- modern javascript
IE8 end of life
ES5 support in all major browsers
http://kangax.github.io/compat-table/es5/
Stop using ES3
- Use the tools from ES5
- Better encapsulation
- Your framework is propably already using them
a small class implementation using Object.defineProperty and ES5 getters and setters
function Counter(){
var count = 0;
this.increment = function(){
return count++;
}
}
function Counter( start, end, increment ){
var init = start || 0,
current = init,
incr = increment || 1,
max = end || Infinity;
this.increment = function(){
if( current + increment < max )
return current += increment;
return current;
};
this.getCurrent = function(){
return current;
};
this.getNext = function(){
var next = current + increment;
return next < max ? next : current;
};
this.getLast = function(){
return max - ( ( max - init ) % incr );
};
}
var counter = new Counter( 10, 99, 3 );
counter.increment(); // 13
counter.getCurrent(); // 13
counter.getNext(); // 16
counter.getLast(); // 97
// ... in the next cycle
counter.increment();
// TypeError: Property 'increment' of
// object #<Counter> is not a function
// somewhere in your code
counter.increment = 1;
'use strict';
// start with a constructor
function Counter( start, end, increment ){
var values = {
start: start || 0,
current: start || 0,
incr: increment || 1,
max: end || Infinity,
};
Object.defineProperties( this, {
__get: {
writable: false,
value: function( name ){
return values[name];
}
},
__set: {
writable: false,
value: function( name, value ){
return values[name] = value;
}
}
});
};
Counter.prototype = {
get current(){
return this.__get('current');
},
get max(){
return this.__get('max');
},
get incr() {
return this.__get('incr');
},
get start() {
return this.__get('start');
},
get next() {
var next = this.current + this.incr;
return next < this.max ? next : this.current;
},
get last() {
return this.max - ( ( this.max - this.start ) % this.incr );
},
get increment() {
return function(){
return this.__set( 'current', this.current + this.incr );
}
}
};
var counter = new Counter( 10, 99, 3 );
counter.increment(); // 13
counter.current; // 13
counter.max; // 16
counter.next; // 16
counter.last; // 97
You promised less code...
- No more getSomething(). Just properties.
- Increment is a read-only property we can call as method
- Impossible to overwrite public members (!)
- Moved implementation into the prototype
- Altough public instance methods, __get and __set should be considered "hidden", i.e. Don't Touch This.
- And they are read-only.
From now on, you're code will fail if you try to overwrite a property:
counter.next = 13;
^
TypeError: Cannot set property next of #<Object> which has only a getter
Let's write a little helper
to avoid coding __get and __set each time:
var Counter = define(function(start, end, increment) {
return {
start: start || 0,
current: start || 0,
incr: increment || 1,
max: end || Infinity,
};
}, {
// prototype...
});
function Counter( start, end, increment ){
var values = {
start: start || 0,
current: start || 0,
incr: increment || 1,
max: end || Infinity,
};
Object.defineProperties( this, {
__get: {
writable: false,
value: function( name ){
return values[name];
}
},
__set: {
writable: false,
value: function( name, value ){
return values[name] = value;
}
}
});
};
function define(ctor, proto) {
var me = function() {
var values = ctor.apply(this, arguments);
Object.defineProperties(this, {
__get: {
value: function(name) {
return values[name];
}
},
__set: {
value: function(name, value) {
return values[name] = value;
}
}
});
};
me.prototype = proto;
return me;
};
{
get current() {
return this.__get('current');
},
get max() {
return this.__get('max');
},
get incr() {
return this.__get('incr');
},
get start() {
return this.__get('start');
},
}
Counter.prototype = {
get current(){
return this.__get('current');
},
get max(){
return this.__get('max');
},
get incr() {
return this.__get('incr');
},
get start() {
return this.__get('start');
},
get next() {
var next = this.current + this.incr;
return next < this.max ? next : this.current;
},
get last() {
return this.max - ( ( this.max - this.start ) % this.incr );
},
get increment() {
return function(){
return this.__set( 'current', this.current + this.incr );
}
}
};
function define(ctor, proto) {
var me = function() {
var values = ctor.apply(this, arguments),
props = {};
// we're using .reduce to create a _closure_ here.
props = Object.keys( values ).reduce(function( props, key ){
props[key] = {
get: function(){
return this.__get(key);
}
};
return props;
}, props );
props.__get = {
value: function(name) {
return values[name];
}
};
props.__set = {
value: function(name, value) {
return values[name] = value;
}
}
Object.defineProperties( this, props );
};
me.prototype = proto;
return me;
}
Almost as short...
as our original ES3 implementation,
but now with some better encapsulatin'.
var Counter = create(function(start, end, increment) {
return {
start: start || 0,
current: start || 0,
incr: increment || 1,
max: end || Infinity,
};
}, {
get next() {
var next = this.current + this.incr;
return next < this.max ? next : this.current;
},
get last() {
return this.max - ((this.max - this.start) % this.incr);
},
get increment() {
return function() {
return this.__set('current', this.current + this.incr);
}
}
});
var Counter = create(function( named ) {
named = named || {};
return {
start: named.start || 0,
current: named.start || 0,
incr: named.increment || 1,
max: named.end || Infinity,
};
}, {
// ...
});
var counter = new Counter({
incr: 3
});
counter.increment(); // 3
counter.current; // 3
counter.max; // Infinity
counter.next; // 6
counter.last; // NaN
var counter = new Counter({
start: 10,
end: 99,
incr: 3
});
counter.increment(); // 13
counter.current; // 13
counter.max; // 16
counter.next; // 16
counter.last; // 97
var Counter = define( function( named ){
named = named || {};
return {
start: named.start || 0,
current: named.start || 0,
incr: named.increment || 1,
max: named.end || Infinity,
};
},
{
get next() {
var next = this.current + this.incr;
return next < this.max ? next : this.current;
},
get last() {
return this.max - ((this.max - this.start) % this.incr);
},
increment: function() {
return this.__set('current', this.current + this.incr);
}
});
var Counter = define({
start: 0,
current: 0,
incr: 1,
max: Infinity,
get next() {
var next = this.current + this.incr;
return next < this.max ? next : this.current;
},
get last() {
return this.max - ((this.max - this.start) % this.incr);
},
increment: function() {
return this.__set('current', this.current + this.incr);
}
});
How that works
function define(def) {
var proto = Object.keys(def).reduce(function(proto, key) {
var desc = Object.getOwnPropertyDescriptor(def, key);
// if not writable, don't touch it
if (!desc.writable) {
proto[key] = desc;
return proto;
}
// a method, let's protect it
if (desc.value instanceof Function) {
desc.writable = false;
proto[key] = desc;
return proto;
}
proto[key] = {
get: function() {
return this.__get(key);
}
};
return proto;
}, {});
var me = function(named) {
named = named || {};
var values = Object.keys(def).reduce(function(values, key) {
values[key] = named[key] || def[key];
return values;
}, {});
Object.defineProperties(this, {
__get: {
value: function(name) {
return values[name];
}
},
__set: {
value: function(name, value) {
return values[name] = value;
}
}
});
};
Object.defineProperties(me.prototype, proto);
return me;
}
Let's break that up...
var proto = Object.keys(def).reduce(function(proto, key) {
var desc = Object.getOwnPropertyDescriptor(def, key);
// if not writable, don't touch it
if (!desc.writable) {
proto[key] = desc;
return proto;
}
// a method, let's protect it
if (desc.value instanceof Function) {
desc.writable = false;
proto[key] = desc;
return proto;
}
// regular property
proto[key] = {
get: function() {
return this.__get(key);
}
};
return proto;
}, {});
function define(def) {
// def is the definition of our class
var proto = Object.keys(def).reduce(function(proto, key) {
//...
}, {});
var me = function(named) {
//...
};
Object.defineProperties(me.prototype, proto);
return me;
}
var me = function(named) {
named = named || {};
var values = Object.keys(def).reduce(function(values, key) {
values[key] = named[key] || def[key];
return values;
}, {});
Object.defineProperties(this, {
__get: {
value: function(name) {
return values[name];
}
},
__set: {
value: function(name, value) {
return values[name] = value;
}
}
});
};
Object.defineProperties(me.prototype, proto);
return me;
That leaves us with...
- better encapsulation, protected properties
- concise and readable api
- DRY code
- introduced Object.definePropert(y)(ies), Object.getOwnPropertyDescriptor, ES5 getters and setters as a tool for creating better objects.
One more thing...
other neat stuff:
- Object.freeze, Object.seal
- Object.create
- Inheritance
- Type checks
Shameless plug:
https://github.com/mvhenten/volan
Questions?
Post Modern Objects in Javascript
By Matthijs van Henten
Post Modern Objects in Javascript
- 800