JavaScript Hashmap Equivalente

354

Como quedó claro en la actualización 3 de esta respuesta , esta notación:

var hash = {};
hash[X]

en realidad no pica el objeto X; en realidad solo se convierte Xen una cadena (a través de .toString()si es un objeto, o alguna otra conversión incorporada para varios tipos primitivos) y luego mira esa cadena hacia arriba, sin cifrar, en " hash". La igualdad de objetos tampoco se verifica: si dos objetos diferentes tienen la misma conversión de cadena, simplemente se sobrescribirán entre sí.

Dado esto, ¿hay implementaciones eficientes de hashmaps en javascript? (Por ejemplo, el segundo resultado de Google javascript hashmapproduce una implementación que es O (n) para cualquier operación. Varios otros resultados ignoran el hecho de que diferentes objetos con representaciones de cadenas equivalentes se sobrescriben entre sí.

Claudiu
fuente
1
@Claudiu: Perdón por la edición, pero el "Mapa" en el título fue realmente engañoso. Retroceda si no está de acuerdo, no tenía la intención de patrocinar. :)
Tomalak
66
@Claudiu: Usted hace muchas preguntas sobre javascript. Buena pregunta. Me gusta eso.
poco el
2
@Claudiu: Además, ¿podría vincular al resultado de Google al que se refiere? Las diferentes versiones locales de Google arrojan resultados diferentes, la implementación a la que te refieres ni siquiera parece aparecer para mí.
Tomalak
@Tomalak: ¡Iba a escribir exactamente lo mismo!
Algunos
3
@ Claudiu No, no enlace a google. Enlace a la página de la que estaba hablando (que encontró a través de google). Vincular a Google tiene los mismos problemas que explicar qué buscar: resultados de personalización de Google basados ​​en la ubicación o en el historial de búsqueda, los resultados de Google cambian con el tiempo (actualmente, este es el mejor resultado para esa búsqueda) y cualquier otra cosa que pueda hacer que sea mostrar resultados diferentes
Jasper

Respuestas:

370

¿Por qué no hash hash tus objetos de forma manual, y utilizar las cadenas resultantes como claves para un diccionario JavaScript normal? Después de todo, usted está en la mejor posición para saber qué hace que sus objetos sean únicos. Eso es lo que hago.

Ejemplo:

var key = function(obj){
  // some unique object-dependent key
  return obj.totallyUniqueEmployeeIdKey; // just an example
};

var dict = {};

dict[key(obj1)] = obj1;
dict[key(obj2)] = obj2;

De esta forma, puede controlar la indexación realizada por JavaScript sin una gran carga de asignación de memoria y manejo de desbordamiento.

Por supuesto, si realmente desea la "solución de grado industrial", puede crear una clase parametrizada por la función clave y con todas las API necesarias del contenedor, pero ... usamos JavaScript y tratamos de ser simples y livianos, así que Esta solución funcional es simple y rápida.

La función de la tecla puede ser tan simple como seleccionar los atributos correctos del objeto, por ejemplo, una tecla o un conjunto de teclas, que ya son únicas, una combinación de teclas, que son únicas juntas, o tan complejas como usar algunos hashes criptográficos como en codificación DojoX o DojoX UUID . Si bien las últimas soluciones pueden producir claves únicas, personalmente trato de evitarlas a toda costa, especialmente si sé qué hace que mis objetos sean únicos.

Actualización en 2014: respondida en 2008, esta solución simple todavía requiere más explicaciones. Permítanme aclarar la idea en forma de preguntas y respuestas.

Su solución no tiene un verdadero hash. ¿¿¿Dónde está???

JavaScript es un lenguaje de alto nivel. Su primitivo básico ( Objeto ) incluye una tabla hash para mantener las propiedades. Esta tabla hash generalmente se escribe en un lenguaje de bajo nivel para mayor eficiencia. Usando un objeto simple con teclas de cadena, usamos una tabla hash implementada eficientemente sin ningún esfuerzo de nuestra parte.

¿Cómo sabes que usan hash?

Hay tres formas principales de mantener una colección de objetos direccionables por una clave:

  • Desordenado En este caso, para recuperar un objeto por su clave, debemos revisar todas las teclas deteniéndose cuando lo encontremos. En promedio, se necesitarán n / 2 comparaciones.
  • Ordenado.
    • Ejemplo # 1: una matriz ordenada: haciendo una búsqueda binaria, encontraremos nuestra clave después de ~ log2 (n) comparaciones en promedio. Mucho mejor.
    • Ejemplo # 2: un árbol. Nuevamente serán ~ log (n) intentos.
  • Tabla de picadillo. En promedio requiere un tiempo constante. Compare: O (n) vs. O (log n) vs. O (1). Auge.

Obviamente, los objetos JavaScript usan tablas hash de alguna forma para manejar casos generales.

¿Los vendedores de navegadores realmente usan tablas hash?

De Verdad.

¿Manejan colisiones?

Si. Véase más arriba. Si encuentra una colisión en cadenas desiguales, no dude en presentar un error a un proveedor.

Entonces, ¿cuál es tu idea?

Si desea hacer un hash de un objeto, encuentre lo que lo hace único y úselo como clave. No intente calcular hash real o emular tablas hash: el objeto JavaScript subyacente ya lo maneja de manera eficiente.

Use esta clave con JavaScript Objectpara aprovechar su tabla hash incorporada y evitar posibles conflictos con las propiedades predeterminadas.

Ejemplos para comenzar:

  • Si sus objetos incluyen un nombre de usuario único, úselo como clave.
  • Si incluye un número de cliente único, úselo como clave.
    • Si incluye números únicos emitidos por el gobierno, como un SSN o un número de pasaporte, y su sistema no permite duplicados, úselo como clave.
  • Si una combinación de campos es única, úsela como clave.
    • Abreviatura estatal + número de licencia de conducir es una clave excelente.
    • La abreviatura del país + el número de pasaporte también es una clave excelente.
  • Algunas funciones en los campos, o un objeto completo, pueden devolver un valor único. Úselo como una clave.

Usé su sugerencia y almacené en caché todos los objetos con un nombre de usuario. ¡Pero un tipo sabio se llama "toString", que es una propiedad incorporada! ¿Qué debería hacer ahora?

Obviamente, si es remotamente posible que la clave resultante consista exclusivamente en caracteres latinos, debe hacer algo al respecto. Por ejemplo, agregue cualquier carácter Unicode no latino que le guste al principio o al final para desempaquetar con las propiedades predeterminadas: "#toString", "#MarySmith". Si se usa una clave compuesta, separe los componentes clave utilizando algún tipo de delimitador no latino: "nombre, ciudad, estado".

En general, este es el lugar, donde tenemos que ser creativos y seleccionar las teclas más fáciles con limitaciones dadas (singularidad, posibles conflictos con las propiedades predeterminadas).

Nota: las claves únicas no entran en conflicto por definición, mientras que los posibles conflictos de hash serán manejados por el subyacente Object.

¿Por qué no te gustan las soluciones industriales?

En mi humilde opinión, el mejor código es ningún código: no tiene errores, no requiere mantenimiento, es fácil de entender y se ejecuta instantáneamente. Todas las "tablas hash en JavaScript" que vi eran> 100 líneas de código e involucraban múltiples objetos. Compararlo con: dict[key] = value.

Otro punto: ¿es posible incluso superar el rendimiento de un objeto primordial escrito en un lenguaje de bajo nivel, usando JavaScript y los mismos objetos primordiales para implementar lo que ya está implementado?

¡Todavía quiero cortar mis objetos sin ninguna llave!

