JavaScript Hashmap เทียบเท่า


353

ชัดเจนในการปรับปรุง 3 ในคำตอบนี้สัญกรณ์นี้:

var hash = {};
hash[X]

ไม่จริงกัญชาวัตถุX; จริงๆแล้วมันจะแปลงXเป็นสตริง (ผ่านทาง.toString()ถ้าเป็นวัตถุหรือมีการแปลงในตัวสำหรับประเภทดั้งเดิมต่างๆ) แล้วดูสตริงนั้นโดยไม่ต้องแปลงเป็น " hash" ความเท่าเทียมกันของวัตถุยังไม่ได้รับการตรวจสอบ - หากวัตถุสองชิ้นที่แตกต่างกันมีการแปลงสตริงเหมือนกันพวกเขาก็จะเขียนทับกัน

ให้นี้ - มีการใช้งานที่มีประสิทธิภาพของ hashmaps ใน javascript หรือไม่ (ตัวอย่างเช่นผลการค้นหาครั้งที่ 2 ของ Google ที่ให้javascript hashmapผลการดำเนินการซึ่งเป็น O (n) สำหรับการดำเนินการใด ๆ ผลการทดสอบอื่น ๆ นั้นไม่สนใจความจริงที่ว่าวัตถุที่แตกต่างกัน


1
@Claudiu: ขออภัยสำหรับการแก้ไข แต่ "แผนที่" ในชื่อเรื่องนั้นทำให้เข้าใจผิดจริงๆ ย้อนกลับไปถ้าคุณไม่เห็นด้วยฉันไม่ได้ตั้งใจจะอุปถัมภ์ :)
Tomalak

6
@Claudiu: คุณถามคำถามมากมายเกี่ยวกับจาวาสคริปต์ คำถามที่ดี ฉันชอบมัน.
บาง

2
@Claudiu: นอกจากนี้คุณสามารถลิงก์ไปยังผลลัพธ์ Google ที่คุณอ้างถึงได้หรือไม่ Google เวอร์ชันในท้องถิ่นที่แตกต่างกันให้ผลลัพธ์ที่แตกต่างการใช้งานที่คุณอ้างถึงดูเหมือนจะไม่ปรากฏสำหรับฉัน
Tomalak

@ Tomalak: ฉันเพิ่งจะเขียนสิ่งเดียวกัน!
บาง

3
@Claudiu ไม่ไม่ต้องเชื่อมโยงไปยัง google เชื่อมโยงไปยังหน้าเว็บที่คุณกำลังพูดถึง (ซึ่งคุณพบผ่าน google) การเชื่อมโยงไปยัง google มีปัญหาเช่นเดียวกับการอธิบายสิ่งที่จะค้นหา: google การปรับแต่งผลการค้นหาขึ้นอยู่กับสถานที่หรือประวัติการค้นหาผลการค้นหาของ google เปลี่ยนแปลงไปตามกาลเวลา (ปัจจุบันนี่คือผลการค้นหาอันดับต้น ๆ ) แสดงผลลัพธ์ที่แตกต่าง
แจสเปอร์

คำตอบ:


369

ทำไมไม่แฮชวัตถุของคุณด้วยตนเองและใช้สตริงผลลัพธ์เป็นกุญแจสำหรับพจนานุกรม JavaScript ปกติ หลังจากคุณอยู่ในตำแหน่งที่ดีที่สุดที่จะรู้ว่าสิ่งที่ทำให้วัตถุของคุณไม่ซ้ำกัน นั่นคือสิ่งที่ฉันทำ.

ตัวอย่าง:

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;

วิธีนี้คุณสามารถควบคุมการจัดทำดัชนีโดย JavaScript ได้โดยไม่ต้องยกหน่วยความจำและการจัดการข้อมูลมากเกินไป

แน่นอนถ้าคุณต้องการ "โซลูชันระดับอุตสาหกรรม" อย่างแท้จริงคุณสามารถสร้างคลาสที่กำหนดค่าโดยฟังก์ชันหลักและด้วย API ที่จำเป็นทั้งหมดของคอนเทนเนอร์ แต่ ... เราใช้ JavaScript และพยายามเรียบง่ายและมีน้ำหนักเบาดังนั้น โซลูชันการทำงานนี้ง่ายและรวดเร็ว

