The new EcmaScript standard, ES6, contains specifications for Map and Set built-in classes.  Each of these collections treats an object-valued key as opaque and unique, e.g.:var foo = new Set();foo.add({bar: baz}).has({bar: baz}) // == falseHowever, there are cases in which one might want to treat similar objects as the same key.  For this purpose, I wrote the following HashMap and HashSet classes.  The objectives:High quality code (readable and maintainable)Two different objects with identical enumerable properties and corresponding values are treated as identicalThe value of a circular reference is considered identical if it refers to the same relative path from the object used as a keyWith the exception of the previous two objectives, instances of HashMap and HashSet should be fully interchangeable with instances of Map and Set respectively.  In other words, where not conflicting with the above, the ES6 standard for instances of those classes (http://www.ecma-international.org/ecma-262/6.0/#sec-map-objects) should apply to instances of the corresponding classes as well.Code below uses ES6 features supported by Firefox at present.  Sadly that does not include class or super.    // This is for replacing cyclic references with relative names.    // as long as the reference is internally the same relative to    // the hashed object, they will hash equal.    const HASH_PATH = Symbol(hash path);    function hashVal(thing, name = this) {      var keys, hash;      if (thing instanceof Array) {        hash = [ +          thing.map((el, idx) =>            hashVal(el, name + [ + idx.toString() + ])          ).join(,) + ];      } else if (typeof thing === object) {        if (thing[HASH_PATH]) {          hash = thing[HASH_PATH];        } else {          keys = Object.keys(thing);          thing[HASH_PATH] = name;          keys.sort((k1, k2) => k1 > k2 ? 1 : -1);          hash = { +            keys.map(key =>              key + : +              hashVal(thing[key], name + . + key)            ).join(,) + };          delete thing[HASH_PATH];        }      } else if (typeof thing === string) {        hash = '' + thing + '';      } else {        hash = thing.toString();      }      return hash;    }    function HashMap(els, hash = this.defHash) {      this.hash = hash;      var elByHash = this.elByHash = new Map();      var keyByHash = this.keyByHash = new Map();      var el, key, hashKey;      if (els)        for ([key, el] of els.entries()) {          hashKey = this.hash(key);          keyByHash.set(hashKey, key);          elByHash.set(hashKey, el);        }    }    HashMap.prototype = {      get size() {          return this.elByHash.size;        },        clear() {          this.elByHash.clear();          this.keyByHash.clear();          return this;        },        delete(key) {          var hash = this.hash(key);          return this.keyByHash.delete(hash) && this.elByHash.delete(hash);        },        entries() {          return function*() {            var keys = this.keyByHash.entries();            var hash, key;            for ([hash, key] of keys) {              yield [key, this.elByHash.get(hash)];            }          };        },        keys() {          return this.keyByHash.values();        },        values() {          return this.elByHash.values();        },        set(key, val) {          var hash = this.hash(key);          this.keyByHash.set(hash, key);          this.elByHash.set(hash, val);          return this;        },        get(key) {          return this.elByHash.get(this.hash(key));        },        has(key) {          return this.keyByHash.has(this.hash(key));        },        forEach(cb, cx = this) {          var key, val;          for ([key, val] of this.entries()) {            cb.call(cx, val, key, this);          }        }, [Symbol.iterator]() {          return this.entries();        },        defHash: hashVal    };     // spec says set.values, set.keys, set[@@iterator] are same object    function hashSetValIterator() {      return this.elements.values();    }    function HashSet(els, hashFn = hashVal) {      var hash = this.hash = hashFn;      var elements = this.elements = new Map();      var key, val;      if (els)        for (val of els) {          elements.set(this.hash(val), val);        }    }    HashSet.prototype = {      defHash: hashVal,      get size() {        return this.elements.size;      },      add(el) {        this.elements.set(this.hash(el), el);        return this;      },      clear() {        this.elements.clear();      },      delete(el) {        return this.elements.delete(this.hash(el));      },      entries() {        var vals = this.values();        return function*() {          for (var v of vals) yield [v, v];        }      },      forEach(cbFn, thisArg = this) {        for (var v of this.values()) {          cbFn.call(thisArg, v, v, this);        }      },      has(el) {        return this.elements.has(this.hash(el));      },      values: hashSetValIterator,      keys: hashSetValIterator,      [Symbol.iterator]: hashSetValIterator,      defHash: hashVal    };Does this code meet the stated objectives?  How so or why not?I elected to not subclass Map or Set.  While I intended to reproduce their interfaces to ensure maximum portability into code currently using the built-in classes and zero cost to learn, the new classes do not fulfill exactly the same contract as the built-ins.  In addition, they don't need to borrow any methods from the built-ins, and there are no opportunities for them to exploit a superclass relationship via super as far as I know.  Thus, subclassing would be a violation of substitution principle with no practical payoff.  If you disagree, I would like to hear about it.PRE-REVIEW REVISION (no review exists at the time of this revision, so CR policy of forbidding edits to code is not applicable)The use of the HASH_PATH symbol in the hashVal function to temporarily associate an object with its relative path within the key object is a subtle but important mistake.  The following are consequences:An object may be frozen or inextensible; the creation of a privately scoped property via adding a closured-key property will throw an error in this case.Proxy obiects are now widely available, and assignment and delete to a Proxy-handled object's property may have unpredictable consequences.The value of HASH_PATH could be leaked to code in the scope that defines the Proxy handler, violating the assumption of locality.Proxy handler methods for these operations are permitted to have side effects with no desirable contract with the HashMap or HashSet.If the object is null, adding and deleting properties will cause an unwanted exception (although this can be avoided without abandoning a private Symbol-valued key).Tentative solution (pending review) is - Add a parameter to hashVal, ns = new WeakMap which maps object-valued properties to name paths  - The type of the name path is open for suggestion.  To consider:   - Strings formed by concatenation of keys and appropriate punctuation are temptingly simple for objects whose keys are all strings following the grammatical production identifierName, but this is far from guaranteed and overcomplicated to detect.   - A key whose value is a Symbol (widely implemented feature) had no accessible String representation that is different from non-equal Symbol values, leading to identical object path strings for paths that actually differ.  Open to suggestion, but one possible mitigation is introduction of a second object-specific local Map of Symbol-valued keys to unique String identifiers.  Measures should be taken to ensure that non-identifierName keys are escaped or bracketed or both, whereas path substrings are marked with a distinguishing token remaing unescaped and unbraceket.  Again suggestions are welcome.
HashMap and HashSet classes for ES6
javascript;hash table;ecmascript 6