Estamos de suerte: ECMAScript 6 (programado para mediados de 2015, más o menos 1-2 años después de que se generalice) define el mapa y el conjunto .

A juzgar por la definición, pueden usar la dirección del objeto como clave, lo que hace que los objetos se distingan instantáneamente sin claves artificiales. OTOH, dos objetos diferentes, pero idénticos, se asignarán como distintos.

Desglose de comparación de MDN :

Los objetos son similares a los Mapas, ya que ambos le permiten establecer claves en valores, recuperar esos valores, eliminar claves y detectar si algo está almacenado en una clave. Debido a esto (y porque no había alternativas incorporadas), los Objetos se han utilizado como Mapas históricamente; sin embargo, existen diferencias importantes que hacen que el uso de un Mapa sea preferible en ciertos casos:

  • Las claves de un objeto son cadenas y símbolos, mientras que pueden ser cualquier valor para un mapa, incluidas funciones, objetos y cualquier primitivo.
  • Las claves en el Mapa se ordenan mientras que las claves agregadas al objeto no. Por lo tanto, al iterar sobre él, un objeto Map devuelve claves en orden de inserción.
  • Puede obtener el tamaño de un Mapa fácilmente con la propiedad de tamaño, mientras que el número de propiedades en un Objeto debe determinarse manualmente.
  • Un mapa es iterable y, por lo tanto, puede iterarse directamente, mientras que iterar sobre un objeto requiere obtener sus claves de alguna manera e iterar sobre ellas.
  • Un objeto tiene un prototipo, por lo que hay claves predeterminadas en el mapa que podrían colisionar con sus claves si no tiene cuidado. A partir de ES5, esto se puede omitir usando map = Object.create (nulo), pero esto rara vez se hace.
  • Un mapa puede funcionar mejor en escenarios que implican la adición y eliminación frecuente de pares de claves.
Eugene Lazutkin
fuente
13
Esto no parece un mapa adecuado, porque no manejas las colisiones. Si esto es cierto: hash (obj1) == hash (obj2), perderá sus datos.
beefeather
32
El cielo te ayuda cuando tanto "PAUL AINLEY" como "PAULA INLEY" se registran en tu sistema ...
Matt R
34
@MattR En realidad, su ejemplo funcionará correctamente sin la ayuda del cielo, incluso con una función hash simulada. Espero que otros lectores se den cuenta de que se utilizó una función hash no realista demasiado simplificada como marcador de posición para demostrar una técnica diferente. Ambos comentarios de código y la respuesta en sí enfatizan que no es real. La selección de las claves adecuadas se discute en el último párrafo de la respuesta.
Eugene Lazutkin
66
@ EugeneLazutkin: sigues equivocado, me temo. Su ejemplo sigue siendo propenso a colisiones hash. ¡No pienses que solo poner el apellido primero te ayudará de alguna manera!
Matt R
3
@EugeneLazutkin La mayoría de la gente no lee, tienes una respuesta antes de que aparezca ES6 ... Déjame felicitarte por tu profundo conocimiento de JS.
Gabriel Andrés Brancolini
171

Descripción del problema

JavaScript no tiene un tipo de mapa general incorporado (a veces llamado matriz asociativa o diccionario ) que permite acceder a valores arbitrarios mediante claves arbitrarias. La estructura de datos fundamental de JavaScript es el objeto , un tipo especial de mapa que solo acepta cadenas como claves y tiene una semántica especial como herencia prototípica, captadores y establecedores y algo más de vudú.

Cuando utilice objetos como mapas, debe recordar que la clave se convertirá en un valor de cadena a través de toString(), lo que da como resultado la asignación 5y '5'al mismo valor y todos los objetos que no sobrescriben el toString()método al valor indexado por '[object Object]'. También puede acceder involuntariamente a sus propiedades heredadas si no marca hasOwnProperty().

El tipo de matriz incorporado de JavaScript no ayuda un bit: las matrices de JavaScript no son matrices asociativas, sino solo objetos con algunas propiedades especiales más. Si quieres saber por qué no se pueden usar como mapas, mira aquí .

La solución de Eugene

Eugene Lazutkin ya describió la idea básica de usar una función hash personalizada para generar cadenas únicas que se pueden usar para buscar los valores asociados como propiedades de un objeto de diccionario. Esta será probablemente la solución más rápida, porque los objetos se implementan internamente como tablas hash .

  • Nota: Las tablas hash (a veces llamadas mapas hash ) son una implementación particular del concepto de mapa usando una matriz de respaldo y búsqueda a través de valores numéricos hash. El entorno de tiempo de ejecución puede usar otras estructuras (como árboles de búsqueda o listas de omisión ) para implementar objetos JavaScript, pero como los objetos son la estructura de datos fundamental, deben optimizarse lo suficiente.

Para obtener un valor hash único para objetos arbitrarios, una posibilidad es usar un contador global y almacenar en caché el valor hash en el objeto mismo (por ejemplo, en una propiedad denominada __hash).

Una función hash que hace esto y funciona tanto para valores primitivos como para objetos es:

function hash(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
}

hash.current = 0;

Esta función se puede usar como lo describe Eugene. Por conveniencia, lo envolveremos en una Mapclase.

Mi Mapimplementacion

La siguiente implementación almacenará adicionalmente los pares clave-valor en una lista doblemente vinculada para permitir una iteración rápida sobre claves y valores. Para proporcionar su propia función hash, puede sobrescribir el hash()método de la instancia después de la creación.

// linking the key-value-pairs is optional
// if no argument is provided, linkItems === undefined, i.e. !== false
// --> linking will be enabled
function Map(linkItems) {
    this.current = undefined;
    this.size = 0;

    if(linkItems === false)
        this.disableLinking();
}

Map.noop = function() {
    return this;
};

Map.illegal = function() {
    throw new Error("illegal operation for maps without linking");
};

// map initialisation from existing object
// doesn't add inherited properties if not explicitly instructed to:
// omitting foreignKeys means foreignKeys === undefined, i.e. == false
// --> inherited properties won't be added
Map.from = function(obj, foreignKeys) {
    var map = new Map;

    for(var prop in obj) {
        if(foreignKeys || obj.hasOwnProperty(prop))
            map.put(prop, obj[prop]);
    }

    return map;
};

Map.prototype.disableLinking = function() {
    this.link = Map.noop;
    this.unlink = Map.noop;
    this.disableLinking = Map.noop;
    this.next = Map.illegal;
    this.key = Map.illegal;
    this.value = Map.illegal;
    this.removeAll = Map.illegal;

    return this;
};

// overwrite in Map instance if necessary
Map.prototype.hash = function(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
};

Map.prototype.hash.current = 0;

// --- mapping functions

Map.prototype.get = function(key) {
    var item = this[this.hash(key)];
    return item === undefined ? undefined : item.value;
};

Map.prototype.put = function(key, value) {
    var hash = this.hash(key);

    if(this[hash] === undefined) {
        var item = { key : key, value : value };
        this[hash] = item;

        this.link(item);
        ++this.size;
    }
    else this[hash].value = value;

    return this;
};

Map.prototype.remove = function(key) {
    var hash = this.hash(key);
    var item = this[hash];

    if(item !== undefined) {
        --this.size;
        this.unlink(item);

        delete this[hash];
    }

    return this;
};

// only works if linked
Map.prototype.removeAll = function() {
    while(this.size)
        this.remove(this.key());

    return this;
};

// --- linked list helper functions

Map.prototype.link = function(item) {
    if(this.size == 0) {
        item.prev = item;
        item.next = item;
        this.current = item;
    }
    else {
        item.prev = this.current.prev;
        item.prev.next = item;
        item.next = this.current;
        this.current.prev = item;
    }
};