ฟังก์ชั่นคีย์สามารถทำได้ง่ายเพียงแค่เลือกคุณสมบัติที่เหมาะสมของวัตถุเช่นคีย์หรือชุดของคีย์ซึ่งเป็นเอกลักษณ์อยู่แล้วการรวมกันของคีย์ซึ่งเป็นเอกลักษณ์ร่วมกันหรือซับซ้อนเช่นเดียวกับการใช้แฮชการเข้ารหัสบางอย่างเช่น ในDojoX การเข้ารหัสหรือDojoX UUID ในขณะที่วิธีแก้ปัญหาหลังอาจสร้างกุญแจที่เป็นเอกลักษณ์ แต่ส่วนตัวฉันพยายามหลีกเลี่ยงพวกมันโดยเฉพาะถ้าฉันรู้ว่าอะไรทำให้วัตถุของฉันไม่เหมือนใคร

อัปเดตในปี 2557:ตอบกลับในปี 2551 โซลูชันที่เรียบง่ายนี้ยังต้องการคำอธิบายเพิ่มเติม ฉันขออธิบายแนวคิดในแบบฟอร์มถามตอบ

โซลูชันของคุณไม่มีแฮชจริง มันอยู่ที่ไหน???

JavaScript เป็นภาษาระดับสูง พื้นฐานดั้งเดิม ( Object ) รวมถึงตารางแฮชเพื่อให้คุณสมบัติ ตารางแฮชนี้มักเขียนด้วยภาษาระดับต่ำเพื่อประสิทธิภาพ การใช้ออบเจ็กต์อย่างง่ายพร้อมกับคีย์สตริงเราใช้ตารางแฮชที่นำไปใช้อย่างมีประสิทธิภาพโดยไม่มีความพยายามในส่วนของเรา

คุณจะรู้ได้อย่างไรว่าพวกเขาใช้แฮช

มีสามวิธีที่สำคัญในการเก็บรวบรวมวัตถุที่คีย์แอดเดรส:

  • เรียงลำดับ ในกรณีนี้เพื่อดึงข้อมูลวัตถุโดยใช้กุญแจเราจะต้องข้ามกุญแจทั้งหมดเมื่อเราพบมัน โดยเฉลี่ยจะใช้การเปรียบเทียบ n / 2
  • สั่งซื้อ
    • ตัวอย่าง # 1: อาร์เรย์ที่เรียงลำดับ - ทำการค้นหาแบบไบนารีเราจะพบกุญแจของเราหลังจากการเปรียบเทียบโดยเฉลี่ย ~ log2 (n) ดีกว่ามาก
    • ตัวอย่าง # 2: ต้นไม้ อีกครั้งมันจะเป็น ~ log (n) ครั้ง
  • ตารางแฮช โดยเฉลี่ยแล้วมันต้องใช้เวลาคงที่ เปรียบเทียบ: O (n) กับ O (log n) กับ O (1) ความเจริญ

เห็นได้ชัดว่าวัตถุ JavaScript ใช้ตารางแฮชในบางรูปแบบเพื่อจัดการกรณีทั่วไป

ผู้ขายเบราว์เซอร์ใช้ตารางแฮชจริงๆหรือ

จริงๆ.

พวกเขาจัดการกับการชนกันหรือไม่?

ใช่. ดูด้านบน. หากคุณพบการชนกันของสตริงที่ไม่เท่ากันโปรดอย่าลังเลที่จะส่งข้อบกพร่องกับผู้ขาย

แล้วความคิดของคุณคืออะไร

หากคุณต้องการแฮชวัตถุให้ค้นหาสิ่งที่ทำให้เป็นเอกลักษณ์และใช้เป็นกุญแจ อย่าพยายามคำนวณตารางแฮชจริงหรือจำลองตารางแฮช - มันได้รับการจัดการอย่างมีประสิทธิภาพโดยวัตถุ JavaScript พื้นฐาน

ใช้คีย์นี้กับ JavaScript Objectเพื่อใช้ประโยชน์จากตารางแฮชในตัวในขณะที่ไม่ต้องมีการปะทะกับคุณสมบัติเริ่มต้น

ตัวอย่างเพื่อให้คุณเริ่มต้น:

  • หากวัตถุของคุณมีชื่อผู้ใช้ที่ไม่ซ้ำกัน - ใช้มันเป็นกุญแจสำคัญ
  • หากมีหมายเลขลูกค้าที่ไม่ซ้ำกัน - ใช้เป็นรหัส
    • หากมีหมายเลขที่ออกโดยรัฐบาลที่ไม่ซ้ำกันเช่น SSN หรือหมายเลขหนังสือเดินทางและระบบของคุณไม่อนุญาตการทำซ้ำ - ใช้เป็นรหัส
  • หากมีการรวมกันของเขตข้อมูลที่ไม่ซ้ำกัน - ใช้มันเป็นกุญแจสำคัญ
    • ตัวย่อสถานะ + หมายเลขใบอนุญาตขับรถทำให้เป็นกุญแจสำคัญที่ยอดเยี่ยม
    • ตัวย่อประเทศ + หมายเลขหนังสือเดินทางเป็นกุญแจสำคัญเช่นกัน
  • ฟังก์ชั่นบางอย่างในฟิลด์หรือวัตถุทั้งหมดสามารถคืนค่าที่ไม่ซ้ำกัน - ใช้มันเป็นกุญแจสำคัญ

