Crossbrowser Array Generics

By crisp on Monday 20 August 2007 00:06 - Comments (2)
Category: Javascript, Views: 8.457

I briefly demonstrated how one can use an array method on non-array objects such as nodeLists or the arguments variable in my previous entry on getElementsByClassName (and Menno van Slooten did me a favour by explaining it on his blog). I also used the Array generic Array.filter there and left those with a promise to explain how these techniques can be implemented crossbrowser.

Basically the filter method is part of Mozilla's Array Extras - a nice collection of native methods for array instances available in JavaScript 1.6 (supported by Firefox 1.5 and beyond). Each method has a seperate page explaining the method and provides a crossbrowser alternative.

Generics are shortcuts to these and the other native methods with which you can apply these methods to other types of objects, so instead of

JavaScript:
1
[].slice.call(someArrayLikeObject, 0);


or

JavaScript:
1
Array.prototype.slice.call(someArrayLikeObject, 0);


you can simple use:

JavaScript:
1
Array.slice(someArrayLikeObject, 0);



Ain't that cool?

It would be much cooler if we could use those techniques in all modern browsers. Luckily Erik Arvidsson already gave us his version of JS Generics back in 2006(!) I also found a demonstration of crossbrowser generics in Dan Webb's Scripting Essentials (which is a summary of techniques and methods that I also use on a daily basis, be it my own versions).

So even though this is not something completely new I thought I might just as well brush up and present my own version which slightly differs from Erik's and Dan's.

First of all I use a modified version of prototype's Object.extend for all object extensions. Why a modified version? Well, prototype's version by default overwrites any existing properties and methods which I feel is a bit obtrusive not to mention foolish in case you are actually overwriting a native implementation of a method. So I take the liberty of overwriting prototype's Object.extend with this version:


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.extend = function(dest, source, allowOverwrite)
{
    for (var prop in source)
    {
        if (source.hasOwnProperty(prop)
            && (allowOverwrite || !dest.hasOwnProperty(prop)))
        {
            dest[prop] = source[prop];
        }
    }

    return dest;
}



Next I use this method to extend the Array object with prototyped versions of the extra array methods:


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
Object.extend(Array.prototype,
{
    indexOf: function(search, from)
    {
        var i = from || 0;

        if (i < 0)
        {
            i += this.length;
            if (i < 0)
                i = 0;
        }

        for (; i < l; i++)
        {
            if (this[i] === search)
                return i;
        }

        return -1;
    },
    lastIndexOf: function(search, from)
    {
        var i = this.length - 1;

        if (from != undefined)
        {
            if (from < 0)
                i += from + 1;
            else if (from < i)
                i = from;
        }

        for (; i >= 0; i--)
        {
            if (this[i] === search)
                return i;
        }

        return -1;
    },
    forEach: function(func, obj)
    {
        for (var i = 0, l = this.length; i < l; i++)
        {
            if (i in this)
                func.call(obj, this[i], i, this);
        }
    },
    map: function(func, obj)
    {
        var res = [];
        for (var i = 0, l = this.length; i < l; i++)
        {
            if (i in this)
                res[i] = func.call(obj, this[i], i, this);
        }

        return res;
    },
    filter: function(func, obj)
    {
        var res = [], val;
        for (var i = 0, l = this.length; i < l; i++)
        {
            if (i in this)
            {
                val = this[i]; // in case func mutates this
                    if (func.call(obj, val, i, this))
                    res.push(val);
            }
        }

        return res;
    },
    some: function(func, obj)
    {
        for (var i = 0, l = this.length; i < l; i++)
        {
            if (i in this && func.call(obj, this[i], i, this))
                return true;
        }

        return false;
    },
    every: function(func, obj)
    {
        for (var i = 0, l = this.length; i < l; i++)
        {
            if (i in this && !func.call(obj, this[i], i, this))
                return false;
        }

        return true;
    }
});



Finally I borrowed Dan's clever way (using forEach) to make these and the native methods into generics:


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
['join', 'reverse', 'sort', 'push', 'pop', 'shift', 'unshift', 'splice',
'concat', 'slice', 'indexOf', 'lastIndexOf', 'forEach', 'map', 'filter',
'some', 'every'].forEach(
    function(func)
    {
        if (!(func in Array) && func in Array.prototype)
        {
            Array[func] = function(obj)
            {
                return this.prototype[func].apply(
                    obj,
                    Array.prototype.slice.call(arguments, 1)
                );
            }
        }
    }
);



And that's all there is to it. You can find the complete sourcecode here - use it as you like.

Volgende: Microsoft, where are you? 09-'07 Microsoft, where are you?
Volgende: getElementsByClassName re-re-re-visited 08-'07 getElementsByClassName re-re-re-visited