1
\$\begingroup\$

First, I must humbly admit that I've been a developer for many years but I've almost never had to work as a front-end, Web UI developer. I've been given an opportunity to work for an organization as a front-end and back-end web developer and I'm having to learn about the in's-and-out's of HTML/CSS/JS code. This is my first attempt at writing any real JavaScript.

I have a case where I need to create a type of collection object within JS. Specifically, I need to have a list of key-value pairs and I need to be able to readily sort the object for use within the UI.

After much reading on the topic of JavaScript arrays and objects, and how they behave, I've created the following JS class for use within my application.

A couple of points worth mentioning. This doesn't have to by hyper-optimized. This list will likely never contain more than 100 sortable objects and even that is the high-end of an estimation. Yes, I'd like for this to be efficient but clean, readable code is a little more important than being able to use this with 1,000,000 entries at the blink of an eye.

Second, this is an internal JS object. Of course, someone may come along, find my code posted here and decide to use. That is fine but I'm not looking to make an open source library.

Dictionary.js

function SortableDictionary() {
    var self = this;
    var sf = "";
    var keys = [];

    //PUBLICS

    this.sort = function (field)
    {
        sf = field == null || field === undefined ? "" : field;
        var arr = getSortedArray();
        for (i = 0, len = arr.length; i < len; i++) {
            var obj = arr[i];
            if (obj == null) continue;
            else if (obj.hasOwnProperty("key") && obj.hasOwnProperty("value")) {
                if (obj["key"] != null) {
                    this.remove(obj["key"]);
                    this[obj["key"]] = obj["value"];
                    keys.push(obj["key"]);
                }
            }
        }
    }
    this.add = function (key, val) {
        if (keys.indexOf(key) != -1)
            throw "The key value '" + key + "' already exists.";
        else keys.push(key);
        this[key] = val;
        this.sort();
    }
    this.remove = function (key) {
        if (this.hasOwnProperty(key)) delete this[key];
        var ndx = keys.indexOf(key);
        if (ndx > -1) keys.splice(ndx,1);
    }

    this.toArray = function () {
        var arr = [];
        for (var i = 0; i < keys.length; i++)
            arr.push({ 'key': keys[i], 'value': this[keys[i]] });
        return arr;
    }

    //PRIVATES
    var getSortedArray = function () {
        return self.toArray().sort(function (a, b) {
            if (a == null) return 1;
            if ((sf == "" || !a.hasOwnProperty("value")) && a.hasOwnProperty("key"))
                return a["key"] < b["key"] ? -1 :
                        a["key"] > b["key"] ? 1 :
                        0
            else if ((sf == "" || !a.hasOwnProperty("value")) && !a.hasOwnProperty("key"))
                return a < b ? -1 :
                        a > b ? 1 :
                        0
            else if (a["value"].hasOwnProperty(sf) && b["value"].hasOwnProperty(sf))
                return a["value"][sf] < b["value"][sf] ? -1 :
                        a["value"][sf] > b["value"][sf] ? 1 :
                        0
            else if (a["value"].hasOwnProperty(sf) && !b["value"].hasOwnProperty(sf)) return -1;
            else if (!a["value"].hasOwnProperty(sf) && b["value"].hasOwnProperty(sf)) return 1;
            else return 0;
        })
    }

    //OVERRIDES
    this.toString = function () {
        return JSON.stringify(this, function(k, v) {return k == "sf" ? undefined : v});
    }
}

Test.html

<html>
<head>
    <script type="text/javascript" src="Scripts/Dictionary.js"></script>
</head>
<body style="font-family: Courier New, Courier, monospace; font-size:smaller">
<script>
    stdout = function(str)
    {
        document.writeln(str);
        document.writeln("<br>");
    }

    dict = new SortableDictionary();
    stdout(dict.toString());

    stdout("Adding 'a'...");
    dict.add("a", { text: "foo" });
    stdout(dict.toString());

    stdout("Adding 'c'...");
    dict.add("c", { text: "bee" });
    stdout(dict.toString());

    stdout("Adding 'd'...");
    dict.add("d", { text: "baz" });
    stdout(dict.toString());

    stdout("Adding 'b'...");
    dict.add("b", { text: "bar" });
    stdout(dict.toString());

    stdout("Sorting generic...");
    dict.sort();
    stdout(dict);

    stdout("Sorting on 'text' value...");
    dict.sort("text");
    stdout(dict);

    stdout("Let's remove something...")
    dict.remove("b");
    stdout(dict.toString());