ฉันใช้คำแนะนำของคุณและแคชวัตถุทั้งหมดโดยใช้ชื่อผู้ใช้ แต่ผู้ชายที่ฉลาดบางคนมีชื่อว่า "toString" ซึ่งเป็นคุณสมบัติในตัว! สิ่งที่ฉันควรทำตอนนี้?

เห็นได้ชัดว่าถ้าเป็นไปได้จากระยะไกลว่ากุญแจผลลัพธ์จะประกอบด้วยตัวอักษรละตินเท่านั้นคุณควรทำอะไรกับมัน ตัวอย่างเช่นเพิ่มอักขระ Unicode ที่ไม่ใช่ละตินใด ๆ ที่คุณชอบที่จุดเริ่มต้นหรือตอนท้ายเพื่อยกเลิกการปะทะด้วยคุณสมบัติเริ่มต้น: "#toString", "#MarySmith" หากมีการใช้คีย์ผสมให้แยกส่วนประกอบของคีย์โดยใช้ตัวคั่นที่ไม่ใช่ละติน: "ชื่อเมืองรัฐ"

โดยทั่วไปนี่คือสถานที่ที่เราจะต้องมีความคิดสร้างสรรค์และเลือกปุ่มที่ง่ายที่สุดที่มีข้อ จำกัด ที่กำหนด (ไม่ซ้ำกันการปะทะที่อาจเกิดขึ้นกับคุณสมบัติเริ่มต้น)

หมายเหตุ: Objectปุ่มที่ไม่ซ้ำกันไม่ได้ปะทะกันตามคำนิยามในขณะที่การปะทะกันกัญชาที่มีศักยภาพจะถูกจัดการโดยพื้นฐาน

ทำไมคุณไม่ชอบโซลูชันทางอุตสาหกรรม

IMHO รหัสที่ดีที่สุดคือไม่มีรหัส: ไม่มีข้อผิดพลาดไม่ต้องบำรุงรักษาเข้าใจง่ายและดำเนินการทันที "ตารางแฮชใน JavaScript" ทั้งหมดที่ฉันเห็นคือ> รหัส 100 บรรทัดและเกี่ยวข้องกับวัตถุหลายรายการ เปรียบเทียบกับ: dict[key] = value.

อีกจุดหนึ่ง: เป็นไปได้ไหมที่จะเอาชนะประสิทธิภาพของออบเจกต์ดั้งเดิมที่เขียนด้วยภาษาระดับต่ำโดยใช้ JavaScript และออบเจกต์ดั้งเดิมเหมือนกัน

ฉันยังต้องการแฮชวัตถุของฉันโดยไม่มีกุญแจ!

เราอยู่ในโชค: ECMAScript 6 (กำหนดไว้สำหรับกลาง 2015 ปล่อยให้หรือใช้เวลา 1-2 ปีหลังจากนั้นจะกลายเป็นที่แพร่หลาย) กำหนด แผนที่และ ชุด

ตัดสินโดยคำจำกัดความที่พวกเขาสามารถใช้ที่อยู่ของวัตถุเป็นกุญแจสำคัญซึ่งทำให้วัตถุที่แตกต่างได้ทันทีโดยไม่ต้องใช้กุญแจเทียม OTOH สองวัตถุที่แตกต่างกัน แต่เหมือนกันจะถูกแมปอย่างชัดเจน

การเปรียบเทียบรายละเอียดจากMDN :