Map.prototype.unlink = function(item) {
    if(this.size == 0)
        this.current = undefined;
    else {
        item.prev.next = item.next;
        item.next.prev = item.prev;
        if(item === this.current)
            this.current = item.next;
    }
};

// --- iterator functions - only work if map is linked

Map.prototype.next = function() {
    this.current = this.current.next;
};

Map.prototype.key = function() {
    return this.current.key;
};

Map.prototype.value = function() {
    return this.current.value;
};

Ejemplo

El siguiente script

var map = new Map;

map.put('spam', 'eggs').
    put('foo', 'bar').
    put('foo', 'baz').
    put({}, 'an object').
    put({}, 'another object').
    put(5, 'five').
    put(5, 'five again').
    put('5', 'another five');

for(var i = 0; i++ < map.size; map.next())
    document.writeln(map.hash(map.key()) + ' : ' + map.value());

genera esta salida:

string spam : eggs
string foo : baz
object 1 : an object
object 2 : another object
number 5 : five again
string 5 : another five

Consideraciones adicionales

PEZ sugirió sobrescribir el toString()método, presumiblemente con nuestra función hash. Esto no es factible porque no funciona para valores primitivos (cambiar toString()por primitivos es una muy mala idea). Si queremos toString()devolver valores significativos para objetos arbitrarios, tendríamos que modificar Object.prototype, lo que algunas personas (yo no incluido) consideran verboten .


Editar: la versión actual de mi Mapimplementación, así como otras ventajas de JavaScript, se pueden obtener desde aquí .

Christoph
fuente
ES5 desprecia el uso de callee ( goo.gl/EeStE ). En cambio, sugiero Map._counter = 0, y en el constructor de mapas hacer this._hash = 'object ' + Map._counter++. Luego de hash () se convierte enreturn (value && value._hash) || (typeof(value) + ' ' + String(value));
broofa
El enlace al código está roto: mercurial.intuxication.org/hg/js-hacks/raw-file/tip/map.js
ahcox
hola @ Christoph, ¿podrías actualizar tu enlace a donde puedo encontrar la implementación de tu mapa?
NumenorForLife
2
@ jsc123: Lo investigaré; por ahora puede obtener un volcado del repositorio en pikacode.com/mercurial.intuxication.org/js-hacks.tar.gz
Christoph el
58

Sé que esta pregunta es bastante antigua, pero hay algunas soluciones realmente buenas hoy en día con bibliotecas externas.

JavaScript también tiene su lenguaje proporcionado Maptambién.

Jamel Toms
fuente
2
Esta es la forma de avanzar al siglo XXI. Lástima que encontré tu publicación después de terminar mi código con un mapa feo hecho en casa. WEEE necesitan más votos por su respuesta
Phung D. Un
1
Collections.js tiene algunas implementaciones, pero no puedo encontrar ninguna en underscore.js o lodash ... ¿a qué te refieres en subrayado que sería útil?
Codebling
@CodeBling no tengo idea. Creo que lo confundí con la función de mapa. Voy a eliminarlo de la respuesta.
Jamel Toms
3
Eso es justo. Cualquiera que esté considerando Collections.js debe tener en cuenta que modifica prototipos globales de matriz, función, objeto y expresión regular de una manera problemática ( vea los problemas que encontré aquí ). Aunque inicialmente estaba muy satisfecho con collections.js (y, por lo tanto, esta respuesta), los riesgos asociados con su uso eran demasiado altos, por lo que lo descarté. Solo la rama v2 de kriskowal de collections.js (específicamente, v2.0.2 +) elimina las modificaciones globales del prototipo y es seguro de usar.
Codebling
28

Aquí hay una manera fácil y conveniente de usar algo similar al mapa de Java:

var map= {
        'map_name_1': map_value_1,
        'map_name_2': map_value_2,
        'map_name_3': map_value_3,
        'map_name_4': map_value_4
        }

Y para obtener el valor:

alert( map['map_name_1'] );    // fives the value of map_value_1

......  etc  .....
Milos
fuente
2
Esto funciona solo para claves de cadena. Creo que el OP estaba interesado en usar claves de cualquier tipo.
fractor
26

Según el estándar ECMAScript 2015 (ES6), JavaScript tiene una implementación de mapa. Más sobre cuál se puede encontrar aquí

Uso básico:

var myMap = new Map();
var keyString = "a string",
    keyObj = {},
    keyFunc = function () {};

// setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, "value associated with keyObj");
myMap.set(keyFunc, "value associated with keyFunc");

myMap.size; // 3

// getting the values
myMap.get(keyString);    // "value associated with 'a string'"
myMap.get(keyObj);       // "value associated with keyObj"
myMap.get(keyFunc);      // "value associated with keyFunc"
Riyafa Abdul Hameed
fuente
21

Puede usar ES6 WeakMapo Map:

  • WeakMaps son mapas clave / valor en el que las claves son objetos.

  • Maplos objetos son simples mapas de clave / valor. Cualquier valor (tanto objetos como valores primitivos) se puede usar como clave o como valor.

Tenga en cuenta que ninguno de los dos es ampliamente compatible, pero puede usar ES6 Shim (requiere ES5 nativo o ES5 Shim ) para admitirlo Map, pero no WeakMap( vea por qué ).

Oriol
fuente
¡En 2019 están muy bien respaldados y tienen métodos increíbles! developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Juanma Menendez
13

Tendría que almacenar en algunos acoplamientos de estado interno de pares de objeto / valor

HashMap = function(){
  this._dict = [];
}
HashMap.prototype._get = function(key){
  for(var i=0, couplet; couplet = this._dict[i]; i++){
    if(couplet[0] === key){
      return couplet;
    }
  }
}
HashMap.prototype.put = function(key, value){
  var couplet = this._get(key);
  if(couplet){
    couplet[1] = value;
  }else{
    this._dict.push([key, value]);
  }
  return this; // for chaining
}
HashMap.prototype.get = function(key){
  var couplet = this._get(key);
  if(couplet){
    return couplet[1];
  }
}

Y úsalo como tal:

var color = {}; // unique object instance
var shape = {}; // unique object instance
var map = new HashMap();
map.put(color, "blue");
map.put(shape, "round");
console.log("Item is", map.get(color), "and", map.get(shape));

Por supuesto, esta implementación también está en algún lugar a lo largo de las líneas de O (n). Los ejemplos anteriores de Eugene son la única forma de obtener un hash que funcione con cualquier tipo de velocidad que esperarías de un hash real.

Actualizar:

Otro enfoque, en la línea de la respuesta de Eugene es de alguna manera adjuntar una identificación única a todos los objetos. Uno de mis enfoques favoritos es tomar uno de los métodos incorporados heredados de la superclase Object, reemplazarlo con un paso de función personalizado y adjuntar propiedades a ese objeto de función. Si tuviera que reescribir mi método HashMap para hacer esto, se vería así:

HashMap = function(){
  this._dict = {};
}
HashMap.prototype._shared = {id: 1};
HashMap.prototype.put = function put(key, value){
  if(typeof key == "object"){
    if(!key.hasOwnProperty._id){
      key.hasOwnProperty = function(key){
        return Object.prototype.hasOwnProperty.call(this, key);
      }
      key.hasOwnProperty._id = this._shared.id++;
    }
    this._dict[key.hasOwnProperty._id] = value;
  }else{
    this._dict[key] = value;
  }
  return this; // for chaining
}
HashMap.prototype.get = function get(key){
  if(typeof key == "object"){
    return this._dict[key.hasOwnProperty._id];
  }
  return this._dict[key];
}

