2

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
Community
  • 1
  • 1
zanona
  • 12,345
  • 25
  • 86
  • 141
  • 1
    I answered to a pretty similar question: [How does a jQuery instance appear as an array when called in console.log?](http://stackoverflow.com/questions/9657612/how-does-a-jquery-instance-appear-as-an-array-when-called-in-console-log/9657916#9657916) – ZER0 Mar 24 '12 at 18:15

3 Answers3

5

The jQuery object is not an Array, nor does it "overwrite" the Array class. It is simply array-like.

You can see how jQuery accomplishes this by browsing the source; also see Array Like Objects in Javascript and Why Are Array-Like Objects Used in Javascript Over Native Arrays.

Community
  • 1
  • 1
Matt Ball
  • 354,903
  • 100
  • 647
  • 710
1

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!
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • It's not quite a true instance of an Array. You won't get the magic `.length` behavior... but it's probably close enough for most purposes, so +1. –  Mar 24 '12 at 18:20
  • @amnotiam I was also thinking about that. I'll add it:p – Rob W Mar 24 '12 at 18:20
  • @amnotiam Forgot to ping you: I've updated my implementation. Play around with `length` ;) – Rob W Mar 25 '12 at 13:35
  • I think it looks great! It's sort of too bad that the `length` needs to be calculated on request since the internal methods also call the function, but I can't see any way around it, except perhaps with `Proxy` though I can't say that would be any better. –  Mar 25 '12 at 14:55
  • thanks man, that was a great and thorough explanation, I just needed a simpler thing, which @MДΓΓ БДLL answer worked fine in my case, although, it seems to me that your solution ties better with the question title. So I don't want to be unfair. :) thanks again – zanona Mar 25 '12 at 14:59
  • @zanona Mind if I edit the question to reflect the Q&A even better? Your intended question has been asked many times before, but I have never seen this one before. – Rob W Mar 25 '12 at 15:01
  • @Robw definitely, please go for it :) – zanona Mar 25 '12 at 15:06
  • 1
    @RobW: One really small issue that makes it differ from an array... If you add a property that uses hexadecimal notation like `"0x23"`, the `length` getter will cause it to be converted to a base 10 number, which could affect the result *(assuming the resulting number is greater than the proper length)*. Really small edge case though. Could be solved of course with `parseInt` and the `10` radix arg. –  Mar 25 '12 at 15:16
0

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

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

Using the costly Object.setPrototypeOf you can create an Object which inherits Array and supports automatic length changing,

function MyArray() {
    var arr = Array.apply(null, arguments); // Array never cares about the custom `this` :(
    Object.setPrototypeOf(arr, MyArray.prototype); // fix prototype chain
    // any futher custom modification of `arr` which isn't inherited goes here..
    return arr;
}
MyArray.prototype = Object.create(Array.prototype); // safe inherit from `Array.prototype`

// now using it, set a custom inherited method
MyArray.prototype.foo = function () {return 'bar';};
// construct
var x = MyArray(1, 2, 3); // [1, 2, 3]

// length starts normally
x.length; // 3

// length automatically increases
x[3] = 4;
x.length; // 4

// can use length to trim
x.length = 1;
x[1]; // undefined

// have custom inherited properties
x.foo(); // "bar"

// but have not modified normal Arrays
try {
    [].foo(); // throws a TypeError
} catch (e) {
    console.log(e); // TypeError [].foo (undefined) is not a function
}

// finally, have
x instanceof MyArray; // true
x instanceof Array; // true

If the automatic properties of length aren't important to you, you can get away with Array-like Objects. These will appear like an Array in the Console if they have a length and a splice, but the length will not automatically change as you modify the Object so you can't simply set with bracket notation etc.

Paul S.
  • 64,864
  • 9
  • 122
  • 138