วัตถุคล้ายกับแผนที่ซึ่งทั้งสองอย่างให้คุณตั้งค่าคีย์เรียกคืนค่าเหล่านั้นลบคีย์และตรวจสอบว่ามีบางอย่างถูกเก็บไว้ที่คีย์ ด้วยเหตุนี้ (และเนื่องจากไม่มีตัวเลือกในตัว) วัตถุจึงถูกใช้เป็นแผนที่ในอดีต อย่างไรก็ตามมีความแตกต่างที่สำคัญที่ทำให้การใช้แผนที่เป็นที่นิยมในบางกรณี:

  • กุญแจของวัตถุคือสตริงและสัญลักษณ์ในขณะที่พวกเขาสามารถเป็นค่าใด ๆ สำหรับแผนที่รวมถึงฟังก์ชั่นวัตถุและดั้งเดิมใด ๆ
  • คีย์ในแผนที่จะถูกจัดเรียงในขณะที่ปุ่มที่เพิ่มให้กับวัตถุนั้นไม่ได้จัดทำ ดังนั้นเมื่อวนซ้ำวัตถุแผนที่จะส่งคืนคีย์ตามลำดับการแทรก
  • คุณสามารถรับขนาดของแผนที่ได้อย่างง่ายดายด้วยคุณสมบัติขนาดในขณะที่ต้องกำหนดจำนวนคุณสมบัติในวัตถุด้วยตนเอง
  • แผนที่คือการทำซ้ำและสามารถทำซ้ำได้โดยตรงในขณะที่การวนซ้ำวัตถุต้องได้รับกุญแจในบางแบบและวนซ้ำมัน
  • วัตถุมีต้นแบบดังนั้นจึงมีปุ่มเริ่มต้นในแผนที่ที่สามารถชนกับปุ่มของคุณได้หากคุณไม่ระวัง ในฐานะที่เป็น ES5 นี้สามารถข้ามได้โดยใช้ map = Object.create (null) แต่สิ่งนี้ทำได้ยาก
  • แผนที่อาจทำงานได้ดีขึ้นในสถานการณ์ที่เกี่ยวข้องกับการเพิ่มและการลบคู่กุญแจบ่อยครั้ง

13
สิ่งนี้ดูไม่เหมือนแผนที่ที่เหมาะสมเพราะคุณไม่จัดการกับการชน หากสิ่งนี้เกิดขึ้นจริง: hash (obj1) == hash (obj2) คุณจะสูญเสียข้อมูลของคุณ
เนื้อวัว

32
สวรรค์ช่วยคุณได้เมื่อทั้ง "PAUL AINLEY" และ "PAULA INLEY" ลงทะเบียนในระบบของคุณ ...
Matt R

34
@MattR ที่จริงตัวอย่างของคุณจะทำงานได้อย่างถูกต้องหากไม่มีสวรรค์ช่วยแม้จะมีฟังก์ชันแฮชจำลอง ฉันหวังว่าผู้อ่านคนอื่นจะรู้ว่ามีการใช้ฟังก์ชั่นแฮชที่ไม่ใช้ความเรียบง่ายเกินความจริงเป็นตัวยึดตำแหน่งเพื่อแสดงเทคนิคที่แตกต่าง ทั้งคอมเม้นท์โค้ดและคำตอบนั้นเน้นว่ามันไม่จริง การเลือกกุญแจที่ถูกต้องจะกล่าวถึงในวรรคสุดท้ายของคำตอบ
Eugene Lazutkin

6
@EugeneLazutkin - คุณยังเข้าใจผิดอยู่ฉันกลัว ตัวอย่างของคุณยังมีแนวโน้มที่จะแฮ็ชชนกัน อย่าคิดว่าการใส่นามสกุลก่อนจะช่วยคุณได้!
Matt R

3
@EugeneLazutkin คนส่วนใหญ่ไม่อ่านคุณมีคำตอบก่อน ES6 นี้ปรากฏแม้ ... ฉันขอแสดงความยินดีสำหรับความรู้ JS ลึกของคุณ
Gabriel Andrés Brancolini

171

คำอธิบายปัญหา

JavaScript ไม่มีประเภทแผนที่ทั่วไปในตัว(บางครั้งเรียกว่าอาเรย์แบบเชื่อมโยงหรือพจนานุกรม ) ซึ่งอนุญาตให้เข้าถึงค่าโดยพลการด้วยคีย์ที่กำหนดเอง โครงสร้างข้อมูลพื้นฐานของจาวาสคริปต์คือวัตถุซึ่งเป็นแผนที่ชนิดพิเศษซึ่งยอมรับเฉพาะสตริงเป็นกุญแจเท่านั้นและมีความหมายพิเศษเช่นการถ่ายทอดทางพันธุกรรมต้นแบบผู้ได้รับและผู้ตั้งค่าและวูดูเพิ่มเติม

เมื่อ usings วัตถุเป็นแผนที่คุณต้องจำไว้ว่ากุญแจสำคัญที่จะถูกแปลงเป็นค่าสตริงผ่านtoString()ซึ่งผลลัพธ์ในการทำแผนที่5และ'5'เป็นค่าเดียวกันและวัตถุทั้งหมดที่ไม่ได้เขียนทับวิธีการเพื่อให้ค่าดัชนีโดยtoString() คุณอาจจะยังไม่ได้ตั้งใจเข้าถึงคุณสมบัติได้รับมรดกของมันถ้าคุณไม่ได้ตรวจสอบ'[object Object]'hasOwnProperty()

