Object.observe vs getters & setters

Benchmark created by Chris on


Setup

if (!Array.prototype.find) {
    
        Array.prototype.find = function (callback, thisArg) {
            'use strict';
            var arr = this,
                arrLen = arr.length,
                i;
            for (i = 0; i < arrLen; i += 1) {
                if (callback.call(thisArg, arr[i], i, arr)) {
                    return arr[i];
                }
            }
            return undefined;
        };
    }
    
    
    var gms = {};
    
    gms.Observer = function( object, property, callback, context, options ) {
    
        if( typeof object !== 'object' ) {
                return;
        }
    
        if( ! ( this instanceof gms.Observer )) {
                return new gms.Observer( object, property, callback, context, options );
        }
    
        this.object    = object;
        this.property  = property;
        this.callback  = callback;
        this.context   = context;
        this.options   = options || {};
    
        this.sync      = ( options ) ? options.sync : false;
    
        // Store the observe function to call ( sync or async )
        this.observeFunction = ( this.sync ) ? gms.Observe.propertySync : gms.Observe.property;
    
        if( this.sync ) {
                this.useNative = false;
        } else {
                this.useNative = ( options && options.useNative !== undefined ) ? options.useNative : Object.observe !== undefined;
        }
    
        // this.useNative = false;
    
    
        // If we've already created the observer, let's use that
        var observer = this._observerAlreadyAttached();
        if( observer && observer.length > 0 ){
                return observer;
        }
    
    
        if( property.indexOf( '.' ) > -1 ) {
                return this._observeChildren( );
        }
    
        this._observingArray = (
                object[ property ] instanceof Array ||
                object instanceof Array && property.indexOf( '@each' ) === 0
        );
    
        this._attachObservers();
    
        return this._attachCallbackFunction();
    
    };
    
    gms.Observer.prototype = {
    
        /**
         * Attaches the appropriate observes ( Object.observe or getter/setter )
         * @return {null}
         */
    
        _attachObservers: function() {
    
                if( this._observingArray ){
    
                        // If the property starts with @each, and our object is an array, use that
                        if( ! ( this.property.indexOf( '@each' === 0 ) && this.object instanceof Array ) ) {
                                this.object = this.object[ this.property ];
                        }
    
                        if( this.useNative ) {
                                this._addArrayObjectObserver( this.object );
                        }
    
                        else {
                                this._addArrayMethods( this.object );
                                this._addSetters( );
                        }
    
    
                }  else {
    
                        if( this.useNative ) {
                                this._addObjectObserver();
                        }
    
                        else {
                                this._addSetters( );
                        }
                }
        },
    
        /**
         * Handles observing arrays
         * @param  {Object}   object   - The object to observe
         * @param  {String}   property - The array property
         * @param  {String}   eachProp - The property to observe on the array
         * @return {null}
         */
        _observeArray: function( object, property, eachProp ) {
    
                var array = ( property === '@each' ) ? object : object[ property ],
                        observers = [];
    
                var observer = this.observeFunction.call( gms.Observe, object, property, this.callback, this.context, this.options );
    
                observers.push( observer );
    
                // Add array each observers
                if( eachProp ) {
    
                        this._addEachProperties( array, eachProp );
    
                        array.__eachObservers__[ eachProp ].push( this._createObserver() );
    
                        var eachObservers = [];
    
                        array.forEach( function( item ) {
                                eachObservers.push(
                                        this.observeFunction.call( gms.Observe, item, eachProp, this.callback, this.context, this.options )
                                );
                        }, this );
    
                        observers = observers.concat( eachObservers );
                }
    
                return observers;
    
        },
    
        /**
         * Adds a property to observe on each item of the array
         * @param {Array} array     - The array to observe
         * @param {String} eachProp - The property to observe on each array item
         */
        _addEachProperties: function( array, eachProp ) {
    
                if( ! array.__eachObservers__ ) {
                        Object.defineProperty( array, '__eachObservers__', {
                                enumerable : true,
                                writable   : true,
                                value: {}
                        } );
                }
    
                if( ! array.__eachObservers__[ eachProp ] ) {
                        Object.defineProperty( array.__eachObservers__, eachProp, {
                                enumerable : true,
                                writable   : true,
                                value      : []
                        } );
                }
    
        },
    
        /**
         * Attaches the callback function to the object.property
         * @return { Observer} - The Observer created
         */
        _attachCallbackFunction: function() {
    
                // If we're observing an array, we want the __observes__ to be an array,
                // since there's no property name for an object
                var __observersValue__ = ( this._observingArray ) ? [] : {};
    
                // Create the __observers__ property
                if( this.object.__observers__  === undefined ) {
    
                        Object.defineProperty( this.object, '__observers__', {
                                enumerable : false,
                                value      : __observersValue__,
                                writable   : true
                        } );
                }
    
                // Create the __observers__[ property ] array to store the observers
                if( !this._observingArray && this.object.__observers__[ this.property ] === undefined ) {
    
                        var observers = this.object.__observers__;
    
                        Object.defineProperty( observers, this.property, {
                                enumerable : true,
                                value      : [],
                                writable   : true
                        } );
    
                }
    
                // Create the Observer actual observer
                var observer = this._createObserver();
    
                // Add it to the object.__observers__ array
                if( !this._observingArray ){
                        this.object.__observers__[ this.property ].push ( observer );
                } else {
                        this.object.__observers__.push ( observer );
                }
    
                return observer;
        },
    
        /**
         * Observe child properties in dot notation ( object, 'prop.subProp' )
         * @optional {Object} object - The object to observe
         * @optional {String} properties - A dot separated list of properties to observe
         * @return {Array}                         - Array of observers
         */
        _observeChildren: function( object, properties ) {
    
                object = object || this.object;
    
                var parts     = ( properties ) ? properties.split( '.' ) : this.property.split( '.' ),
                        property  = parts.shift(),
    
                        observers = [], observer;
    
                var observeLastProperty = this.options.observeLastProperty;
    
                // Use the right observe function ( sync/async
    
                if( parts[ 0 ] === '@each' || property === '@each'  ) {
                        observer = this._addEachObservers( object, property, parts );
                }
    
                else if( property === '@any' ) {
    
                        return this._addAnyObservers( parts.join( '.' ) );
                }
    
                else {
    
                        object[ property ] = object[ property ] || {};
    
                        // Observe the first child
                        observer = this.observeFunction.call( gms.Observe, object, property, this.callback, this.context, this.options );
                }
    
                // Add the observer to the array
                observers.push( observer );
    
                // Get recursive
                if( parts.length > 1 ){
                        var childObservers = this._observeChildren( object[ property ], parts.join('.') );
                        observers = observers.concat( childObservers );
                }
    
                // Observe the last child
                else {
    
                        if( property  !== '@any' ) {
                                object = object[ property ];
                        }
    
                        property = parts[ 0 ];
    
                        // Clone the options object so we can set a property on it
                        var optionsClone   = ( this.options !== undefined ) ? goog.object.clone( this.options ) : { };
    
                        if( observeLastProperty ){
                                optionsClone.lastProperty = true;
                        }
    
                        observer = this.observeFunction.call( gms.Observe, object, parts.join( '.' ), this.callback, this.context, optionsClone );
                        observers.push( observer );
                }
    
                return observers;
        },
    
        /**
         * Adds an .@each observer to an array
         * @param {Object} object - The object to observe
         * @param {String} property - The property
         * @param  {Array}      parts    - An array of all the child observers
         * @return {Observer}            - The observer added
         */
        _addEachObservers: function( object, property, parts ) {
    
                var eachObserver = ( parts[ 0 ] === '@each' ) ? parts.slice( 1 ).join( '.' ) : parts.join( '.' );
    
                object[ property ] = object[ property ] || [];
    
                var observer = this._observeArray( object, property, eachObserver );
    
                if( parts[ 0 ] === '@each' ){
                        parts.splice( 0, 1 );
                }
    
                return observer;
        },
    
        /**
         * Adds an .@any observer to an object
         * @param  {String}     property - The property
         * @return {Array}               - The observers added
         */
        _addAnyObservers: function( property ) {
    
                var observers = [];
    
                if( this.sync && ! this.options.ignoreAnySyncWarning ){
                        console.warn( '----------------' );
                        console.log( 'You are synchronously observing @any on an object.' );
                        console.log( 'Your callback will fire when any property currently defined on the object changes' );
                        console.log( 'But will not fire when new properties are added.' );
                        console.log( 'To acheive both, you must observe @any asynchronously.' );
                        console.log( 'To suppress this message pass ignoreAnySyncWarning: true as an Observer options argument');
                        console.warn( '----------------' );
                }
    
                for( var prop in this.object ) {
    
                        var observer = this.observeFunction.call( gms.Observe, this.object, prop, this.callback, this.context, this.options );
                        observers.push( observer );
    
                        // If we're observing a property on @any item, do it here!
                        if( property !== '' && property !== '@any' ) {
                                observer = this.observeFunction.call( gms.Observe, this.object[ prop ], property, this.callback, this.context, this.options );
                                observers.push( observer );
                        }
                }
    
                return observers;
        },
    
        /**
         * Creates the Observer object
         * @return {Observer} - The Observer object
         */
        _createObserver: function() {
    
                // TODO: maybe make this its own class?
                return {
                        callback      : this.callback,
                        context       : this.context,
                        sync          : this.sync,
                        object        : this.object,
                        property      : this.property,
                        nativeObserve : this.useNative,
                        options       : this.options
                };
        },
    
        /**
         * Creates the change object that gets returned to the callback
         * @param {Object} object   - The object listening for the change
         * @param {String} property - The property changed
         * @param {*} oldValue      - The previous value
         * @return {change}         - the Change object to return to the callback
         */
        _createChange: function( object, property, oldValue, args ) {
    
                // TODO: maybe make this its own class?
                return {
                        type         : 'update',
                        object       : object,
                        name         : property,
                        oldValue     : oldValue,
                        args         : args,
                        currentValue : object[ property ]
                };
        },
    
        _createArrayChange: function( array, item, type, oldValue, args ) {
    
                var change = {
                        type         : type,
                        object       : array,
                        index        : array.indexOf( item ),
                        oldValue     : oldValue,
                        currentValue : array[ array.indexOf( item ) ],
                        args         : args
                };
    
                if( type === 'delete' ) {
                        change.oldValue = item;
                }
    
                return change;
        },
    
        /**
         * Takes a change object returned by the native Object.observe and adds some properties to it
         * @param  {Object} change - The change returned by O.o
         * @return {Change}        - The new change object
         */
        _cloneNativeChange: function( change, args ) {
    
                // TODO: maybe make this its own class?
                return {
                        type         : 'update',
                        object       : change.object,
                        name         : change.name,
                        oldValue     : change.oldValue,
                        args         : args,
                        currentValue : change.object[ change.name ]
                };
        },
    
        /**
         * Takes a change object returned by the native Object.observe( array ) and adds some properties to it
         * @param  {Object} change - The change returned by O.o
         * @return {Change}        - The new change object
         */
        _cloneNativeArrayChange: function( change, args ) {
    
                // TODO: maybe make this its own class?
                return {
                        type         : change.type,
                        object       : change.object,
                        index        : change.name,
                        oldValue     : change.oldValue,
                        args         : args,
                        currentValue : change.object[ change.name ]
                };
        },
    
        /**
         **** Native Object.observe Methods *******
         -----------------------------------
        */
    
        /**
         * Uses the native Object.observe
         */
        _addObjectObserver: function() {
    
                var observedProperty = this.property,
                        _this = this;
    
                Object.observe( this.object, function( changes ) {
    
                        // We only care if an object has been updated, added, or deleted
                        changes = changes.filter( function( c ) {
                                return [ 'add', 'update', 'delete' ].indexOf( c.type ) > -1;
                        });
    
                        _this._objectObserverCallback( changes, observedProperty  );
                });
        },
    
        /**
         * Users the native Object.observe to observe changes to an array
         * @param {Array} array - The array to observe
         */
        _addArrayObjectObserver: function( array ) {
    
                var _this = this;
    
                Object.observe( array, function( change ) {
                        _this._arrayObjectObserverCallback( change  );
    
                });
    
        },
    
        /**
         * The callback fired by the native Object.observe
         * @param  {Object} changed          - The change object
         * @param  {String} observedProperty - The property observed
         * @return {null}
         */
        _objectObserverCallback: function( changed, observedProperty ) {
    
                changed.forEach( function( change ) {
                        this._forEachChangedObject( change, observedProperty );
                }, this );
        },
    
        /**
         * Callback fired by the array Object.observe
         * @param  {Array} changed - Array of changes
         * @return {null}
         */
        _arrayObjectObserverCallback: function( changed ) {
    
                changed.forEach( function( change ) {
                        this._forEachArrayChangedObject( change );
                }, this );
        },
    
        /**
         * Looks through all the changed objects for one matching our property
         * @param  {[type]} change [description]
         * @return {[type]}        [description]
         */
        _forEachChangedObject: function( change, observedProperty ) {
    
                var property  = observedProperty,
                        object    = change.object,
                        observers = object.__observers__;
    
                if( change.name === property || observedProperty === '@any' ) {
    
                        observers[ property ].forEach( function( observer ){
                                change = this._cloneNativeChange( change, observer.options );
                                gms.Observe._onObservedPropChange( observer, change );
                        }, this );
    
                }
        },
    
        /**
         * Handles adding array changes to the observer queue
         * Only adds to queue when an actual item in the array has changed, not an array property ( like length )
         * @param  {Change} change - A single change object
         * @return {null}
         */
        _forEachArrayChangedObject: function( change ) {
    
                var array    = change.object,
                        observers = array.__observers__;
    
                if( ! isNaN( change.name ) ) {
    
                        this._reattachEachObservers( array, change.object[ change.name ] );
    
                        observers.forEach( function( observer ){
                                change = this._cloneNativeArrayChange( change, observer.options );
                                gms.Observe._onObservedPropChange( observer, change );
                        }, this );
    
                }
        },
    
        /**
         **** Non-Native Observer Methods ****
         *------------------------------------
         */
    
        /**
         * Manually creates setters and getter functions
         */
        _addSetters: function( ){
    
                var object   = this.object,
                        property = this.property;
    
                if( property === '@any' ) {
                        return this._addAnyObservers(  this.property );
                }
    
                // Check if __ private __ property is defined
                if( ! object.hasOwnProperty( '__' + property + '__' ) ) {
    
                        this._addPrivateProp( object, property );
    
                        // Then redefine the propertyerty
                        var setter = object.__lookupSetter__( property ),
                                _this = this;
    
                        Object.defineProperty( object, property, {
    
                                configurable: true,
    
                                get: function( ) {
                                        return this[ '__' + property + '__' ];
                                },
    
                                set: function( value ) {
    
                                        var oldValue = this[ property ];
    
                                        if( value !== oldValue ) {
    
                                                if( setter ) {
                                                        setter( value );
                                                } else {
                                                        object[ '__' + property + '__' ] = value;
                                                }
    
                                                return _this._set.call( _this, this, property, oldValue );
                                        }
    
                                }
    
                        });
    
                }
        },
    
        /**
         * Function called after an observed property is changed, creates a queue of callbacks to fire
         * @param {Object} object   The observed object
         * @param {String} property The changed property name
         * @param {*}      oldValue The previous value of the property
         */
        _set: function( object, property, oldValue ) {
    
                if( object.__observers__ && object.__observers__[ property ] ) {
    
                        object.__observers__[ property ].forEach( function( observer ){
    
                                var changed = this._createChange( object, property, oldValue, observer.options );
                                gms.Observe._onObservedPropChange( observer, changed );
    
                        }, this );
                }
        },
    
        /**
         * Creates proxies for array methods push, splice, pop and sort; which trigger _onArrayUpdated when called
         * @param {Array} array - The array to add the methods to
         */
        _addArrayMethods: function( array ) {
    
                var _this = this;
    
                array.push = function( ) {
                        var pushed = Array.prototype.push.apply( this, arguments );
                        _this._onArrayUpdated( this, 'add', Array.prototype.slice.call( arguments, [ this[ pushed ] ] ) );
                        return pushed;
                };
    
                array.splice = function( ) {
                        var spliced = Array.prototype.splice.apply( this, arguments );
    
                        if( arguments[ 2 ] !== undefined ){
                                _this._onArrayUpdated( this, 'add', [ arguments[ 2 ] ] );
                        } else {
                                _this._onArrayUpdated( this, 'delete', spliced );
                        }
                        return spliced;
                };
    
                array.pop = function( ) {
                        var popped = Array.prototype.pop.apply( this, arguments );
                        _this._onArrayUpdated( this, 'delete', popped );
                        return popped;
                };
    
                array.sort = function( ) {
                        var sorted = Array.prototype.sort.apply( this, arguments );
                        _this._onArrayUpdated( this, 'change', sorted );
                        return sorted;
                };
        },
    
        /**
         * Called by the proxied array methods
         * @param  {Array} array        - The array updated
         * @param  {String} type         - The type of update [ 'add', 'delete', 'update' ]
         * @param  {Array} changedItems - The items that were added, deleted, or updated
         * @return {null}
         */
        _onArrayUpdated: function( array, type, changedItems ) {
    
                if( array['__observers__'] ) {
    
                        // If we're observing properties on each array item
                        // Observe them on the new items we're adding
                        if( type === 'add' && array.__eachObservers__ ){
                                this._reattachEachObservers( array, changedItems );
                        }
    
                        for( var i = 0, l = array['__observers__'].length; i < l; i++ ) {
    
                                var observer = array['__observers__'][ i ];
    
                                var changes = [];
    
                                if( changedItems instanceof Array  ) {
                                        changedItems.forEach( function( item ) {
                                                var change = this._createArrayChange( array, item, type, observer.options );
                                                changes.push( change );
                                        }, this );
                                }
    
                                else {
                                        changes.push( this._createArrayChange( array, changedItems, type, observer.options ) );
                                }
    
                                gms.Observe._onObservedPropChange( observer, changes );
    
                        }
                }
        },
    
        /**
         * Reattaches the each observer to items in an array
         * @param  {Array} array - The array
         * @param  {Array} items - An array of items to attach
         * @return {null}
         */
        _reattachEachObservers: function( array, items ) {
    
                for( var observedProp in array.__eachObservers__ ){
    
                        var observers = array.__eachObservers__[ observedProp ];
    
                        observers.forEach( function( observer ){
    
                                if( items instanceof Array ){
    
                                        items.forEach( function( arrayItem ){
                                                this.observeFunction.call( gms.Observe, arrayItem, observedProp, observer.callback, observer.context, observer.args );
                                        }, this );
                                }
    
                                else {
                                        this.observeFunction.call( gms.Observe, items, observedProp, observer.callback, observer.context, observer.args );
                                }
    
                        }, this );
                }
        },
    
        /**
         * Creates the internal __property__ on the object
         * @param {Object} object   The object
         * @param {String} property The property
         */
        _addPrivateProp: function( object, property ) {
    
                var getter = object.__lookupGetter__( property );
    
                if( getter ) {
    
                        Object.defineProperty( object, '__' + property + '__', {
                                enumerable : false,
                                get        : getter
                        });
    
                }
    
                else {
    
                        Object.defineProperty( object, '__' + property + '__', {
                                enumerable : false,
                                value      : object[ property ],
                                writable   : true
                        });
                }
        },
    
        _observerAlreadyAttached: function( ) {
    
                if( this.object.__observers__ && this.object.__observers__[ this.property ] ){
                        return this.object.__observers__[ this.property ].filter( function( observer ) {
                          return this.callback === observer.callback && this.context === observer.context;
                        }, this );
                }
    
                return false;
    
        }
    };
    
    
    gms.Observe = {
    
        // The callback queue
        _queue: [],
    
        /**
         * Observes a single property
         * @param  {Object} object     - The object to observe
         * @param  {String} property   - The name of the property
         * @param  {Function} callback - The callback function
         * @param  {Object} context    - The context to run the callback
         * @param {Object} options     - Optional options
         * @return {Observer}          - The Observer object
         */
        property: function( object, property, callback, context, options ) {
    
                if( typeof property !== 'string' ) {
    
                        if( Array.isArray( property ) ){
                                return this.properties.apply( this, arguments );
                        }
    
                        throw 'You must pass a string to Observe.property';
                }
    
                return new gms.Observer( object, property, callback, context, options );
    
        },
    
        /**
         * Observe a single property synchronously
         * @param  {Object}   object   - The object to observe
         * @param  {String}   property - The property to observe
         * @param  {Function} callback - The callback function
         * @param  {Object} context    - The context to run the callback
         * @param {Object} options     - Optional options
         * @return {Observer}          - The observer object
         */
        propertySync: function( object, property, callback, context, options ) {
    
                options      = options || {};
                options.sync = true;
    
                return this.property( object, property, callback, context, options );
        },
    
        /**
         * Observe multiple properties on an object
         * @param  {Object}   object     - The object to observer
         * @param  {Array}    properties - An array of properties
         * @param  {Function} callback   - The callback to fire
         * @param  {Object} context      - The context to run the callback
         * @param {Object} options       - Optional options
         * @return {Array}               - The created observers
         */
        properties: function( object, properties, callback, context, options ) {
    
                if( ! ( properties instanceof Array ) ) {
                        throw 'You must pass an array to Observe.properties';
                }
    
                var observers = [];
    
                properties.forEach( function( property ){
                        observers.push( this.property( object, property, callback, context, options ) );
                }, this );
    
                return observers;
        },
    
        /**
         * Observe multiple properties on an object synchronously
         * @param  {Object}   object     - The object to observer
         * @param  {Array}    properties - An array of properties
         * @param  {Function} callback   - The callback to fire
         * @param  {Object} context      - The context to run the callback
         * @param {Object} options       - Optional options
         * @return {Array}               - The created observers
         */
        propertiesSync: function( object, properties, callback, context, options ) {
    
                var observers = [];
    
                properties.forEach( function( property ){
                        observers.push( this.propertySync( object, property, callback, context, options ) );
                }, this );
    
                return observers;
        },
    
        /**
         * Creates observers to fire the observer only when the last property in the chain changes
         * @param  {Object}   object   - The object to observe
         * @param  {String}   property - The property
         * @param  {Function} callback - The callback
         * @param  {Object}   context  - The context to run the callback with
         * @param {Object} options     - Optional options
         * @return {Observer}            The Observer
         */
        lastProperty: function( object, property, callback, context, options ) {
    
                options = options || {};
                options.observeLastProperty = true;
    
                this.property( object, property, callback, context, options );
        },
    
        /**
         * Creates observers to fire the observer synchronously only when the last property in the chain changes
         * @param  {Object}   object   - The object to observe
         * @param  {String}   property - The property
         * @param  {Function} callback - The callback
         * @param  {Object}   context  - The context to run the callback with
         * @param {Object} options     - Optional options
         * @return {Observer}            The Observer
         */
        lastPropertySync: function( object, property, callback, context, options ) {
    
                options = options || {};
                options.observeLastProperty = true;
    
                this.propertySync( object, property, callback, context, options );
        },
    
        /**
         * Stop observing a property
         * @param  {Observer|Array} observer - The observer(s) to stop
         * @return {null}
         */
        stop: function( observer ) {
    
                if( observer instanceof Array ) {
                        observer.forEach( this._removeObserver, this );
                } else {
                        this._removeObserver( observer );
                }
        },
    
        /**
         * Remove an observer from an object
         * @param  {Object}   object   - The object
         * @param  {String}   property - The property to stop observing
         * @param  {Function} callback - The callback
         * @param  {Object}   context  - The context the callback runs in
         * @return {null}
         */
        remove: function( object, property, callback, context ) {
    
                var observers     = object.__observers__,
    
                        // Find the observers to remove
                        observersToRemove = observers[ property ].filter( function( o ) {
                         return o.callback === callback && o.context === context;
                        });
    
                this.stop( observersToRemove );
        },
    
        /**
         * Pause an observer
         * @param {Observer} observer - The observer to pause
         */
        pause: function( observer ) {
                observer.paused = true;
        },
    
        /**
         * Resume a paused observer
         * @param {Observer} observer      - The observer to resume
         * @param {Boolean} fireLastChange - Whether to fire the callback for the last change
         */
        resume: function( observer, fireLastChange ) {
    
                observer.paused = false;
    
                if( fireLastChange && observer.__lastChange__ ) {
                        this._onObservedPropChange( observer, observer.__lastChange__ );
                }
    
                delete observer.__lastChange__;
        },
    
        removeAllFromObject: function( object ) {
    
                for( var prop in object.__observers__ ){
                        object.__observers__[ prop ].forEach( this._removeObserver, this );
                }
    
        },
    
    
    
        /**
         * Adds an observer to the queue and starts resets a timer to fire the queue
         * @param  {Observer} observer - The Observer to add
         * @param  {Array} changes     - Array of changes
         * @return {null}
         */
        _pushObserverToQueue: function( observer, changes ) {
    
                var item = this._queue.find( function( item ){
                        return item.callback === observer.callback && item.context === observer.context;
                } );
    
                if( item ) {
                        item.changes = item.changes.concat( changes );
                } else {
    
                        this._queue.push( {
                                callback : observer.callback,
                                context  : observer.context,
                                changes  : changes
                        });
                }
    
                window.clearTimeout( this._queueTimer );
                this._queueTimer = window.setTimeout( this._fireQueuedCallbacks.bind( this ), 2 );
    
        },
    
        /**
         * Fires the queued callbacks from the changed/setters
         * @return {null}
         */
        _fireQueuedCallbacks: function() {
    
                for( var i = 0, l = this._queue.length; i < l; i++ ) {
    
                        var item = this._queue.shift();
    
                        if( item ) {
                                item.callback.call( item.context, item.changes, item.options );
                        }
                }
    
                // If things got added while we were running the _queue, run it again!
                if( this._queue.length > 0 ) {
                        this._fireQueuedCallbacks();
                }
        },
    
    
        /**
         * Fired when an observed property changes
         * Handles reattaching child observers and queuing/firing callbacks
         * @param  {Observer} observer   - The observer that changed
         * @param  {Array|Object} change - The change object(s)
         * @return {null}
         */
        _onObservedPropChange: function( observer, change ) {
    
                // Always make changes an array
                var changes = ( change instanceof Array ) ? change: [ change ];
    
                this._reattachOnChange( changes );
    
                if( observer.paused ) {
                        observer.__lastChange__ = change;
                        return;
                }
    
                if( observer.options && observer.options.observeLastProperty && ! observer.options.lastProperty ) {
                        return;
                }
    
                if( observer.sync ){
                        observer.callback.call( observer.context, changes, observer.options );
                } else {
                        gms.Observe._pushObserverToQueue( observer, changes );
                }
        },
    
        /**
         * Reattaches all child observers to a changed object
         * @param  {Array} changes - Array of changes
         * @return {null}
         */
        _reattachOnChange: function( changes ) {
    
                changes.forEach( function( change ){
                        this._reattachChildObservers( change.object, change.name, change.oldValue );
                }, this );
    
        },
    
        _reattachChildObservers: function( object, property, oldValue ) {
    
                if( typeof object !== 'object' || object === null ) {
                        return;
                }
    
                if( !oldValue || oldValue.__observers__ === undefined ) {
                        return;
                }
    
                var observersToRemove = [];
    
                // Loop through all the old observers
                for( var observedProp in oldValue.__observers__ ) {
    
                        var observers = oldValue.__observers__[ observedProp ];
    
                        // Reattach them all
                        observers.forEach( function( observer ) {
    
                                if( observer.context === oldValue ) {
                                        observer.context = object[ property ];
                                }
    
                                // Use the right observe function ( sync/async )
                                var observeFunction = ( observer.sync ) ? this.propertySync : this.property;
    
                                // Reattach the observers to this property
                                observeFunction.call( this, object[ property ], observedProp, observer.callback, observer.context );
    
                                // Add this observer to the list to remove
                                observersToRemove.push( observer );
    
                        }, this );
    
                        this._reattachChildObservers( object[ property ], observedProp, oldValue[ observedProp ] );
                }
    
                // Remove all the old observers
                observersToRemove.forEach( this._removeObserver, this );
        },
    
    
        /**
         * Removes a single observer
         * @param  {Observer} observer - The observer to remove
         * @return {null}
         */
        _removeObserver: function( observer ) {
    
                if( ! observer ) {
                        return;
                }
    
                var object    = observer.object,
                        property  = observer.property;
    
                if( ! object ) {
                        return;
                }
    
                var observerIndex = object.__observers__[ property ].findIndex( function( o ) {
                  return o === observer;
                });
    
                object.__observers__[ property ].splice( observerIndex, 1 );
    
                // If there are no observers left, stop observing the object
                var hasNoKeys = Object.keys( object.__observers__ ).length === 0;
    
                if( hasNoKeys ) {
                        if( observer.nativeObserve ) {
                                Object.unobserve( observer.object, observer.callback );
                        }
                }
        },
    };
    
    
    var object = {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: 5 
    };
    
    var changes;
    var callbackFunction = function( changes ) {
       changes = changes;
       console.log( changes );
    debugger;
    deferred.resolve();
    }

Test runner

Ready to run.

Testing in
TestOps/sec
Object.observe
// async test
gms.Observe.property( object, 'a', callbackFunction );
object.a = 2;
ready
Getters-setters
// async test
debugger;
gms.Observe.property( object, 'a', callbackFunction, null, { useNative: false } );
object.a = 2;
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.