Easy JavaScript Generic List Implementation
This is an alternative for "Easy JavaScript Generic List Implementation"
*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 listElementAt
: 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 queryLastOrDefault
: Return the last object in the list that meets the 'query' criteria or null if none meet the given queryFirst
: Return the fire element in the listLast
: Return the last element in the listCount
: 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 -1LastIndexOf
: Returns the last index of the element if contained in the list or -1Contains
: Return a bool indicating if the element is contained in the listRemove
: Removes an element from the listRemoveAll
: Removes all specified element from the list which meet the given criteriaRemoveAt
: Removes an element from the list at the specified indexRemoveRange
: Removes elements from the list starting at the given index count timesCopyTo
: Copies the list to the specified targetCount
: 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 skippedOrderBy
: 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 propertyReverse
: Gets a copy of the list in reverse orderTrueForAll
: Determines weather every element in the list matches the given predicateDistinct
: Gets a copy of the list containing only unique entriesDispose
: Erases all elements and members from the list and remove this instance from the List instance memory (called via eventwindow.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 theList.instances
member.type
: The constructor function which all elements in the list must point at.
Example of using Car
objects to fill the List:
/*******************************************************************************************
Title: Javascript Generic List Implementation
Description: An implementation of a Generic List with LINQ support (based off .NET).
Author: Julius Friedman / Shawn Lawsure
Usage Example:
function Car(make, model)
{
this.make = make;
this.model = model;
}
var myList = new List();
myList.Add(new Car("Honda", "Civic"));
myList.Add(new Car("Nissan", "Sentra"));
myList.Add(new Car("Honda", "Cr-V"));
myList.Add(new Car("Honda", "Cr-V"));
//Where all of the following are equivalent
var selList = myList.Where("make == 'Honda'").OrderByDescending("model").Distinct();
var anotherList = myList.Where(function(){ this.make == 'Honda'}).OrderByDescending("model").Distinct();
var yetAnotherList = myList.Where(function(c){ c.make == 'Honda'}).OrderByDescending("model").Distinct();
var finalList = myList.Where(function(){ make == 'Honda'}).OrderByDescending("model").Distinct();
*********************************************************************************************/
/*namespace GenericList*/(function () {
// =============== Static Members =================================================
var $List$Created = -1, // Id counter used to identify each List instance.
$List$Instances = {}, // Storage for each instance which has yet to be disposed.
//Destructor Logic
$List$Dispose = (function (who, disposing) {
try {
//Ensure who is a List instance
if (typeof who === 'number') who = $List$Instances[who];
//Determine if event was called
disposing = disposing || this === window;
//Remove instance from Type storage
$List$Instances[who.$key] = null;
delete $List$Instances[who.$key];
//Dispose Type in a closure
if (disposing && Object.keys($List$Instances).length === 0) {
//Anonymously
(function () {
//set a timeout for 0 seconds to dispose the List Type
setTimeout((function () {
List = null;
return delete List;
}), 0);
})();
}
} catch (E) { }
});
// Method: Constructor
// Description: Returns a new List instance based on the given parameters.
function List(/*type, array*/) {
// =============== Private Attributes =================================================
var key = ++$List$Created, // Identify each List instance with an incremented Id.
oType = undefined, // Used to ensure that all objects added to the list are of the same type.
$containsLastResult = undefined, //Storage pointer for the last result of Contains call
listArray = []; // Stores all the list data.
$List$Instances[key] = this; // Store instance with key
// =============== Public Properties =================================================
//If supported define public properties on the List instance being created
if (Object.defineProperty) {
// Property: array
// Description: Gets or Sets in Native inner array utilized by the List for storage. The elements contained must be of the same type in which this List instance was created with.
Object.defineProperty(List.prototype, 'array',
{
// writable: false, // True if and only if the value associated with the property may be changed. (data descriptors only). Defaults to false.
enumerable: true, // true if and only if this property shows up during enumeration of the properties on the corresponding object. Defaults to false.
configurable: true, // true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. Defaults to false.
get: function () {
return listArray;
},
set: function (value) {
//Ensure Array
if (value instanceof Array) {
//Ensure length
if (value.length) {
//Ensure types
value.forEach(function (v) { if (v.constructor !== oType) throw "Only one object type is allowed in a list"; });
}
//Set member
listArray = value;
}
}
});
// Property: $key
// Description: Gets the machine key which identifies this List instance in the memory of all created List instances.
Object.defineProperty(List.prototype, '$key',
{
configurable: true,
get: function () {
return key;
}
});
// Property: $key
// Description: Gets the constructor or type in which this List was created with
Object.defineProperty(List.prototype, '$type',
{
enumerable: true,
configurable: true,
get: function () {
return oType;
}
});
} else {
this.$type = oType; // Compatibility
this.array = listArray;
this.$key = key;
}
// =============== Private Methods ====================================================
// Method: validate
// Description: Make sure that all objects added to the List are of the same type.
function validate(object) {
//If we have not yet determined a type it is determined by the first object added
if (!oType) {
oType = object.constructor;
}
else if (object.constructor !== oType) {
throw "Only one object type is allowed in a list";
}
}
// Method: select
// Description: Return a copy of this List object with only the elements that meet the criteria
// as defined by the 'query' parameter.
// Usage Example:
// var selList = select("make == 'Honda'").
// var anotherList = select(function(){ return this.make === 'Honda' });
// var yetAnotherList = select(function(c){ return c.make === 'Honda' });
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();
//possibly need to bind query on this if query instanceof function
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;
}
// Method: genericSort
// Description: Sort comparison function using an object property name. Pass this function to
// the Javascript sort function to sort the list by a given property name.
// Usage Example:
// var sortedList = listArray.sort(genericSort('model'));
function genericSort(property) {
return function (a, b) {
return (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
}
}
// =============== Public Methods ======================================================
// Method: Add
// Description: Add an element to the end of the list.
this.Add = function (object) {
validate(object);
listArray.push(object);
return this;
}
// Method: AddRange
// Description: Adds each of the elements in the given Array or List to this List instance
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;
}
// Method: Clear
// Description: Removes all elements from this List instance
this.Clear = function () { listArray = []; return this; }
// Method: CopyTo
// Description: Adds all elemets in this List to the given array, List or Object. Returns the source
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;
}
// Method: Insert
// Description: Inserts an element into the List at the specified index (where).
// Insert and InsertRange are the same physcially. (They alias eachother)
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; }
}
// Method: Sort
// Description: Sorts the elements in the entire List using the specified comparer or genericSort
this.Sort = function (comparer) {
comparer = comparer || genericSort;
try { listArray.sort(comparer); }
catch (e) { throw e; }
}
// Method: All
// Description: returns true if all elements in the List match the given query
this.All = function (query) { return query ? this.Count() === this.Where(query).Count() : false; }
// Method: Remove
// Description: Removes occurrences of a specific object from the List or a given amount.
this.Remove = function (what/*, all, where, howMany */) {
//Determine arguments for logic
var all = arguments[1] || false,
where = arguments[2] || undefined,
howMany = arguments[3] || 1,
results = new List();
//Determine branch
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;
}
// Method: RemoveAt
// Description: Removes the element at the specified index. Returns the list if nothing is removed or the removed items.
this.RemoveAt = function (index) {
if (!index) return this;
return this.Remove(undefined, undefined, index, 1);
}
// Method: RemoveRange
// Description: Removes a range of elements from the List starting at the given index count || 1 time(s). Returns the List if nothing is removed or the removed items.
this.RemoveRange = function (index, count) {
if (!index) return this;
return this.Remove(undefined, undefined, index, count || 1);
}
// Method: RemoveAll
// Description: Removes all the elements that match the conditions defined by the specified predicate. Returns the List if nothing is removed or the removed items.
this.RemoveAll = function (query) {
if (!query) return this;
//Run Where with the query to get results then pass the resulting List to ForEach on this instances Remove function
return this.Where(query).ForEach(this.Remove);
}
// Method: ElementAt
// Description: Get the element at a specific index in the list.
this.ElementAt = function (index) {
if (index >= listArray.length || index < 0) throw "Invalid index parameter in call to List.ElementAt";
return listArray[index];
}
// Method: Where
// Description: Return a copy of this List object with only the elements that meet the criteria
// as defined by the 'query' parameter.
this.Where = function (query) { return query ? select(query) : null; }
// Method: FirstOrDefault
// Description: Return the first object in the list that meets the 'query' criteria or null if no objects are found.
this.FirstOrDefault = function (query/*, last*/) {
var list = select(query),
last = arguments[1] || false;
return list ? list.ElementAt(last ? listArray.length - 1 : 0) : null;
}
// Method: Count
// Description: Return the number of elements in the list or optionally the number of elements which are equal to the object given
this.Count = function (what) {
if (!what) return listArray.length;
else {
var count = 0;
listArray.forEach(function (el) { if (el === what) ++count; });
return count;
}
}
//Method: Reverse
//Description: Reverses the list starting at the given index || 0 count times.
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;
}
//Method: ToArray
//Description: Copies the elements of the List to a new array.
this.ToArray = function () { return Array(listArray); }
// Method: OrderBy
// Description: Order (ascending) the objects in the list by the given object property name.
this.OrderBy = function (property/*, desc*/) {
//Make the list and the interval array
var l = new List(listArray.slice(0).sort(genericSort(property))),
//Determine if we need to reverse
desc = arguments[1] || false;
if (desc) l.Reverse();
//return
return l;
}
// Method: OrderByDescending
// Description: Order (descending) the objects in the list by the given object property name.
this.OrderByDescending = function (property) { return property ? this.OrderBy(property, true) : null; }
// Method: Contains
// Description: Determines if the given object is contained in the List. sets the private $containsLastResult to the index found for the given object.
this.Contains = function (object, start) {
if (!object) return false;
var contained = false,
keys = Object.keys(object);
start = start || 0;
//Iterate list
listArray.forEach(function (tEl) {
//Iterate keys
keys.forEach(function (key, index) {
if (index < start) return;
//Try to ascertain equality
try {
//contained is equal to the expression of tEl[key] being exactly equal to object[key]'s value
if (contained = (tEl[key] === object[key])) $containsLastResult = index; //Store the last index if contained
else $containsLastResult = -1;
} catch (e) { contained = false; $containsLastResult = -1; }
});
});
return contained;
}
// Method: IndexOf
// Description: Returns the index of the specified element in this List or -1 if not found
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; }
}
// Method: LastIndexOf
// Description: Returns the last index of the specified element in this List or -1 if not found
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;
}
// Method: Distinct
// Description: Gets a copy of the list with only unique elements.
this.Distinct = function () {
var results = new List();
try {
//this.ForEach(function (tEl) { if (!results.Contains(tEl)) results.Add(tEl); });
//Equivelant to the following except the below is native
listArray.forEach(function (tEl) { if (!results.Contains(tEl)) results.Add(tEl); });
} catch (E) { }
return results;
}
// Method: ForEach
// Description: Calls the given query on each element of the List
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();
});
}
// Method: TrueForAll
// Description: etermines whether every element in the List matches the conditions defined by the specified predicate.
this.TrueForAll = function (query) { return listArray.length === this.Where(query).Count(); }
// Extension Methods
// Method: First
// Description: returns the first element in the List or null if nothing is contained
this.First = function () { return listArray.length ? listArray[0] : null; }
// Method: Last
// Description: returns the last element in the List or null if nothing is contained
this.Last = function () { return listArray.length ? listArray[listArray.length - 1] : null; }
// Method: Any
// Description: returns true on the first element in the List which meets the given query.
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;
}
// Method: LastOrDefault
// Description: Return the last object in the list that meets the 'query' criteria or null if no objects are found.
this.LastOrDefault = function (query) { return query ? this.FirstOrDefault(query, true) : null; }
// Method: Single
// Description: Returns the first object in the list that meets the 'query' criteria or null if no objects are found.
this.Single = function (query) { return query ? this.FirstOrDefault(query) : null; }
// Method: Single
// Description: Bypasses elements in a sequence as long as a specified condition is true and then returns the remaining elements. The element's index is used in the logic of the predicate function.
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;
}
//Copy constructor (utilized if first parameter in constructor is a List instance
if (arguments[0] && arguments[0] instanceof List) {
oType = arguments[0].$type; // Set the type of the List from the given
listArray = arguments[0].array; // Set the inner array of the List from the given
} else if (arguments[0] && arguments[0].length) { //If there is a type given it may be contained in an array which is to be used as the interal array..
try {
oType = arguments[0][0].constructor; // Set type of the List from the first element in the given array
arguments[1] = arguments[1] || arguments[0]; // Make a new argument incase one is not given which should be the array given. This will be used after AddRange is constructed to verify each given item complies with the List logic.
} catch (e) { }
};
//If there is an array given then each member of the array must be added and verified
if (arguments[1] && arguments[1].length) {
try {
this.AddRange(arguments[1]);
} catch (e) { throw e; }
}
//Cleanup instance prototype
for (var p in this) if (!this.hasOwnProperty(p)) delete this.p;
//Add event for destructor in executed closure
window.addEventListener('unload', function (self) { $List$Dispose(self, true); } (this));
//freeze new instance
return Object.freeze(this);
}
// Method: indexOf
// Description: adds logic to retrieve the index of an element from an array if present, otherwise -1
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (elt /*, from*/) {
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;
};
}
// Method: forEach
// Description: Allows you to provide a function and optional binding to invoke on each member of the array
// -- Borrowed from MooTools --
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);
}
}
}
// Method: forEach
// Description: Allows you to provide a function and optional binding to invoke on each member of the array
if (!Object.keys) {
Object.keys = function (that) {
var results = [];
for (var p in that)
if (that.hasOwnProperty(p))
results.push(that.p);
return results;
};
}
// Method: freeze
// Description: Does nothing since it requires runtime support or modification of apply which can hinder legacy code
if (!Object.freeze) {
Object.freeze = function (object) { };
}
//Export and Freeze List to the window namespace
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