ชนิดอาร์เรย์ในตัวของ JavaScript ไม่ช่วยหนึ่งบิต: อาร์เรย์ JavaScript ไม่ใช่อาร์เรย์เชื่อมโยง แต่เป็นเพียงวัตถุที่มีคุณสมบัติพิเศษอีกสองสามตัว หากคุณต้องการที่จะรู้ว่าทำไมพวกเขาไม่สามารถใช้เป็นแผนที่ดูที่นี่

ทางออกของยูจีน

Eugene Lazutkin ได้อธิบายแนวคิดพื้นฐานของการใช้ฟังก์ชันแฮชแบบกำหนดเองเพื่อสร้างสตริงที่ไม่ซ้ำซึ่งสามารถใช้เพื่อค้นหาค่าที่เกี่ยวข้องเป็นคุณสมบัติของวัตถุพจนานุกรม นี้มักจะเป็นวิธีที่เร็วที่สุดเพราะวัตถุที่จะดำเนินการภายในเป็นตารางแฮช

  • หมายเหตุ:ตารางแฮช (บางครั้งเรียกว่าแผนที่แฮช ) เป็นการใช้งานแนวคิดแผนที่โดยเฉพาะโดยใช้อาเรย์สำรองและค้นหาผ่านค่าแฮชตัวเลข สภาพแวดล้อมรันไทม์อาจใช้โครงสร้างอื่น ๆ (เช่นแผนผังการค้นหาหรือรายการข้าม ) เพื่อใช้วัตถุ JavaScript แต่เนื่องจากวัตถุเป็นโครงสร้างข้อมูลพื้นฐานจึงควรปรับให้เหมาะสม

เพื่อให้ได้ค่าแฮชที่ไม่ซ้ำกันสำหรับวัตถุใด ๆ ความเป็นไปได้อย่างหนึ่งคือการใช้ตัวนับโกลบอลและแคชค่าแฮชในวัตถุเอง (เช่นในคุณสมบัติที่มีชื่อ__hash)

ฟังก์ชันแฮชซึ่งทำหน้าที่นี้และใช้ได้ทั้งกับค่าดั้งเดิมและวัตถุคือ:

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

hash.current = 0;

ฟังก์ชั่นนี้สามารถใช้งานได้ตามที่อธิบายโดยยูจีน เพื่อความสะดวกเราจะทำการห่อต่อในMapชั้นเรียน

Mapการใช้งานของฉัน

การใช้งานต่อไปนี้จะเก็บคู่คีย์ - ค่าเพิ่มเติมในรายการที่ลิงก์ซ้ำกันเพื่อให้สามารถทำซ้ำได้อย่างรวดเร็วบนทั้งคีย์และค่า ในการจัดหาฟังก์ชันแฮชของคุณเองคุณสามารถเขียนทับhash()วิธีของอินสแตนซ์หลังจากการสร้าง

// 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;
};

ตัวอย่าง

สคริปต์ต่อไปนี้

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());

สร้างผลลัพธ์นี้:

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

ข้อควรพิจารณาเพิ่มเติม

PEZ แนะนำให้เขียนทับtoString()วิธีดังกล่าวด้วยฟังก์ชันแฮชของเรา สิ่งนี้ไม่เป็นไปได้เพราะไม่ได้ผลกับค่าดั้งเดิม (การเปลี่ยนค่าtoString()ดั้งเดิมเป็นความคิดที่แย่มาก ) ถ้าเราต้องการtoString()ที่จะกลับค่าที่มีความหมายสำหรับวัตถุโดยพลการที่เราจะมีการปรับเปลี่ยนObject.prototypeซึ่งบางคน (ตัวเองไม่รวม) พิจารณาverboten


แก้ไข:รุ่นปัจจุบันของฉันMapการดำเนินการเช่นเดียวกับสารพัด JavaScript อื่น ๆสามารถหาได้จากที่นี่


ES5 เลิกใช้งาน callee ( goo.gl/EeStE ) แต่ผมขอแนะนำและในแผนที่คอนสตรัคทำMap._counter = 0 this._hash = 'object ' + Map._counter++จากนั้นแฮช () จะกลายเป็นreturn (value && value._hash) || (typeof(value) + ' ' + String(value));
broofa

ลิงก์ไปยังรหัสใช้งานไม่ได้: mercurial.intuxication.org/hg/js-hacks/raw-file/tip/map.js
ahcox

สวัสดี @Christoph คุณช่วยอัพเดทลิงค์ของคุณให้เป็นที่ซึ่งฉันสามารถหาการใช้งานแผนที่ได้ไหม
NumenorForLife

2
@ jsc123: ฉันจะดูว่า - ตอนนี้คุณสามารถได้รับการถ่ายโอนข้อมูลของพื้นที่เก็บข้อมูลที่pikacode.com/mercurial.intuxication.org/js-hacks.tar.gz
Christoph

