*Update*
New syntax is now working 100%
Introduction
Generic collections in .NET, in combination with LINQ, are powerful tools for the C# or VB.NET developer; however, nothing like it comes with JavaScript. This code provides the beginnings of a JavaScript implementation for a Generic List. Fortunately, due to JavaScript's flexibility, this can be achieved with a relatively small amount of code.
Using the code
When instantiated the List class will automatically keep track of each instance which will be available in the static $List$Instances member. Each instance is assigned a machine key ( $key ) property which will be used to store and retrieve the instance in the scope from which the List class was imported. Each instance automatically disposes when the window is unloaded.
The following public functions with a description of each are currently supported:
Add
: Add an element to the end of the list.AddRange
: Adds a range of elements to the list ElementAt
: Get the element at a specific index in the list. Where
: Return a copy of this List object with only the elements that meet the criteria. FirstOrDefault
: Return the first object in the list that meets the 'query' criteria or null if meet the given query LastOrDefault
: Return the last object in the list that meets the 'query' criteria or null if none meet the given query First
: Return the fire element in the listLast
: Return the last element in the list Count
: Return the number of elements in the list. Clear
: Empties the list of elements but keeps the
associated type. IndexOf
: Returns the index of the element if contained in the list or -1 LastIndexOf
: Returns the last index of the element if contained in the list or -1
Contains
: Return a bool indicating if the element is contained in the list Remove
: Removes an element from the list RemoveAll
: Removes all specified element from the list which meet the given criteria RemoveAt
: Removes an element from the list at the specified index RemoveRange
: Removes elements from the list starting at the given index count times CopyTo
: Copies the list to the specified target Count
: Return the number of elements in the list. Sort
: Sorts the elements in the entire List using the specified comparer or genericSort. Single
: Returns the first object in the list that meets the 'query' criteria or null if no objects are found.
SkipWhile
: Runs the given query on all elements in the list and returns a list containing the elements which were skipped OrderBy
: Order (ascending) the objects in the list by the given object property name. OrderByDescending
: Order (descending) the objects in the list by the given object property Reverse
: Gets a copy of the list in reverse order TrueForAll
: Determines weather every element in the list matches the given predicate Distinct
: Gets a copy of the list containing only unique entries Dispose
: Erases all elements and members from the list and remove this instance from the List instance memory
(called via event window.unload
).
The following public properties with a description of each are currently supported:
array
: The native JavaScript Array which the List instance uses for storage $key
: The machine key which identifies the List instance in the
List.instances
member. type
: The constructor function which all elements in the list must point at.
Example of using Car
objects to fill the List:
(function () {
var $List$Created = -1,
$List$Instances = {},
$List$Dispose = (function (who, disposing) {
try {
if (typeof who === 'number') who = $List$Instances[who];
disposing = disposing || this === window;
$List$Instances[who.$key] = null;
delete $List$Instances[who.$key];
if (disposing && Object.keys($List$Instances).length === 0) {
(function () {
setTimeout((function () {
List = null;
return delete List;
}), 0);
})();
}
} catch (E) { }
});
function List() {
var key = ++$List$Created,
oType = undefined,
$containsLastResult = undefined,
listArray = [];
$List$Instances[key] = this;
if (Object.defineProperty) {
Object.defineProperty(List.prototype, 'array',
{
enumerable: true,
configurable: true,
get: function () {
return listArray;
},
set: function (value) {
if (value instanceof Array) {
if (value.length) {
value.forEach(function (v) { if (v.constructor !== oType) throw "Only one object type is allowed in a list"; });
}
listArray = value;
}
}
});
Object.defineProperty(List.prototype, '$key',
{
configurable: true,
get: function () {
return key;
}
});
Object.defineProperty(List.prototype, '$type',
{
enumerable: true,
configurable: true,
get: function () {
return oType;
}
});
} else {
this.$type = oType;
this.array = listArray;
this.$key = key;
}
function validate(object) {
if (!oType) {
oType = object.constructor;
}
else if (object.constructor !== oType) {
throw "Only one object type is allowed in a list";
}
}
function select(query) {
if (!query) return this;
var bind = (query instanceof Function) && query.toString().indexOf('this') !== -1,
pass = !bind && typeof query !== 'string';
selectList = new List();
listArray.forEach(function (tEl) {
var result = undefined;
if (bind) result = (query.bind(tEl)());
else if (pass) {
try { result = (query(tEl)); }
catch (e) {
try { with (tEl) result = (query(tEl)); }
catch (e) { result = false; }
}
}
else with (tEl) result = eval(query);
if (result) selectList.Add(tEl);
});
return selectList;
}
function genericSort(property) {
return function (a, b) {
return (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
}
}
this.Add = function (object) {
validate(object);
listArray.push(object);
return this;
}
this.AddRange = function (arrayOrList) {
if (!arrayOrList) return;
if (arrayOrList instanceof List) arrayOrList = arrayOrList.array;
try {
arrayOrList.forEach(function (tEl) {
this.Add(tEl);
}, this);
} catch (e) { throw e; }
return this;
}
this.Clear = function () { listArray = []; return this; }
this.CopyTo = function (source) {
if (!source) return;
if (source instanceof List) {
listArray.forEach(function (tEl) {
source.Add(tEl);
});
} else if (source instanceof Array) {
listArray.forEach(function (tEl) {
source.push(tEl);
});
} else {
listArray.forEach(function (tEl) {
source[tEl.toString()] = tEl;
});
}
return source;
}
this.InsertRange = this.Insert = function (where, what) {
if (where < 0 || where >= listArray.length) throw "Invalid index parameter in call to List.Insert (arguments[0] = '" + where + "')";
if (!what) return this;
try {
if (what.length) what.forEach(validate)
else validate(what);
listArray.splice(where, 0, what);
} catch (e) { throw e; }
}
this.Sort = function (comparer) {
comparer = comparer || genericSort;
try { listArray.sort(comparer); }
catch (e) { throw e; }
}
this.All = function (query) { return query ? this.Count() === this.Where(query).Count() : false; }
this.Remove = function (what) {
var all = arguments[1] || false,
where = arguments[2] || undefined,
howMany = arguments[3] || 1,
results = new List();
if (where < 0 || where >= listArray.length) throw "Invalid index parameter in call to List.Remove (arguments[3] = '" + where + "')";
if (!where && this.Contains(what)) {
try { results.AddRange(listArray.splice($containsLastResult, howMany)); }
catch (e) { throw e; }
}
else if (where) {
try { results.AddRange(listArray.splice(where, howMany)); }
catch (e) { throw e; }
}
if (all && this.Contains(what)) {
try { results.AddRange(this.Remove(undefined, undefined, $containsLastResult)); }
catch (E) { throw e; }
}
return results;
}
this.RemoveAt = function (index) {
if (!index) return this;
return this.Remove(undefined, undefined, index, 1);
}
this.RemoveRange = function (index, count) {
if (!index) return this;
return this.Remove(undefined, undefined, index, count || 1);
}
this.RemoveAll = function (query) {
if (!query) return this;
return this.Where(query).ForEach(this.Remove);
}
this.ElementAt = function (index) {
if (index >= listArray.length || index < 0) throw "Invalid index parameter in call to List.ElementAt";
return listArray[index];
}
this.Where = function (query) { return query ? select(query) : null; }
this.FirstOrDefault = function (query) {
var list = select(query),
last = arguments[1] || false;
return list ? list.ElementAt(last ? listArray.length - 1 : 0) : null;
}
this.Count = function (what) {
if (!what) return listArray.length;
else {
var count = 0;
listArray.forEach(function (el) { if (el === what) ++count; });
return count;
}
}
this.Reverse = function (index, count) {
index = index || 0;
count = count || listArray.length - 1;
if (index < 0 || count >= listArray.length) throw "Invalid index or count parameter in call to List.Reverse";
for (; count > index; ++index, --count) {
var temp = listArray[index];
listArray[index] = listArray[count];
listArray[count] = temp;
}
return;
}
this.ToArray = function () { return Array(listArray); }
this.OrderBy = function (property) {
var l = new List(listArray.slice(0).sort(genericSort(property))),
desc = arguments[1] || false;
if (desc) l.Reverse();
return l;
}
this.OrderByDescending = function (property) { return property ? this.OrderBy(property, true) : null; }
this.Contains = function (object, start) {
if (!object) return false;
var contained = false,
keys = Object.keys(object);
start = start || 0;
listArray.forEach(function (tEl) {
keys.forEach(function (key, index) {
if (index < start) return;
try {
if (contained = (tEl[key] === object[key])) $containsLastResult = index;
else $containsLastResult = -1;
} catch (e) { contained = false; $containsLastResult = -1; }
});
});
return contained;
}
this.IndexOf = function (what, start) {
if (!what) return -1;
else if (start < 0) throw "Invalid start paramater given in List.IndexOf";
try { return this.Contains(what, start || 0) ? $containsLastResult : -1; }
catch (e) { return -1; }
}
this.LastIndexOf = function (what, start, end) {
var lastIndex = -1;
if (!what) return lastIndex;
start = start || 0;
end = end || listArray.length;
if (start < 0 || end >= listArray.length) throw "Invalid start or end parameter in call to List.LastIndexOf";
while (this.Contains(what, start++) && start < end) lastIndex = $containsLastResult;
return lastIndex;
}
this.Distinct = function () {
var results = new List();
try {
listArray.forEach(function (tEl) { if (!results.Contains(tEl)) results.Add(tEl); });
} catch (E) { }
return results;
}
this.ForEach = function (query, start, end) {
if (!query) return;
start = start || 0;
end = end || listArray.length - 1;
if (start < 0 || end >= listArray.length) throw "Invalid start or end parameter in call to List.ForEach";
listArray.forEach(function (tEl, index) {
if (start > index || end < index) return;
with (tEl) query();
});
}
this.TrueForAll = function (query) { return listArray.length === this.Where(query).Count(); }
this.First = function () { return listArray.length ? listArray[0] : null; }
this.Last = function () { return listArray.length ? listArray[listArray.length - 1] : null; }
this.Any = function (query) {
var result = Boolean(query);
if (!result) return false;
result = false;
try {
listArray.forEach(function (tEl) {
with (tEl)
if (query()) {
result = true;
throw new Error();
}
});
} catch (E) { }
return result;
}
this.LastOrDefault = function (query) { return query ? this.FirstOrDefault(query, true) : null; }
this.Single = function (query) { return query ? this.FirstOrDefault(query) : null; }
this.SkipWhile = function (query, start) {
if (!query) return this;
start = start || 0;
if (start < 0 || start >= listArray.length) throw "Invalid start parameter in call to List.SkipWhile";
var results = new List();
listArray.forEach(function (tEl, index) {
if (index < start) return;
with (tEl) if (!query()) results.Add(tEl);
});
return results;
}
if (arguments[0] && arguments[0] instanceof List) {
oType = arguments[0].$type;
listArray = arguments[0].array;
} else if (arguments[0] && arguments[0].length) {
try {
oType = arguments[0][0].constructor;
arguments[1] = arguments[1] || arguments[0];
} catch (e) { }
};
if (arguments[1] && arguments[1].length) {
try {
this.AddRange(arguments[1]);
} catch (e) { throw e; }
}
for (var p in this) if (!this.hasOwnProperty(p)) delete this.p;
window.addEventListener('unload', function (self) { $List$Dispose(self, true); } (this));
return Object.freeze(this);
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (elt ) {
var len = this.length,
from = Number(arguments[1]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0) from += len;
for (; from < len; from++)
if (from in this && this[from] === elt)
return from;
return -1;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (fn, bind) {
for (var i = 0, l = this.length; i < l; ++i) {
if (i in this) fn.call(bind, this[i], i, this);
}
}
}
if (!Object.keys) {
Object.keys = function (that) {
var results = [];
for (var p in that)
if (that.hasOwnProperty(p))
results.push(that.p);
return results;
};
}
if (!Object.freeze) {
Object.freeze = function (object) { };
}
Object.freeze(window.List = List);
})();
Points of Interest
Originally written by Shawn Lawsure on CodeProject Here I found it to be somewhat lacking, I modified the code to utilize newer constructs as well as support additional methods which were inline with the initial goal of being compatible with the Generic List implementation in .Net.
The original implementation did not have a Contains method or a Distinct method and used duplicate code for OrderByDescending.
I added various things such as:
Instance Tracking, Public Properties, Destructor support and I managed the List object prototype to ensure memory is reasonable for each instance. I also improved the type checking because the original author did not understand how typeof works in JavaScript or at least did not account on it being used for two object literals in his code. This code allows any literals to be given or classes to the Add function and will handle any type in the Distinct function. I have also updated the demo to show the new methods and I have also shown additional variations of how to use the LINQ Syntax.
The only further improvement I would be able to see would be the addition of the Array.prototype members to be available from the list either by alias et al. I would also imagine that adding the same methods which are available from the .Net implementation would be helpful for some.
It works on Chrome, IE 9, Safari, Android and Opera it works fine however on FireFox it just does not work however no exception is produced... It has something to do with the getters and setters I think but it is late and I want to go to bed so I can figure that out later.
I am not sure the use of freeze is warranted but it looks cool and gave me a place to try it out...
Let me / us know what you think!
History
Version 1 uploaded on May 4, 2012. -> Here
Version 1.5 uploaded on May 5, 2012. @ This article
Version 2.0 uploaded on May 7, 2012 @ This article. (Included new demo)
Converted into NetJS Library on May 9, 2012