</script>
</body>
</html>

jsFiddle

For posterity, here is the code moved to JS Fiddle. The Dictionary object code is defined at the top, and the test code is executed at the bottom.

\$\endgroup\$
  • \$\begingroup\$ By using this[key] = val you make your dictionary not able to hold the names of your methods. For example, after you add the key remove, this.remove won't be the method you wrote anymore. \$\endgroup\$ – Rodrigo5244 Dec 4 '15 at 16:42
  • \$\begingroup\$ @Waterscroll That is a good point worth mentioning, but in this context there will not be a key "remove". However, I'll keep that in mind if I ever consider using something like this for a future project. Is there a way to add functions that are part of the prototype chain or something, so that I wouldn't have to worry about it. JS is such an interesting beast when you start digging into the "advanced" topics. It's so different than C#. It's easy to get lost and confused in the "weeds". \$\endgroup\$ – RLH Dec 4 '15 at 21:18
  • \$\begingroup\$ The only difference, if the method is in the prototype, is that you can remove the key to get access to your method again with this.remove. If it is in the prototype you could access your method from the prototype instead of the instance like this SortableDictionary.prototype.remove.call(this, ...). \$\endgroup\$ – Rodrigo5244 Dec 6 '15 at 0:36
1
\$\begingroup\$

If I'm reading this right, you're essentially sorting the object by adding/removing properties. However, order isn't guaranteed. Yes, it almost always works out so that properties are listed in the order they were added. But it's not guaranteed.

This SO answer has more on the subject, and suggests using Map instead - if that's an option in the clients you have to support.

But to stick with a custom solution, you have to use arrays if you want guaranteed ordering. Much like you do already in toArray. You'll still want to wrap it in an API, so you can have custom add/remove methods and avoid duplicate keys.

Something like this, for instance:

function SortableCollection() {
  var items = []; // private

  this.add = function(key, value) {
    this.remove(key);
    items.push({
      key: String(key),
      value: value
    });
  };

  this.remove = function(key, item) {
    // the key has to be stringified to match the behavior of
    // object property names
    key = String(key);
    items = items.filter(function(item) {
      return item.key != key;
    });
  };

  // a less clever but more versatile sort function
  this.sort = function(comparator) {
    items.sort(function(a, b) {
      return comparator(a.value, b.value);
    });
  };

  this.toArray = function() {
    return items.slice(); // return a copy, not a reference
  };

  this.toObject = function() {
    return items.reduce({}, function(obj, item) {
      obj[item.key] = item.value;
      return obj;
    });
  };
}

// for testing purposes
function writeLine(text) {
  document.write(text + "<br>");
}

dict = new SortableCollection();
writeLine(JSON.stringify(dict.toArray()));

writeLine("Adding 'a'...");
dict.add("a", { text: "foo" });
writeLine(JSON.stringify(dict.toArray()));

writeLine("Adding 'c'...");
dict.add("c", { text: "bee" });
writeLine(JSON.stringify(dict.toArray()));

writeLine("Adding 'd'...");
dict.add("d", { text: "baz" });
writeLine(JSON.stringify(dict.toArray()));

writeLine("Adding 'b'...");
dict.add("b", { text: "bar" });
writeLine(JSON.stringify(dict.toArray()));

writeLine("Sorting on 'text' value...");
dict.sort(function (a, b) { return a.text < b.text ? -1 : 1 });
writeLine(JSON.stringify(dict.toArray()));

writeLine("Let's remove something...")
dict.remove("b");
writeLine(JSON.stringify(dict.toArray()));

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks Flambino for the answer. I should have added this to the test, but one of the requirements that I'd really like to maintain is that I'd like to be able to access objects as simply as myDictionary["a"] and get back an object ({"text": "foo"}). Is readily possible with an implementation similar to yours? \$\endgroup\$ – RLH Dec 3 '15 at 20:24
  • \$\begingroup\$ @RLH Access, yes - if go through toObject. E.g. dict.toObject()["a"]. Not pretty, but it'll work. The trick still is that you can't have an object with named properties and []-style access and guarantee property ordering. \$\endgroup\$ – Flambino Dec 3 '15 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.