58

ฉันรู้ว่าคำถามนี้ค่อนข้างเก่า แต่มีวิธีแก้ปัญหาที่ยอดเยี่ยมจริงๆในปัจจุบันที่มีห้องสมุดภายนอก

JavaScript ยังมีภาษาที่ให้บริการMapเช่นกัน


2
นี่คือวิธีที่จะก้าวไปข้างหน้าสู่ศตวรรษที่ 21 น่าเสียดายที่ฉันพบโพสต์ของคุณหลังจากเขียนโค้ดให้เสร็จด้วยแผนที่ทำเองที่บ้าน WEEE ต้องการคะแนนมากขึ้นสำหรับคำตอบของคุณ
Phung D. An

1
Collections.js มีการใช้งานบางอย่าง แต่ฉันไม่พบสิ่งใดใน underscore.js หรือ lodash ... คุณหมายถึงอะไรในขีดล่างที่จะเป็นประโยชน์
Codebling

@ CodeBling ไม่มีความคิด ฉันคิดว่าฉันสับสนกับฟังก์ชั่นแผนที่ ฉันจะลบมันออกจากคำตอบ
Jamel Toms

3
นั่นยุติธรรม ทุกคนที่พิจารณา Collections.js ควรตระหนักว่ามันได้ทำการดัดแปลงต้นแบบ Array, Function, Object และ Regexp ทั่วโลกในแบบที่มีปัญหา ( ดูปัญหาที่ฉันพบที่นี่ ) แม้ว่าในตอนแรกฉันพอใจกับ collection.js มาก (และเป็นคำตอบนี้) ความเสี่ยงที่เกี่ยวข้องกับการใช้มันสูงเกินไปดังนั้นฉันจึงทิ้งมันไป เฉพาะคอลเล็กชันคอลเลกชัน v2ของ kriskowal (โดยเฉพาะ v2.0.2 +) กำจัดการแก้ไขต้นแบบโกลบอลและปลอดภัยที่จะใช้
Codebling

28

นี่เป็นวิธีที่ง่ายและสะดวกในการใช้สิ่งที่คล้ายกับแผนที่ 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
        }

และเพื่อรับค่า:

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

......  etc  .....

2
ใช้งานได้กับสตริงคีย์เท่านั้น ฉันเชื่อว่า OP มีความสนใจในการใช้กุญแจทุกประเภท
fractor

26

ตามจาวาสคริปต์มาตรฐาน ECMAScript 2015 (ES6) มีการใช้งานแผนที่ ข้อมูลเพิ่มเติมเกี่ยวกับสิ่งที่สามารถพบได้ที่นี่

การใช้งานขั้นพื้นฐาน:

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"

21

คุณสามารถใช้ ES6 WeakMapหรือMap:

  • WeakMaps เป็นแผนที่ / ค่าคีย์ที่เป็นวัตถุ

  • Mapวัตถุเป็นแผนที่คีย์ / ค่าอย่างง่าย ค่าใด ๆ (ทั้งวัตถุและค่าดั้งเดิม) อาจใช้เป็นคีย์หรือค่า

โปรดทราบว่าไม่ได้รับการสนับสนุนอย่างกว้างขวาง แต่คุณสามารถใช้ES6 Shim (ต้องใช้ ES5 ดั้งเดิมหรือES5 Shim ) เพื่อรองรับMapแต่ไม่ใช่WeakMap ( ดูสาเหตุ )


ในปี 2019 พวกเขาได้รับการสนับสนุนเป็นอย่างดีและมีวิธีการที่ยอดเยี่ยม developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
Juanma Menendez

13

คุณจะต้องเก็บไว้ในคู่สถานะภายในของคู่ของวัตถุ / ค่า

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];
  }
}

และใช้มันเช่น:

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));

แน่นอนว่าการติดตั้งใช้งานนี้ยังอยู่ที่ใดที่หนึ่งตามแนวของ O (n) ตัวอย่างของ Eugene ด้านบนเป็นวิธีเดียวที่จะได้รับแฮชที่ทำงานกับความเร็วทุกประเภทที่คุณคาดหวังจากแฮชจริง

ปรับปรุง:

อีกวิธีหนึ่งตามแนวของคำตอบของ Eugene ก็คือการแนบ ID ที่ไม่ซ้ำกับวัตถุทั้งหมด หนึ่งในวิธีการโปรดของฉันคือการใช้หนึ่งในวิธีการในตัวที่สืบทอดมาจาก Object superclass แทนที่ด้วย passthrough ฟังก์ชันที่กำหนดเองและแนบคุณสมบัติกับฟังก์ชันวัตถุนั้น หากคุณต้องเขียนวิธี HashMap ของฉันใหม่ให้ทำเช่นนี้มันจะมีลักษณะดังนี้:

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];
}

