How can I implement an array with an indexer in JavaScript? Is there something like a dictionary in .Net?
In my last project, I was tasked with creating a browser client application that would read 10 of thousands of rows of data, then group and aggregate the data for display in grids and for charting. The target technologies were HTML 5, CSS 3 and EMCS 5.(modern browser in Jun 2013). Because older browser compatibility was not a concern the external libraries were limited to D3 (no JQuery).
I needed to build a data model. I had built one before in C# and relied on custom dictionary objects to quickly access the data, groups, and aggregates. I had not worked in JavaScript in years so I started searching for a dictionary. I found JavaScript still did not have a true native dictionary. I found a few sample implementations but nothing that really met my expectation. So I built one.
As I mentioned, I had not worked in JavaScript for years. The advancements (or maybe just the web availability of information) were quite impressive. All my previous work was with class based languages so the prototype base language took some time to get used to (and I still have a long way to go).
This project, like most, was due before it started so I learned as I went making many of the newb mistakes that would be expected when transitioning from a class based to a prototype based language. The dictionary created was functional but after a time, I realized some improvements I could make by making it less newbish. The project ran out of funding before I had time to rework the dictionary. Oh, and my position lost funding at the same time (amazing how that can happen). So I decide to recreate the dictionary using what I had learned and determine if the dictionary was actually a performance improvement over an array.
/*
* Dictionary Factory Object
* Holds common object functions. similar to V-Table
* this.New() used to create new dictionary objects
* Uses Object.defineProperties so won't work on older browsers.
* Browser Compatibility (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties)
* Firefox (Gecko) 4.0 (2), Chrome 5, IE 9, Opera 11.60, Safari 5
*/
function Dict() {
/*
* Create a new Dictionary
*/
this.New = function () {
return new dict();
};
/*
* Return argument f if it is a function otherwise return undefined
*/
function ensureF(f) {
if (isFunct(f)) {
return f;
}
}
function isFunct(f) {
return (typeof f == "function");
}
/*
* Add a "_" as first character just to be sure valid property name
*/
function makeKey(k) {
return "_" + k;
};
/*
* Key Value Pair object - held in array
*/
function newkvp(key, value) {
return {
key: key,
value: value,
toString: function () { return this.key; },
valueOf: function () { return this.key; }
};
};
/*
* Return the current set of keys.
*/
function keys(a) {
// remove the leading "-" character from the keys
return a.map(function (e) { return e.key.substr(1); });
// Alternative: Requires Opera 12 vs. 11.60
// -- Must pass the internal object instead of the array
// -- Still need to remove the leading "-" to return user key values
// Object.keys(o).map(function (e) { return e.key.substr(1); });
};
/*
* Return the current set of values.
*/
function values(a) {
return a.map(function(e) { return e.value; } );
};
/*
* Return the current set of key value pairs.
*/
function kvPs(a) {
// remove the leading "-" character from the keys
return a.map(function (e) { return newkvp(e.key.substr(1), e.value); });
}
/*
* Returns true if key exists in the dictionary.
* k - Key to check (with the leading "_" character)
*/
function exists(k, o) {
return o.hasOwnProperty(k);
}
/*
* Array Map implementation
*/
function map(a, f) {
if (!isFunct(f)) { return; }
return a.map(function (e, i) { return f(e.value, i); });
}
/*
* Array Every implementation
*/
function every(a, f) {
if (!isFunct(f)) { return; }
return a.every(function (e, i) { return f(e.value, i) });
}
/*
* Returns subset of "values" where function "f" returns true for the "value"
*/
function filter(a, f) {
if (!isFunct(f)) {return; }
var ret = a.filter(function (e, i) { return f(e.value, i); });
// if anything returned by array.filter, then get the "values" from the key value pairs
if (ret && ret.length > 0) {
ret = values(ret);
}
return ret;
}
/*
* Array Reverse implementation
*/
function reverse(a, o) {
a.reverse();
reindex(a, o, 0);
}
/**
* Randomize array element order in-place.
* Using Fisher-Yates shuffle algorithm.
* (Added just because:-)
*/
function shuffle(a, o) {
var j, t;
for (var i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
t = a[i];
a[i] = a[j];
a[j] = t;
}
reindex(a, o, 0);
return a;
}
/*
* Array Some implementation
*/
function some(a, f) {
if (!isFunct(f)) { return; }
return a.some(function (e, i) { return f(e.value, i) });
}
/*
* Sort the dictionary. Sorts the array and reindexes the object.
* a - dictionary array
* o - dictionary object
* sf - dictionary default sort function (can be undefined)
* f - sort method sort function argument (can be undefined)
*/
function sort(a, o, sf, f) {
var sf1 = f || sf; // sort function method used if not undefined
// if there is a customer sort function, use it
if (isFunct(sf1)) {
a.sort(function (e1, e2) { return sf1(e1.value, e2.value); });
}
else {
// sort by key values
a.sort();
}
// reindex - adds O(n) to perf
reindex(a, o, 0);
// return sorted values (not entire array)
// adds O(n) to perf
return values(a);
};
/*
* forEach iteration of "values"
* uses "for" loop to allow exiting iteration when function returns true
*/
function forEach(a, f) {
if (!isFunct(f)) { return; }
// use for loop to allow exiting early and not iterating all items
for(var i = 0; i < a.length; i++) {
if (f(a[i].value, i)) { break; }
}
};
/*
* forEachR iteration of "values" in reverse order
* uses "for" loop to allow exiting iteration when function returns true
*/
function forEachR(a, f) {
if (!isFunct(f)) { return; }
// use for loop to allow exiting early and not iterating all items
for (var i = a.length - 1; i > -1; i--) {
if (f(a[i].value, i)) { break; }
}
}
/*
* Add a new Key Value Pair, or update the value of an existing key value pair
*/
function add(key, value, a, o, resort, sf) {
var k = makeKey(key);
// Update value if key exists
if (exists(k, o)) {
a[o[k]].value = value;
}
else {
// Add a new Key value Pair
var kvp = newkvp(k, value);
o[kvp.key] = a.length;
a.push(kvp);
}
// resort if requested
if (resort) { sort(a, o, sf); }
};
/*
* Removes an existing key value pair and returns the "value" If the key does not exists, returns undefined
*/
function remove(key, a, o) {
var k = makeKey(key);
// return undefined if the key does not exist
if (!exists(k, o)) { return; }
// get the array index
var i = o[k];
// get the key value pair
var ret = a[i];
// remove the array element
a.splice(i, 1);
// remove the object property
delete o[k];
// reindex the object properties from the remove element to end of the array
reindex(a, o, i);
// return the removed value
return ret.value;
};
/*
* Returns true if key exists in the dictionary.
* k - Key to check (without the leading "_" character)
*/
function keyExists(k, o) {
return exists(makeKey(k), o);
};
/*
* Returns value assocated with "key". Returns undefined if key not found
*/
function item(key, a, o) {
var k = makeKey(key);
if (exists(k, o)) {
return a[o[k]].value;
}
}
/*
* changes index values held by object properties to match the array index location
* Called after sorting or removing
*/
function reindex(a, o, i){
for (var j = i; j < a.length; j++) {
o[a[j].key] = j;
}
}
/*
* The "real dictionary"
*/
function dict() {
var _a = [];
var _o = {};
var _sortF;
Object.defineProperties(this, {
"length": { get: function () { return _a.length; }, enumerable: true },
"keys": { get: function() { return keys(_a); }, enumerable: true },
"values": { get: function() { return values(_a); }, enumerable: true },
"keyValuePairs": { get: function() { return kvPs(_a); }, enumerable: true},
"sortFunction": { get: function() { return _sortF; }, set: function(funct) { _sortF = ensureF(funct); }, enumerable: true }
});
// Array Methods - Only modification to not pass the actual array to the callback function
this.map = function(funct) { return map(_a, funct); };
this.every = function(funct) { return every(_a, funct); };
this.filter = function(funct) { return filter(_a, funct); };
this.reverse = function() { reverse(_a, _o); };
this.shuffle = function () { return shuffle(_a, _o); };
this.some = function(funct) { return some(_a, funct); };
this.sort = function(funct) { return sort(_a, _o, _sortF, funct); };
// Array Methods - Modified aborts when funct returns true.
this.forEach = function (funct) { forEach(_a, funct) };
// forEach in reverse order
this.forEachRev = function (funct) { forEachR(_a, funct) };
// Dictionary Methods
this.addOrUpdate = function(key, value, resort) { return add(key, value, _a, _o, resort, _sortF); };
this.remove = function(key) { return remove(key, _a, _o); };
this.exists = function(key) { return keyExists(key, _o); };
this.item = function(key) { return item(key, _a, _o); };
this.get = function (index) { if (index > -1 && index < _a.length) { return _a[index].value; } } ,
this.clear = function() { _a = []; _o = {}; };
return this;
}
return this;
}
One of the epiphanies I had while trying to mentally reconcile class vs. prototype objects is that the prototype is basically a v-table for the created objects. Additionally functions in an enclosure can also act like v-table entries. As the project progressed, I started using Object Factories where a top level object contained common functions for the object type and included a “this.New(args)” method that was used to create the actual objects used in the solution. This is the style I used for the dictionary.
The core of the dictionary is an Array, an Object and a KeyValuePair object. The “addOrUpdate” method takes a key and a value and:
NOTE: I read the object property names can start with “almost any” Unicode character. The project would be dealing with customer data which can start with “any” Unicode character. To ensure the dictionary did not blowup due to an invalid property name, I prefix an underscore (_) to the key and strip off that underscore when returning keys external to the dictionary.
For the dictionary to be functional, the internal Array and Object must be kept in sync. To ensure this neither the Array nor the Object are exposed externally. I wanted to avoid accidental changes such as those that can happen when a “If” test has only one equal sign and the left have value is set by mistake.
If(dict.KeyObj[“SomeKey”] = “oops”) { alert(“good luck tracing this down:-)”); }
This typical error with the dictionary might be very hard to track down when bugs (the symptoms) start showing up in calculation, display, etc. Consequently the “this” property would not have access to either one. This protectionism is one of the reasons I did not dig more into prototypes. It had crossed my mind to use an internal object with the Array and Object exposed and pass that internal object when using the “call” or “apply” methods and I may look at that later as I’m still not sure I wouldn’t have to expose that internal object which would defeat the purpose of protecting the core Array and Object.
I fixed some of the newb mistakes I made with the first dictionary object I created.
Technically not, but you can use a regular JavaScript object like a dictionary:
var a = {"a":"wohoo", 2:"hello2", "d":"hello"};
alert(a["a"]);
alert(a[2]);
alert(a["d"]);
The nearest implementation that I have used to a .Net dictionary in Javascript is a hash object (see link: http://www.mojavelinux.com/articles/javascript_hashes.html). It implements an array under the hood and has similarly named methods to those of a .Net dictionary.
ECMAScript 6 (aka the 2015 JavaScript spec), specifies a dictionary interface, named Map. It supports arbitrary keys of any type, has a read-only size
property, is not cluttered with prototypes related stuff like objects, and can be iterated over using the new for...of...
construct or Map.forEach
. Check the documentation on the MDN here, and the browser compatibility table here.