Esta versión parece ser solo un poco más rápida, pero en teoría será significativamente más rápida para grandes conjuntos de datos.

carne en maceta
fuente
Una matriz asociativa, es decir, una matriz de 2 tuplas, es un Mapa, no un HashMap; un HashMap es un mapa que utiliza hashes para un mejor rendimiento.
Erik Kaplun
Es cierto, pero ¿por qué dividir los pelos sobre el tema? No hay forma de crear un verdadero mapa hash en JavaScript ya que no puede obtener direcciones de memoria de objetos. Y los pares de clave / valor de objeto incorporados de JavaScript (utilizados en mi segundo ejemplo) pueden actuar como HashMaps, pero no necesariamente, ya que depende del tiempo de ejecución utilizado en el navegador en cuanto a cómo se implementa la búsqueda.
pottedmeat
11

Desafortunadamente, ninguna de las respuestas anteriores fue buena para mi caso: diferentes objetos clave pueden tener el mismo código hash. Por lo tanto, escribí una versión simple de HashMap similar a Java:

function HashMap() {
    this.buckets = {};
}

HashMap.prototype.put = function(key, value) {
    var hashCode = key.hashCode();
    var bucket = this.buckets[hashCode];
    if (!bucket) {
        bucket = new Array();
        this.buckets[hashCode] = bucket;
    }
    for (var i = 0; i < bucket.length; ++i) {
        if (bucket[i].key.equals(key)) {
            bucket[i].value = value;
            return;
        }
    }
    bucket.push({ key: key, value: value });
}

HashMap.prototype.get = function(key) {
    var hashCode = key.hashCode();
    var bucket = this.buckets[hashCode];
    if (!bucket) {
        return null;
    }
    for (var i = 0; i < bucket.length; ++i) {
        if (bucket[i].key.equals(key)) {
            return bucket[i].value;
        }
    }
}

HashMap.prototype.keys = function() {
    var keys = new Array();
    for (var hashKey in this.buckets) {
        var bucket = this.buckets[hashKey];
        for (var i = 0; i < bucket.length; ++i) {
            keys.push(bucket[i].key);
        }
    }
    return keys;
}

HashMap.prototype.values = function() {
    var values = new Array();
    for (var hashKey in this.buckets) {
        var bucket = this.buckets[hashKey];
        for (var i = 0; i < bucket.length; ++i) {
            values.push(bucket[i].value);
        }
    }
    return values;
}

Nota: los objetos clave deben "implementar" los métodos hashCode () y equals ().

Michael Spector
fuente
77
¿La preferencia de new Array()over []es asegurar la absoluta similitud de Java de su código? :)
Erik Kaplun
6

He implementado un JavaScript HashMap cuyo código se puede obtener de http://github.com/lambder/HashMapJS/tree/master

Aquí está el código:

/*
 =====================================================================
 @license MIT
 @author Lambder
 @copyright 2009 Lambder.
 @end
 =====================================================================
 */
var HashMap = function() {
  this.initialize();
}

HashMap.prototype = {
  hashkey_prefix: "<#HashMapHashkeyPerfix>",
  hashcode_field: "<#HashMapHashkeyPerfix>",

  initialize: function() {
    this.backing_hash = {};
    this.code = 0;
  },
  /*
   maps value to key returning previous assocciation
   */
  put: function(key, value) {
    var prev;
    if (key && value) {
      var hashCode = key[this.hashcode_field];
      if (hashCode) {
        prev = this.backing_hash[hashCode];
      } else {
        this.code += 1;
        hashCode = this.hashkey_prefix + this.code;
        key[this.hashcode_field] = hashCode;
      }
      this.backing_hash[hashCode] = value;
    }
    return prev;
  },
  /*
   returns value associated with given key
   */
  get: function(key) {
    var value;
    if (key) {
      var hashCode = key[this.hashcode_field];
      if (hashCode) {
        value = this.backing_hash[hashCode];
      }
    }
    return value;
  },
  /*
   deletes association by given key.
   Returns true if the assocciation existed, false otherwise
   */
  del: function(key) {
    var success = false;
    if (key) {
      var hashCode = key[this.hashcode_field];
      if (hashCode) {
        var prev = this.backing_hash[hashCode];
        this.backing_hash[hashCode] = undefined;
        if(prev !== undefined)
          success = true;
      }
    }
    return success;
  }
}

//// Usage

// creation

var my_map = new HashMap();

// insertion

var a_key = {};
var a_value = {struct: "structA"};
var b_key = {};
var b_value = {struct: "structB"};
var c_key = {};
var c_value = {struct: "structC"};

my_map.put(a_key, a_value);
my_map.put(b_key, b_value);
var prev_b = my_map.put(b_key, c_value);

// retrieval

if(my_map.get(a_key) !== a_value){
  throw("fail1")
}
if(my_map.get(b_key) !== c_value){
  throw("fail2")
}
if(prev_b !== b_value){
  throw("fail3")
}

// deletion

var a_existed = my_map.del(a_key);
var c_existed = my_map.del(c_key);
var a2_existed = my_map.del(a_key);

if(a_existed !== true){
  throw("fail4")
}
if(c_existed !== false){
  throw("fail5")
}
if(a2_existed !== false){
  throw("fail6")
}
Lambder
fuente
2
Su código no parece funcionar con poner el mismo objeto en varios correos HashMapelectrónicos.
Erik Kaplun
5

En ECMA6 puedes usar WeakMap

Ejemplo:

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because there is no value for o2 on wm2
wm2.get(o3); // undefined, because that is the set value

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (even if the value itself is 'undefined')

wm3.set(o1, 37);
wm3.get(o1); // 37
wm3.clear();
wm3.get(o1); // undefined, because wm3 was cleared and there is no value for o1 anymore

wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false

Pero:

Because of references being weak, WeakMap keys are not enumerable (i.e. there is no method giving you a list of the keys). 
Nox73
fuente
oh, alabado sea Jesús, finalmente están agregando referencias débiles a JavaScript. es hora ... +1 para eso, pero en realidad sería horrible de usar porque las referencias son débiles
Claudiu
2

Javascript no incorpora Map / hashmap. Debería llamarse matriz asociativa .

hash["X"]es igual a hash.X, pero permite "X" como una variable de cadena. En otras palabras, hash[x]es funcionalmente igual aeval("hash."+x.toString())

Es más similar a object.properties que al mapeo de valores clave. Si está buscando una mejor asignación de clave / valor en Javascript, utilice el objeto Map que puede encontrar en la web.

Dennis C
fuente
2

Pruebe la implementación de mi tabla hash de JavaScript: http://www.timdown.co.uk/jshashtable

Busca un método hashCode () de objetos clave, o puede proporcionar una función hash al crear un objeto Hashtable.

Tim Down
fuente
2

Esto parece una solución bastante robusta: https://github.com/flesler/hashmap . Incluso funcionará bien para funciones y objetos que se ven idénticos. El único truco que usa es agregar un miembro oscuro a un objeto para identificarlo. Si su programa no sobrescribe esa variable oscura (es algo así como hashid ), está dorado.

BT
fuente
2

Si el rendimiento no es crítico (por ejemplo, la cantidad de teclas es relativamente pequeña) y no quieren contaminar su (o tal vez no su) objetos con campos adicionales como _hash, _id, etc., entonces usted puede hacer uso del hecho de que Array.prototype.indexOfemplea estricta igualdad Aquí hay una implementación simple:

var Dict = (function(){
    // IE 8 and earlier has no Array.prototype.indexOf
    function indexOfPolyfill(val) {
      for (var i = 0, l = this.length; i < l; ++i) {
        if (this[i] === val) {
          return i;
        }
      }
      return -1;
    }

    function Dict(){
      this.keys = [];
      this.values = [];
      if (!this.keys.indexOf) {
        this.keys.indexOf = indexOfPolyfill;
      }
    };

    Dict.prototype.has = function(key){
      return this.keys.indexOf(key) != -1;
    };

    Dict.prototype.get = function(key, defaultValue){
      var index = this.keys.indexOf(key);
      return index == -1 ? defaultValue : this.values[index];
    };

    Dict.prototype.set = function(key, value){
      var index = this.keys.indexOf(key);
      if (index == -1) {
        this.keys.push(key);
        this.values.push(value);
      } else {
        var prevValue = this.values[index];
        this.values[index] = value;
        return prevValue;
      }
    };

    Dict.prototype.delete = function(key){
      var index = this.keys.indexOf(key);
      if (index != -1) {
        this.keys.splice(index, 1);
        return this.values.splice(index, 1)[0];
      }
    };

    Dict.prototype.clear = function(){
      this.keys.splice(0, this.keys.length);
      this.values.splice(0, this.values.length);
    };

    return Dict;
})();

Ejemplo de uso:

var a = {}, b = {},
    c = { toString: function(){ return '1'; } },
    d = 1, s = '1', u = undefined, n = null,
    dict = new Dict();

// keys and values can be anything
dict.set(a, 'a');
dict.set(b, 'b');
dict.set(c, 'c');
dict.set(d, 'd');
dict.set(s, 's');
dict.set(u, 'u');
dict.set(n, 'n');

dict.get(a); // 'a'
dict.get(b); // 'b'
dict.get(s); // 's'
dict.get(u); // 'u'
dict.get(n); // 'n'
// etc.

En comparación con ES6 WeakMap, tiene dos problemas: O (n) tiempo de búsqueda y falta de debilidad (es decir, provocará una pérdida de memoria si no usa deleteo clearsuelta las teclas).

skozin
fuente
2

My Map Implementation, derivado del ejemplo de Christoph:

Ejemplo de uso:

var map = new Map();  //creates an "in-memory" map
var map = new Map("storageId");  //creates a map that is loaded/persisted using html5 storage

function Map(storageId) {
    this.current = undefined;
    this.size = 0;
    this.storageId = storageId;
    if (this.storageId) {
        this.keys = new Array();
        this.disableLinking();
    }
}

Map.noop = function() {
    return this;
};

Map.illegal = function() {
    throw new Error("illegal operation for maps without linking");
};

// map initialisation from existing object
// doesn't add inherited properties if not explicitly instructed to:
// omitting foreignKeys means foreignKeys === undefined, i.e. == false
// --> inherited properties won't be added
Map.from = function(obj, foreignKeys) {
    var map = new Map;
    for(var prop in obj) {
        if(foreignKeys || obj.hasOwnProperty(prop))
            map.put(prop, obj[prop]);
    }
    return map;
};

Map.prototype.disableLinking = function() {
    this.link = Map.noop;
    this.unlink = Map.noop;
    this.disableLinking = Map.noop;

    this.next = Map.illegal;
    this.key = Map.illegal;
    this.value = Map.illegal;
//    this.removeAll = Map.illegal;


    return this;
};

// overwrite in Map instance if necessary
Map.prototype.hash = function(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
};

Map.prototype.hash.current = 0;

// --- mapping functions

Map.prototype.get = function(key) {
    var item = this[this.hash(key)];
    if (item === undefined) {
        if (this.storageId) {
            try {
                var itemStr = localStorage.getItem(this.storageId + key);
                if (itemStr && itemStr !== 'undefined') {
                    item = JSON.parse(itemStr);
                    this[this.hash(key)] = item;
                    this.keys.push(key);
                    ++this.size;
                }
            } catch (e) {
                console.log(e);
            }
        }
    }
    return item === undefined ? undefined : item.value;
};

Map.prototype.put = function(key, value) {
    var hash = this.hash(key);

    if(this[hash] === undefined) {
        var item = { key : key, value : value };
        this[hash] = item;

        this.link(item);
        ++this.size;
    }
    else this[hash].value = value;
    if (this.storageId) {
        this.keys.push(key);
        try {
            localStorage.setItem(this.storageId + key, JSON.stringify(this[hash]));
        } catch (e) {
            console.log(e);
        }
    }
    return this;
};

Map.prototype.remove = function(key) {
    var hash = this.hash(key);
    var item = this[hash];
    if(item !== undefined) {
        --this.size;
        this.unlink(item);

        delete this[hash];
    }
    if (this.storageId) {
        try {
            localStorage.setItem(this.storageId + key, undefined);
        } catch (e) {
            console.log(e);
        }
    }
    return this;
};

// only works if linked
Map.prototype.removeAll = function() {
    if (this.storageId) {
        for (var i=0; i<this.keys.length; i++) {
            this.remove(this.keys[i]);
        }
        this.keys.length = 0;
    } else {
        while(this.size)
            this.remove(this.key());
    }
    return this;
};

// --- linked list helper functions

Map.prototype.link = function(item) {
    if (this.storageId) {
        return;
    }
    if(this.size == 0) {
        item.prev = item;
        item.next = item;
        this.current = item;
    }
    else {
        item.prev = this.current.prev;
        item.prev.next = item;
        item.next = this.current;
        this.current.prev = item;
    }
};

Map.prototype.unlink = function(item) {
    if (this.storageId) {
        return;
    }
    if(this.size == 0)
        this.current = undefined;
    else {
        item.prev.next = item.next;
        item.next.prev = item.prev;
        if(item === this.current)
            this.current = item.next;
    }
};

// --- iterator functions - only work if map is linked

Map.prototype.next = function() {
    this.current = this.current.next;
};

Map.prototype.key = function() {
    if (this.storageId) {
        return undefined;
    } else {
        return this.current.key;
    }
};

Map.prototype.value = function() {
    if (this.storageId) {
        return undefined;
    }
    return this.current.value;
};
g00dnatur3
fuente
1

Agregando otra solución: HashMapes prácticamente la primera clase que porté de Java a Javascript. Se podría decir que hay mucha sobrecarga, pero la implementación es casi 100% igual a la implementación de Java e incluye todas las interfaces y subclases.

El proyecto se puede encontrar aquí: https://github.com/Airblader/jsava También adjuntaré el código fuente (actual) para la clase HashMap, pero como se indicó también depende de la superclase, etc. El marco de OOP utilizado es qooxdoo.

Editar: tenga en cuenta que este código ya está desactualizado y consulte el proyecto github para el trabajo actual. Al escribir esto, también hay una ArrayListimplementación.