รุ่นนี้ดูเหมือนจะเร็วขึ้นเพียงเล็กน้อยเท่านั้น แต่ในทางทฤษฎีแล้วมันจะเร็วขึ้นอย่างมากสำหรับชุดข้อมูลขนาดใหญ่


อาเรย์แบบเชื่อมโยงคืออาเรย์ของ 2-tuples เป็นแผนที่ไม่ใช่ HashMap HashMap เป็นแผนที่ที่ใช้แฮชเพื่อประสิทธิภาพที่ดีขึ้น
Erik Kaplun

จริง แต่ทำไมเส้นผมแตกในหัวข้อ? ไม่มีวิธีสร้างแผนที่แฮชจริงใน JavaScript เนื่องจากคุณไม่สามารถรับที่อยู่หน่วยความจำวัตถุ และคู่คีย์ / ค่าของออบเจ็กต์ในตัว (ใช้ในตัวอย่างที่สองของฉัน) อาจทำหน้าที่เป็น HashMaps แต่ไม่จำเป็นเพราะมันขึ้นอยู่กับรันไทม์ที่ใช้ในเบราว์เซอร์ว่ามีการใช้งานการค้นหาอย่างไร
pottedmeat

11

น่าเสียดายที่ไม่มีคำตอบข้างต้นที่ดีสำหรับกรณีของฉัน: วัตถุสำคัญที่แตกต่างกันอาจมีรหัสแฮชเดียวกัน ดังนั้นฉันจึงเขียนเวอร์ชัน HashMap แบบ 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;
}

หมายเหตุ: วัตถุหลักจะต้อง "ใช้งาน" เมธอด hashCode () และ equals ()


7
การตั้งค่าที่new Array()สูงเกินไป[]คือการทำให้แน่ใจว่า Java-likeness ของรหัสของคุณคืออะไร? :)
Erik Kaplun

6

ฉันใช้งาน JavaScript HashMap ซึ่งสามารถรับรหัสได้ http://github.com/lambder/HashMapJS/tree/master

นี่คือรหัส:

/*
 =====================================================================
 @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")
}

2
รหัสดูเหมือนจะไม่ทำงานร่วมกับการวางวัตถุเดียวกันในหลายHashMaps
Erik Kaplun

5

ใน ECMA6 คุณสามารถใช้WeakMap

ตัวอย่าง:

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

แต่:

Because of references being weak, WeakMap keys are not enumerable (i.e. there is no method giving you a list of the keys). 

โอ้สรรเสริญพระเยซูในที่สุดพวกเขาก็เพิ่มการอ้างอิงที่อ่อนแอให้กับจาวาสคริปต์ มันเกี่ยวกับเวลา ... +1 สำหรับสิ่งนั้น แต่จริงๆแล้วมันน่ากลัวที่จะใช้เพราะการอ้างอิงนั้นอ่อนแอ
Claudiu

2

Javascript ไม่ได้สร้างใน Map / hashmap มันควรจะเรียกว่าอาเรย์

hash["X"]เท่ากับhash.Xแต่อนุญาตให้ "X" เป็นตัวแปรสตริง ในคำอื่น ๆhash[x]คือหน้าที่เท่ากับeval("hash."+x.toString())

มันคล้ายกับ object.properties มากกว่าการแม็พคีย์ - ค่า หากคุณกำลังมองหาการทำแผนที่คีย์ / ค่าที่ดีขึ้นใน Javascript โปรดใช้วัตถุแผนที่ซึ่งคุณสามารถค้นหาบนเว็บ


2

ลองใช้งานตารางแฮช JavaScript ของฉัน: http://www.timdown.co.uk/jshashtable

มันจะมองหาวิธีการ hashCode () ของวัตถุที่สำคัญหรือคุณสามารถจัดหาฟังก์ชั่นการแฮชเมื่อสร้างวัตถุ Hashtable


2

ดูเหมือนว่าโซลูชันที่แข็งแกร่งพอสมควร: https://github.com/flesler/hashmap มันจะทำงานได้ดีสำหรับฟังก์ชั่นและวัตถุที่มีลักษณะเหมือนกัน แฮ็คเดียวที่ใช้คือการเพิ่มสมาชิกที่ไม่ชัดเจนในวัตถุเพื่อระบุตัวตน หากโปรแกรมของคุณไม่เขียนทับตัวแปรที่ไม่ชัดเจน (ซึ่งคล้ายกับhashid ) แสดงว่าคุณเป็นทอง


2

ถ้าประสิทธิภาพไม่สำคัญ (เช่นจำนวนของปุ่มมีขนาดค่อนข้างเล็ก) และคุณไม่ต้องการที่จะก่อให้เกิดมลพิษของคุณ (หรืออาจจะไม่ได้ของคุณ) วัตถุที่มีเขตข้อมูลเพิ่มเติมเช่น_hash, _idฯลฯ แล้วคุณสามารถใช้ประโยชน์จากความจริงที่ว่าArray.prototype.indexOfมีพนักงาน ความเท่าเทียมกันที่เข้มงวด นี่คือการดำเนินการที่เรียบง่าย:

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;
})();

ตัวอย่างการใช้งาน:

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.

เมื่อเทียบกับ ES6 WeakMap มันมีสองประเด็น: O (n) เวลาค้นหาและไม่ใช่จุดอ่อน (เช่นจะทำให้หน่วยความจำรั่วหากคุณไม่ได้ใช้deleteหรือclearปล่อยกุญแจ)


2

My Map Implementation ซึ่งมาจากตัวอย่างของ Christoph:

ตัวอย่างการใช้งาน:

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;
};

1

การเพิ่มโซลูชันอื่น: HashMapเป็นชั้นแรกที่ฉันย้ายจาก Java ไปยัง Javascript คุณสามารถพูดได้ว่ามีค่าใช้จ่ายจำนวนมาก แต่การใช้งานเกือบเท่ากับ 100% เท่ากับการใช้งานของ Java และรวมถึงอินเตอร์เฟสและคลาสย่อยทั้งหมด

โครงการสามารถพบได้ที่นี่: https://github.com/Airblader/jsava ฉันจะแนบซอร์สโค้ด (ปัจจุบัน) สำหรับคลาส HashMap แต่ตามที่ระบุไว้มันยังขึ้นอยู่กับคลาสซูเปอร์เป็นต้นกรอบ OOP ที่ใช้ คือ qooxdoo

แก้ไข:โปรดทราบว่ารหัสนี้ล้าสมัยแล้วและอ้างถึงโครงการ GitHub สำหรับงานปัจจุบัน ในการเขียนสิ่งนี้มีArrayListการนำไปปฏิบัติเช่นกัน

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();
                }
            }
        } )
    }
} );

อืมมวิธีที่น่าสนใจ .. คุณเคยลองใช้วิธีการแบบอัตโนมัติหรือไม่? นั่นคือการรันคอมไพเลอร์ Java-to-javascript บนซอร์สโค้ดสำหรับการประยุกต์ใช้จาวาปัจจุบันหรือไม่?
Claudiu

ไม่ :) นี่เป็นเพียงโครงการที่สนุกสำหรับฉันและมีบางสิ่งที่ฉันไม่สามารถเพียงแค่ "คัดลอก" รหัส ฉันไม่ทราบว่าคอมไพเลอร์ Java-to-Javascript แม้ว่าฉันจะเชื่อว่าพวกเขามีอยู่ ฉันไม่แน่ใจว่าพวกเขาจะแปลได้ดีแค่ไหน ฉันค่อนข้างแน่ใจว่าพวกเขาจะไม่สร้างรหัสที่มีคุณภาพดี แต่อย่างใด
Ingo Bürk

อ้า gotcha ฉันคิดว่าคอมไพเลอร์ของGoogle Web Toolkitแต่ดูเหมือนว่าพวกเขาทำสิ่งที่คุณทำที่นี่เพื่อห้องสมุดหลัก: "คอมไพเลอร์ GWT สนับสนุนภาษาจาวาส่วนใหญ่ตัวเองไลบรารีรันไทม์ GWT จำลองส่วนย่อยที่เกี่ยวข้องของ Java runtime library ". บางทีสิ่งที่ต้องดูเพื่อดูว่าคนอื่นแก้ไขปัญหาเดียวกันได้อย่างไร!
Claudiu

ใช่. ฉันแน่ใจว่าทางออกของ Google นั้นไกลเกินกว่าที่จะเป็นจริง แต่อีกครั้งฉันแค่สนุกกับการเล่น น่าเสียดายที่ซอร์สโค้ดดูเหมือนจะถูกเพิกถอน (?) อย่างน้อยฉันก็ไม่สามารถเรียกดูได้และดูเหมือนว่าลิงก์ที่น่าสนใจจะตาย แย่มากฉันอยากจะลองดู
Ingo Bürk

การมีความสนุกสนานในการเล่นเป็นวิธีที่ดีที่สุดในการเรียนรู้ =) ขอบคุณสำหรับการแบ่งปัน
Claudiu

0

อีกหนึ่งการใช้แผนที่ของฉัน ด้วย randomizer 'generics' และ '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;
    }
}

ตัวอย่าง:

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
*/
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.