Search code examples
javascriptarraysclassconstructorimplementation

How to create a constructor which behaves like a true Array?


How can I create a custom array constructor, which is an extended version of the native Array constructor?

jQuery, for example, looks like an array with additional methods, such as $().addClass. However, it didn't modify Array.prototype, because new Array().hasClass is undefined.

So, how can I create an extended array implementation, without modifying Array.prototype?

Example:

Employees( ... )          //-> [{name: 'John', age: 32}, {name: 'Bob', age: 29}];
Employees( ... ).byAge(32)//-> [{name: 'John', age: 32}];
// and
Array().byAge             //-> undefined

Solution

  • jQuery is not a true Array implementation: jQuery instanceof Array is false!

    If you want to create a true instance of an array, and add custom methods, use this code. It uses Function.prototype.bind to call a constructor with an arbitrary number of parameters.

    The implementation behaves exactly as a true array, except at one point:

    • When the Array constructor is called with a single argument, it's creating an array with a length of this argument.
    • Since this feature is often a source of bugs, I have decided to omit it in the implementation. You can still set a length of n by setting the length property.

    Code: http://jsfiddle.net/G3DJH/

    function Employees() {
        // Deal with missing "new"
        if (!(this instanceof Employees)) {
            // Convert arguments to an array, because we have to shift all index by 1
            var args = Array.prototype.slice.call(arguments);
            args.unshift(this); // Shift all indexes, set "this" 
            return new (Function.prototype.bind.apply(Employees, args));
        } else {
            // Set length property.
            var len = arguments.length,
                /*
                 * fn_internalLength: Internal method for calculating the length
                 **/
                fn_internalLength,
                /*
                 * explicitLength: Deals with explicit length setter
                 **/
                explicitLength = 0;
    
            // Setting all numeric keys
            while (len--) {
                this[len] = arguments[len];
            }
    
            // Internal method for defining lengths
            fn_internalLength = function() {
                var allKeys = Object.keys(this).sort(function(x, y) {
                    // Sort list. Highest index on top.
                    return y - x;
                }), i=-1, length = allKeys.length, tmpKey,
                foundLength = 0;
    
                // Loop through all keys
                while (++i < length && (tmpKey = allKeys[i]) >= 0) {
                    // Is the key is an INTEGER?
                    if (tmpKey - tmpKey === 0 && tmpKey % 1 === 0) {
                        foundLength = 1*tmpKey + 1;
                        break;
                    }
                }
                // Return MAX(actual length, explictly set length)
                return foundLength > explicitLength ? foundLength : explicitLength;
            }.bind(this);
    
            // Define the magic length property
            Object.defineProperty(this, 'length',
            {
                get: fn_internalLength,
                set: function(newLength) {
                    var length = fn_internalLength();
                    if (newLength < length) {
                        for (var i=newLength; i<length; i++) {
                            delete this[i];
                        }
                    }
                    // Set explicit length
                    explicitLength = newLength;
                },
                enumerable: false,
                configurable: false
            });
        }
    }
    Employees.prototype = new Array;
    
    
    // Example: Custom method:
    Employees.prototype.print = function() {
        return this.join('--'); // Using inherit Array.prototype.join
    };
    
    // Just like the Array, `new` is optional
    console.log(new Employees(1,2).print());
    console.log(Employees(1,2).print());
    
    // Is the object an array?
    console.log(new Employees() instanceof Array);    // True!
    // Can't believe it?
    console.log(new Employees() instanceof Employees); // True!