qx.Class.define( 'jsava.util.HashMap', {
    extend: jsava.util.AbstractMap,
    implement: [jsava.util.Map, jsava.io.Serializable, jsava.lang.Cloneable],

    construct: function () {
        var args = Array.prototype.slice.call( arguments ),
            initialCapacity = this.self( arguments ).DEFAULT_INITIAL_CAPACITY,
            loadFactor = this.self( arguments ).DEFAULT_LOAD_FACTOR;

        switch( args.length ) {
            case 1:
                if( qx.Class.implementsInterface( args[0], jsava.util.Map ) ) {
                    initialCapacity = Math.max( ((args[0].size() / this.self( arguments ).DEFAULT_LOAD_FACTOR) | 0) + 1,
                        this.self( arguments ).DEFAULT_INITIAL_CAPACITY );
                    loadFactor = this.self( arguments ).DEFAULT_LOAD_FACTOR;
                } else {
                    initialCapacity = args[0];
                }
                break;
            case 2:
                initialCapacity = args[0];
                loadFactor = args[1];
                break;
        }

        if( initialCapacity < 0 ) {
            throw new jsava.lang.IllegalArgumentException( 'Illegal initial capacity: ' + initialCapacity );
        }
        if( initialCapacity > this.self( arguments ).MAXIMUM_CAPACITY ) {
            initialCapacity = this.self( arguments ).MAXIMUM_CAPACITY;
        }
        if( loadFactor <= 0 || isNaN( loadFactor ) ) {
            throw new jsava.lang.IllegalArgumentException( 'Illegal load factor: ' + loadFactor );
        }

        var capacity = 1;
        while( capacity < initialCapacity ) {
            capacity <<= 1;
        }

        this._loadFactor = loadFactor;
        this._threshold = (capacity * loadFactor) | 0;
        this._table = jsava.JsavaUtils.emptyArrayOfGivenSize( capacity, null );
        this._init();
    },

    statics: {
        serialVersionUID: 1,

        DEFAULT_INITIAL_CAPACITY: 16,
        MAXIMUM_CAPACITY: 1 << 30,
        DEFAULT_LOAD_FACTOR: 0.75,

        _hash: function (hash) {
            hash ^= (hash >>> 20) ^ (hash >>> 12);
            return hash ^ (hash >>> 7) ^ (hash >>> 4);
        },

        _indexFor: function (hashCode, length) {
            return hashCode & (length - 1);
        },

        Entry: qx.Class.define( 'jsava.util.HashMap.Entry', {
            extend: jsava.lang.Object,
            implement: [jsava.util.Map.Entry],

            construct: function (hash, key, value, nextEntry) {
                this._value = value;
                this._next = nextEntry;
                this._key = key;
                this._hash = hash;
            },

            members: {
                _key: null,
                _value: null,
                /** @type jsava.util.HashMap.Entry */
                _next: null,
                /** @type Number */
                _hash: 0,

                getKey: function () {
                    return this._key;
                },

                getValue: function () {
                    return this._value;
                },

                setValue: function (newValue) {
                    var oldValue = this._value;
                    this._value = newValue;
                    return oldValue;
                },

                equals: function (obj) {
                    if( obj === null || !qx.Class.implementsInterface( obj, jsava.util.HashMap.Entry ) ) {
                        return false;
                    }

                    /** @type jsava.util.HashMap.Entry */
                    var entry = obj,
                        key1 = this.getKey(),
                        key2 = entry.getKey();
                    if( key1 === key2 || (key1 !== null && key1.equals( key2 )) ) {
                        var value1 = this.getValue(),
                            value2 = entry.getValue();
                        if( value1 === value2 || (value1 !== null && value1.equals( value2 )) ) {
                            return true;
                        }
                    }

                    return false;
                },

                hashCode: function () {
                    return (this._key === null ? 0 : this._key.hashCode()) ^
                        (this._value === null ? 0 : this._value.hashCode());
                },

                toString: function () {
                    return this.getKey() + '=' + this.getValue();
                },

                /**
                 * This method is invoked whenever the value in an entry is
                 * overwritten by an invocation of put(k,v) for a key k that's already
                 * in the HashMap.
                 */
                _recordAccess: function (map) {
                },

                /**
                 * This method is invoked whenever the entry is
                 * removed from the table.
                 */
                _recordRemoval: function (map) {
                }
            }
        } )
    },

    members: {
        /** @type jsava.util.HashMap.Entry[] */
        _table: null,
        /** @type Number */
        _size: 0,
        /** @type Number */
        _threshold: 0,
        /** @type Number */
        _loadFactor: 0,
        /** @type Number */
        _modCount: 0,
        /** @implements jsava.util.Set */
        __entrySet: null,

        /**
         * Initialization hook for subclasses. This method is called
         * in all constructors and pseudo-constructors (clone, readObject)
         * after HashMap has been initialized but before any entries have
         * been inserted.  (In the absence of this method, readObject would
         * require explicit knowledge of subclasses.)
         */
        _init: function () {
        },

        size: function () {
            return this._size;
        },

        isEmpty: function () {
            return this._size === 0;
        },

        get: function (key) {
            if( key === null ) {
                return this.__getForNullKey();
            }

            var hash = this.self( arguments )._hash( key.hashCode() );
            for( var entry = this._table[this.self( arguments )._indexFor( hash, this._table.length )];
                 entry !== null; entry = entry._next ) {
                /** @type jsava.lang.Object */
                var k;
                if( entry._hash === hash && ((k = entry._key) === key || key.equals( k )) ) {
                    return entry._value;
                }
            }

            return null;
        },

        __getForNullKey: function () {
            for( var entry = this._table[0]; entry !== null; entry = entry._next ) {
                if( entry._key === null ) {
                    return entry._value;
                }
            }

            return null;
        },

        containsKey: function (key) {
            return this._getEntry( key ) !== null;
        },

        _getEntry: function (key) {
            var hash = (key === null) ? 0 : this.self( arguments )._hash( key.hashCode() );
            for( var entry = this._table[this.self( arguments )._indexFor( hash, this._table.length )];
                 entry !== null; entry = entry._next ) {
                /** @type jsava.lang.Object */
                var k;
                if( entry._hash === hash
                    && ( ( k = entry._key ) === key || ( key !== null && key.equals( k ) ) ) ) {
                    return entry;
                }
            }

            return null;
        },

        put: function (key, value) {
            if( key === null ) {
                return this.__putForNullKey( value );
            }

            var hash = this.self( arguments )._hash( key.hashCode() ),
                i = this.self( arguments )._indexFor( hash, this._table.length );
            for( var entry = this._table[i]; entry !== null; entry = entry._next ) {
                /** @type jsava.lang.Object */
                var k;
                if( entry._hash === hash && ( (k = entry._key) === key || key.equals( k ) ) ) {
                    var oldValue = entry._value;
                    entry._value = value;
                    entry._recordAccess( this );
                    return oldValue;
                }
            }

            this._modCount++;
            this._addEntry( hash, key, value, i );
            return null;
        },

        __putForNullKey: function (value) {
            for( var entry = this._table[0]; entry !== null; entry = entry._next ) {
                if( entry._key === null ) {
                    var oldValue = entry._value;
                    entry._value = value;
                    entry._recordAccess( this );
                    return oldValue;
                }
            }

            this._modCount++;
            this._addEntry( 0, null, value, 0 );
            return null;
        },

        __putForCreate: function (key, value) {
            var hash = (key === null) ? 0 : this.self( arguments )._hash( key.hashCode() ),
                i = this.self( arguments )._indexFor( hash, this._table.length );
            for( var entry = this._table[i]; entry !== null; entry = entry._next ) {
                /** @type jsava.lang.Object */
                var k;
                if( entry._hash === hash
                    && ( (k = entry._key) === key || ( key !== null && key.equals( k ) ) ) ) {
                    entry._value = value;
                    return;
                }
            }

            this._createEntry( hash, key, value, i );
        },

        __putAllForCreate: function (map) {
            var iterator = map.entrySet().iterator();
            while( iterator.hasNext() ) {
                var entry = iterator.next();
                this.__putForCreate( entry.getKey(), entry.getValue() );
            }
        },

        _resize: function (newCapacity) {
            var oldTable = this._table,
                oldCapacity = oldTable.length;
            if( oldCapacity === this.self( arguments ).MAXIMUM_CAPACITY ) {
                this._threshold = Number.MAX_VALUE;
                return;
            }

            var newTable = jsava.JsavaUtils.emptyArrayOfGivenSize( newCapacity, null );
            this._transfer( newTable );
            this._table = newTable;
            this._threshold = (newCapacity * this._loadFactor) | 0;
        },

        _transfer: function (newTable) {
            var src = this._table,
                newCapacity = newTable.length;
            for( var j = 0; j < src.length; j++ ) {
                var entry = src[j];
                if( entry !== null ) {
                    src[j] = null;
                    do {
                        var next = entry._next,
                            i = this.self( arguments )._indexFor( entry._hash, newCapacity );
                        entry._next = newTable[i];
                        newTable[i] = entry;
                        entry = next;
                    } while( entry !== null );
                }
            }
        },

        putAll: function (map) {
            var numKeyToBeAdded = map.size();
            if( numKeyToBeAdded === 0 ) {
                return;
            }

            if( numKeyToBeAdded > this._threshold ) {
                var targetCapacity = (numKeyToBeAdded / this._loadFactor + 1) | 0;
                if( targetCapacity > this.self( arguments ).MAXIMUM_CAPACITY ) {
                    targetCapacity = this.self( arguments ).MAXIMUM_CAPACITY;
                }

                var newCapacity = this._table.length;
                while( newCapacity < targetCapacity ) {
                    newCapacity <<= 1;
                }
                if( newCapacity > this._table.length ) {
                    this._resize( newCapacity );
                }
            }

            var iterator = map.entrySet().iterator();
            while( iterator.hasNext() ) {
                var entry = iterator.next();
                this.put( entry.getKey(), entry.getValue() );
            }
        },

        remove: function (key) {
            var entry = this._removeEntryForKey( key );
            return entry === null ? null : entry._value;
        },

        _removeEntryForKey: function (key) {
            var hash = (key === null) ? 0 : this.self( arguments )._hash( key.hashCode() ),
                i = this.self( arguments )._indexFor( hash, this._table.length ),
                prev = this._table[i],
                entry = prev;

            while( entry !== null ) {
                var next = entry._next,
                    /** @type jsava.lang.Object */
                        k;
                if( entry._hash === hash
                    && ( (k = entry._key) === key || ( key !== null && key.equals( k ) ) ) ) {
                    this._modCount++;
                    this._size--;
                    if( prev === entry ) {
                        this._table[i] = next;
                    } else {
                        prev._next = next;
                    }
                    entry._recordRemoval( this );
                    return entry;
                }
                prev = entry;
                entry = next;
            }

            return entry;
        },

        _removeMapping: function (obj) {
            if( obj === null || !qx.Class.implementsInterface( obj, jsava.util.Map.Entry ) ) {
                return null;
            }

            /** @implements jsava.util.Map.Entry */
            var entry = obj,
                key = entry.getKey(),
                hash = (key === null) ? 0 : this.self( arguments )._hash( key.hashCode() ),
                i = this.self( arguments )._indexFor( hash, this._table.length ),
                prev = this._table[i],
                e = prev;

            while( e !== null ) {
                var next = e._next;
                if( e._hash === hash && e.equals( entry ) ) {
                    this._modCount++;
                    this._size--;
                    if( prev === e ) {
                        this._table[i] = next;
                    } else {
                        prev._next = next;
                    }
                    e._recordRemoval( this );
                    return e;
                }
                prev = e;
                e = next;
            }

            return e;
        },

        clear: function () {
            this._modCount++;
            var table = this._table;
            for( var i = 0; i < table.length; i++ ) {
                table[i] = null;
            }
            this._size = 0;
        },

        containsValue: function (value) {
            if( value === null ) {
                return this.__containsNullValue();
            }

            var table = this._table;
            for( var i = 0; i < table.length; i++ ) {
                for( var entry = table[i]; entry !== null; entry = entry._next ) {
                    if( value.equals( entry._value ) ) {
                        return true;
                    }
                }
            }

            return false;
        },

        __containsNullValue: function () {
            var table = this._table;
            for( var i = 0; i < table.length; i++ ) {
                for( var entry = table[i]; entry !== null; entry = entry._next ) {
                    if( entry._value === null ) {
                        return true;
                    }
                }
            }

            return false;
        },

        clone: function () {
            /** @type jsava.util.HashMap */
            var result = null;
            try {
                result = this.base( arguments );
            } catch( e ) {
                if( !qx.Class.isSubClassOf( e.constructor, jsava.lang.CloneNotSupportedException ) ) {
                    throw e;
                }
            }

            result._table = jsava.JsavaUtils.emptyArrayOfGivenSize( this._table.length, null );
            result.__entrySet = null;
            result._modCount = 0;
            result._size = 0;
            result._init();
            result.__putAllForCreate( this );

            return result;
        },

        _addEntry: function (hash, key, value, bucketIndex) {
            var entry = this._table[bucketIndex];
            this._table[bucketIndex] = new (this.self( arguments ).Entry)( hash, key, value, entry );
            if( this._size++ >= this._threshold ) {
                this._resize( 2 * this._table.length );
            }
        },

        _createEntry: function (hash, key, value, bucketIndex) {
            var entry = this._table[bucketIndex];
            this._table[bucketIndex] = new (this.self( arguments ).Entry)( hash, key, value, entry );
            this._size++;
        },

        keySet: function () {
            var keySet = this._keySet;
            return keySet !== null ? keySet : ( this._keySet = new this.KeySet( this ) );
        },

        values: function () {
            var values = this._values;
            return values !== null ? values : ( this._values = new this.Values( this ) );
        },

        entrySet: function () {
            return this.__entrySet0();
        },

        __entrySet0: function () {
            var entrySet = this.__entrySet;
            return entrySet !== null ? entrySet : ( this.__entrySet = new this.EntrySet( this ) );
        },

        /** @private */
        HashIterator: qx.Class.define( 'jsava.util.HashMap.HashIterator', {
            extend: jsava.lang.Object,
            implement: [jsava.util.Iterator],

            type: 'abstract',

            /** @protected */
            construct: function (thisHashMap) {
                this.__thisHashMap = thisHashMap;
                this._expectedModCount = this.__thisHashMap._modCount;
                if( this.__thisHashMap._size > 0 ) {
                    var table = this.__thisHashMap._table;
                    while( this._index < table.length && ( this._next = table[this._index++] ) === null ) {
                        // do nothing
                    }
                }
            },

            members: {
                __thisHashMap: null,

                /** @type jsava.util.HashMap.Entry */
                _next: null,
                /** @type Number */
                _expectedModCount: 0,
                /** @type Number */
                _index: 0,
                /** @type jsava.util.HashMap.Entry */
                _current: null,

                hasNext: function () {
                    return this._next !== null;
                },

                _nextEntry: function () {
                    if( this.__thisHashMap._modCount !== this._expectedModCount ) {
                        throw new jsava.lang.ConcurrentModificationException();
                    }

                    var entry = this._next;
                    if( entry === null ) {
                        throw new jsava.lang.NoSuchElementException();
                    }

                    if( (this._next = entry._next) === null ) {
                        var table = this.__thisHashMap._table;
                        while( this._index < table.length && ( this._next = table[this._index++] ) === null ) {
                            // do nothing
                        }
                    }

                    this._current = entry;
                    return entry;
                },

                remove: function () {
                    if( this._current === null ) {
                        throw new jsava.lang.IllegalStateException();
                    }

                    if( this.__thisHashMap._modCount !== this._expectedModCount ) {
                        throw new jsava.lang.ConcurrentModificationException();
                    }

                    var key = this._current._key;
                    this._current = null;
                    this.__thisHashMap._removeEntryForKey( key );
                    this._expectedModCount = this.__thisHashMap._modCount;
                }
            }
        } ),

        _newKeyIterator: function () {
            return new this.KeyIterator( this );
        },

        _newValueIterator: function () {
            return new this.ValueIterator( this );
        },

        _newEntryIterator: function () {
            return new this.EntryIterator( this );
        },

        /** @private */
        ValueIterator: qx.Class.define( 'jsava.util.HashMap.ValueIterator', {
            extend: jsava.util.HashMap.HashIterator,

            construct: function (thisHashMap) {
                this.base( arguments, thisHashMap );
            },

            members: {
                next: function () {
                    return this._nextEntry()._value;
                }
            }
        } ),

        /** @private */
        KeyIterator: qx.Class.define( 'jsava.util.HashMap.KeyIterator', {
            extend: jsava.util.HashMap.HashIterator,

            construct: function (thisHashMap) {
                this.base( arguments, thisHashMap );
            },

            members: {
                next: function () {
                    return this._nextEntry().getKey();
                }
            }
        } ),

        /** @private */
        EntryIterator: qx.Class.define( 'jsava.util.HashMap.EntryIterator', {
            extend: jsava.util.HashMap.HashIterator,

            construct: function (thisHashMap) {
                this.base( arguments, thisHashMap );
            },

            members: {
                next: function () {
                    return this._nextEntry();
                }
            }
        } ),

        /** @private */
        KeySet: qx.Class.define( 'jsava.util.HashMap.KeySet', {
            extend: jsava.util.AbstractSet,

            construct: function (thisHashMap) {
                this.base( arguments );
                this.__thisHashMap = thisHashMap;
            },

            members: {
                __thisHashMap: null,

                iterator: function () {
                    return this.__thisHashMap._newKeyIterator();
                },

                size: function () {
                    return this.__thisHashMap._size;
                },

                contains: function (obj) {
                    return this.__thisHashMap.containsKey( obj );
                },

                remove: function (obj) {
                    return this.__thisHashMap._removeEntryForKey( obj ) !== null;
                },

                clear: function () {
                    this.__thisHashMap.clear();
                }
            }
        } ),

        /** @private */
        Values: qx.Class.define( 'jsava.util.HashMap.Values', {
            extend: jsava.util.AbstractCollection,

            construct: function (thisHashMap) {
                this.base( arguments );
                this.__thisHashMap = thisHashMap;
            },

            members: {
                __thisHashMap: null,

                iterator: function () {
                    return this.__thisHashMap._newValueIterator();
                },

                size: function () {
                    return this.__thisHashMap._size;
                },

                contains: function (obj) {
                    return this.__thisHashMap.containsValue( obj );
                },

                clear: function () {
                    this.__thisHashMap.clear();
                }
            }
        } ),

        /** @private */
        EntrySet: qx.Class.define( 'jsava.util.HashMap.EntrySet', {
            extend: jsava.util.AbstractSet,

            construct: function (thisHashMap) {
                this.base( arguments );
                this.__thisHashMap = thisHashMap;
            },

            members: {
                __thisHashMap: null,

                iterator: function () {
                    return this.__thisHashMap._newEntryIterator();
                },

                size: function () {
                    return this.__thisHashMap._size;
                },

                contains: function (obj) {
                    if( obj === null || !qx.Class.implementsInterface( obj, jsava.util.Map.Entry ) ) {
                        return false;
                    }

                    /** @implements jsava.util.Map.Entry */
                    var entry = obj,
                        candidate = this.__thisHashMap._getEntry( entry.getKey() );
                    return candidate !== null && candidate.equals( entry );
                },

                remove: function (obj) {
                    return this.__thisHashMap._removeMapping( obj ) !== null;
                },

                clear: function () {
                    this.__thisHashMap.clear();
                }
            }
        } )
    }
} );
Ingo Bürk
fuente
Hmm enfoque interesante ... ¿has considerado probar un enfoque automatizado? es decir, ejecutar un compilador de Java a JavaScript en el código fuente para la implementación actual de Java?
Claudiu
No :) Esto es solo un proyecto divertido para mí y había bastantes cosas en las que no podía simplemente "copiar" el código. No conozco los compiladores de Java a Javascript, aunque creo que existen. No estoy seguro de qué tan bien traducirían esto. Sin embargo, estoy bastante seguro de que no producirían código de buena calidad en ningún caso.
Ingo Bürk
Ah te tengo. Estaba pensando en el compilador de Google Web Toolkit , pero parece que terminaron haciendo lo que está haciendo aquí para las bibliotecas principales: "El compilador GWT admite la gran mayoría del lenguaje Java. La biblioteca de tiempo de ejecución GWT emula un subconjunto relevante de Biblioteca de tiempo de ejecución de Java ". ¡Tal vez algo que mirar para ver cómo otros resolvieron el mismo problema!
Claudiu
Si. Estoy seguro de que la solución de Google está mucho más allá de la mía, pero de nuevo, solo me estoy divirtiendo jugando. Desafortunadamente, el código fuente parece haber sido revocado (?), Al menos no puedo navegarlo y los enlaces interesantes parecen estar muertos. Lástima, me hubiera encantado mirarlo.
Ingo Bürk
Divertirse jugando es la mejor manera de aprender =). gracias por compartir
Claudiu
0

