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

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