Sin embargo, otra implementación del mapa por mí. Con randomizer, 'generics' e 'iterator' =)

var HashMap = function (TKey, TValue) {
    var db = [];
    var keyType, valueType;

    (function () {
        keyType = TKey;
        valueType = TValue;
    })();

    var getIndexOfKey = function (key) {
        if (typeof key !== keyType)
            throw new Error('Type of key should be ' + keyType);
        for (var i = 0; i < db.length; i++) {
            if (db[i][0] == key)
                return i;
        }
        return -1;
    }

    this.add = function (key, value) {
        if (typeof key !== keyType) {
            throw new Error('Type of key should be ' + keyType);
        } else if (typeof value !== valueType) {
            throw new Error('Type of value should be ' + valueType);
        }
        var index = getIndexOfKey(key);
        if (index === -1)
            db.push([key, value]);
        else
            db[index][1] = value;
        return this;
    }

    this.get = function (key) {
        if (typeof key !== keyType || db.length === 0)
            return null;
        for (var i = 0; i < db.length; i++) {
            if (db[i][0] == key)
                return db[i][1];
        }
        return null;
    }

    this.size = function () {
        return db.length;
    }

    this.keys = function () {
        if (db.length === 0)
            return [];
        var result = [];
        for (var i = 0; i < db.length; i++) {
            result.push(db[i][0]);
        }
        return result;
    }

    this.values = function () {
        if (db.length === 0)
            return [];
        var result = [];
        for (var i = 0; i < db.length; i++) {
            result.push(db[i][1]);
        }
        return result;
    }

    this.randomize = function () {
        if (db.length === 0)
            return this;
        var currentIndex = db.length, temporaryValue, randomIndex;
        while (0 !== currentIndex) {
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex--;
            temporaryValue = db[currentIndex];
            db[currentIndex] = db[randomIndex];
            db[randomIndex] = temporaryValue;
        }
        return this;
    }

    this.iterate = function (callback) {
        if (db.length === 0)
            return false;
        for (var i = 0; i < db.length; i++) {
            callback(db[i][0], db[i][1]);
        }
        return true;
    }
}

Ejemplo:

var a = new HashMap("string", "number");
a.add('test', 1132)
 .add('test14', 666)
 .add('1421test14', 12312666)
 .iterate(function (key, value) {console.log('a['+key+']='+value)});
/*
a[test]=1132
a[test14]=666
a[1421test14]=12312666 
*/
a.randomize();
/*
a[1421test14]=12312666
a[test]=1132
a[test14]=666
*/
ovnia
fuente