সরাসরি পর্বে যান
পর্ব ১৮ · ৫৯-এর মধ্যে · মডিউল ৩

Map, Set, WeakMap আর WeakRef — সঠিক সংগ্রহ বেছে নেওয়া

JavaScript Collections — Map, Set, WeakMap, WeakSet, WeakRef & FinalizationRegistry

Taught in Bangla, this lesson explains JavaScript collections: why plain objects make poor collections, Map with any key type and insertion order, Map versus object, Set for unique values and the new union, intersection and difference methods, WeakMap and WeakSet for memory-friendly keys, WeakRef and FinalizationRegistry with care, and everyday counting and grouping patterns.

এই পর্বে যা শিখবেন

  • সাধারণ object কখন যথেষ্ট, আর কখন Map বা Set দরকার — key-এর type, ক্রম, আকার আর prototype-এর ফাঁদ দেখে
  • Map-এর পুরো API: তৈরি, পড়া, মোছা, ঘোরা, গোনা, আর JSON-এ রূপান্তর
  • Set দিয়ে duplicate সরানো, unique visitor গোনা, আর union/intersection/difference
  • WeakMap আর WeakSet কীভাবে garbage collection-এর সাথে কাজ করে, আর কেন এগুলো memory leak ঠেকায়
  • WeakRef ও FinalizationRegistry কী, আর কেন এগুলো “যতটা সম্ভব এড়িয়ে চলুন” ধরনের টুল

১ · Object দিয়ে কেন সব হয় না — রেকর্ড বনাম সংগ্রহ

পর্ব ১৪-এ object দেখেছেন: { name: "শাড়ি", price: 2500, stock: 12 }. এখানে key-গুলো আগে থেকে জানা, সংখ্যায় নির্দিষ্ট, আর সবই string. এ ধরনের ডেটাকে বলা যায় রেকর্ড — একটি জিনিসের নির্দিষ্ট কয়েকটি বৈশিষ্ট্য। রেকর্ডের জন্য object চমৎকার।

এবার একটি ভিন্ন প্রশ্ন। আপনার অনলাইন দোকানে সারাদিন কোন পণ্য কতবার দেখা হলো, তা গুনতে চান। পণ্যের id সংখ্যা: ১০১, ২০৫, ৩০৭… আগে থেকে জানা নেই কোন id আসবে, কতগুলো আসবে। নতুন id এলে যোগ হবে, পণ্য সরালে মুছবে। এ ধরনের ডেটা হলো সংগ্রহ (collection) — একই রকম অনেক জিনিস, যা চলার সময় বাড়ে-কমে। সংগ্রহের জন্য object দিয়েও কাজ চালানো যায়, আর ২০১৫ সালের আগে মানুষ তাই করত। কিন্তু তাতে কয়েকটি গোপন ফাঁদ আছে।

একটি স্কুলের ভর্তি ফর্ম আর হাজিরা খাতার কথা ভাবুন। ভর্তি ফর্মে ঘরগুলো ছাপানো — নাম, পিতার নাম, জন্মতারিখ। এটি রেকর্ড, object-এর মতো। হাজিরা খাতায় প্রতিদিন নতুন নাম যোগ হয়, কেউ বদলি হয়ে চলে যায়, আর আপনাকে প্রায়ই জানতে হয় “মোট কতজন?” এটি সংগ্রহ। ছাপানো ফর্মে হাজিরা রাখা যায় না, যায়ও যদি, প্রতিদিন কাটাকাটি করতে হয়। JavaScript-এর Map আর Set হলো এই হাজিরা খাতা — সংগ্রহের জন্যই বানানো।

Object-কে সংগ্রহ হিসেবে চালালে প্রথম ফাঁদটি key-এর type-এ। সাধারণ object-এর key সবসময় string (বা Symbol). আপনি সংখ্যা দিলে সেটি string হয়ে যায়, আর object দিলে সেটি হয়ে যায় "[object Object]" লেখাটি। নিচের ডায়াগ্রামে একই কাজ object আর Map-এ পাশাপাশি দেখুন। “পরে” চাপুন।

1const obj = {};2const map = new Map();3const user = { name: 'Rafi' };4obj[1] = 'এক';    map.set(1, 'এক');5obj['1'] = 'string এক'; map.set('1', 'string এক');6obj[user] = 'রাফি'; map.set(user, 'রাফি');7const mim = { name: 'Mim' };8console.log(obj[mim], map.get(mim));
ভেতরে কী জমা হলো
obj
Object.keys(obj)
map
  1. দুটি খালি সংগ্রহ। একটি সাধারণ object, একটি Map, আর একজন ব্যবহারকারীর object user. এখনো কিছু রাখা হয়নি।
  2. Number key 1. Object-এ obj[1] লিখলেও engine key-টিকে string '1' বানিয়ে রাখে। Map সংখ্যাটিকে সংখ্যা হিসেবেই রাখে — Map-এ key-এর type বদলায় না।
  3. String key '1'. Object-এ এটি আগের key-ই, তাই পুরোনো মান মুছে নতুন মান বসল — একটি entry হারিয়ে গেল, কোনো সতর্কবার্তা ছাড়াই। Map-এ 1 আর '1' দুটি আলাদা key, তাই এখন দুটি entry.
  4. Object key. Object-এ key হিসেবে object দিলে engine সেটিকে string বানায় — আর যেকোনো সাধারণ object string হলে হয় '[object Object]'. Map object-টিকেই key হিসেবে রাখে, তার reference ধরে।
  5. অন্য একজনকে খোঁজা। mim সম্পূর্ণ আলাদা মানুষ, কিন্তু object-এ সে-ও '[object Object]' হয়ে যায় — তাই পায় রাফির মান! এমন বাগ খুঁজে পাওয়া খুব কঠিন। Map-এ mim নামে কোনো key নেই, তাই সৎভাবে undefined দেয়।
মূল ধারণা

Object-এ key ঢোকার আগে string-এ রূপান্তরিত হয়; Map-এ key যেমন আছে তেমনই থাকে। তাই key যদি সংখ্যা, object, function বা বাইরে থেকে আসা যেকোনো মান হয় — Map বেছে নিন।

প্রথম ২০ বছর JavaScript-এ আলাদা কোনো map বা set ছিল না। ES2015 (ES6) একসাথে চারটি সংগ্রহ যোগ করে — Map, Set, WeakMap আর WeakSet. পরে ES2021-এ এল WeakRef আর FinalizationRegistry, ES2024-এ Map.groupBy, আর ES2025-এ Set-এর গণিতের method-গুলো। এর আগের “object-কে hash map বানানো”র যুগ নিয়ে ভাবনার প্রশ্ন ১-এ বিস্তারিত আছে।

২ · Map — যেকোনো মান key, ক্রম বজায়, আকার তৈরি

Map হলো key থেকে value-এর একটি সংগ্রহ। প্রতিটি key-value জোড়াকে বলে একটি entry. Map তৈরি করার দুই উপায়: খালি new Map(), অথবা শুরুর entry-গুলো [key, value] জোড়ার array হিসেবে দিয়ে।

const stock = new Map([
  ["শাড়ি", 12],
  ["পাঞ্জাবি", 5],
]);

stock.set("লুঙ্গি", 30).set("গামছা", 50);  // set নিজেই Map ফেরত দেয়
stock.get("শাড়ি");      // 12
stock.get("জুতা");       // undefined — key নেই
stock.has("পাঞ্জাবি");   // true
stock.delete("গামছা");   // true (মুছল); না থাকলে false
stock.size;              // 3 — property, method নয়
stock.clear();           // সব খালি, size 0

ছয়টি কাজই মনে রাখার মতো সহজ: set (রাখা বা বদলানো), get (পড়া), has (আছে কি না), delete (মোছা), clear (সব মোছা) আর size (কতগুলো)। set Map-টিকেই ফেরত দেয়, তাই পরপর .set().set() লেখা যায় — একে বলে chaining. খেয়াল করুন, size একটি property — size() লিখলে TypeError. আর একটি সূক্ষ্ম কথা: key আছে কিন্তু মান undefined — এমন হলে get দেখে বোঝা যায় না, তাই “আছে কি না” জানতে সবসময় has.

ঘোরার ক্রম = ঢোকানোর ক্রম

Map একটি iterable, আর এর ঘোরার ক্রম নির্দিষ্ট: insertion order — যে key আগে ঢুকেছে, সে আগে আসবে। পুরোনো key-এর মান set দিয়ে বদলালে তার জায়গা বদলায় না; মুছে আবার যোগ করলে সে যায় শেষে। ঘোরার চারটি উপায়:

for (const [item, qty] of stock) { }        // প্রতিটি entry [key, value]
for (const item of stock.keys()) { }         // শুধু key
for (const qty of stock.values()) { }        // শুধু value
stock.forEach((qty, item) => { });           // সাবধান: আগে value, পরে key

const total = [...stock.values()].reduce((s, q) => s + q, 0);
const lowStock = [...stock].filter(([, q]) => q < 10);

for…of সরাসরি Map ঘুরলে প্রতিবার একটি দুই-ঘরের array পায়, আর পর্ব ১৭-এর destructuring দিয়ে আমরা সেটিকে [item, qty] নামে ভেঙে নিই। keys(), values(), entries() array নয়, iterator ফেরত দেয় — তাই map/filter/reduce (পর্ব ১৬) লাগাতে আগে spread দিয়ে array বানাতে হয়। forEach-এর callback-এ ক্রম (value, key) — array-এর (item, index)-এর সাথে মিলিয়ে এভাবে রাখা হয়েছে, কিন্তু অনেকেই উল্টো ধরে ভুল করেন।

Key মেলানোর নিয়ম — SameValueZero

Map দুটি key-কে “একই” ধরে কীভাবে? === দিয়ে নয়, বরং প্রায় একই রকম একটি নিয়মে, যার নাম SameValueZero. পার্থক্য মাত্র একটি জায়গায়:

  • NaN নিজের সমান। পর্ব ১০-এ দেখেছেন NaN === NaN মিথ্যা। === দিয়ে মেলালে NaN key দিয়ে রাখা মান আর কোনোদিন খুঁজে পাওয়া যেত না। SameValueZero-তে NaN আর NaN একই key.
  • +0 আর -0 একই। Map-এ -0 key রাখলে সেটি 0 হিসেবে জমা হয়।
  • বাকি সব ===-এর মতোই। Type বদলায় না (1 ≠ "1"), আর object মেলে শুধু একই reference হলে — দেখতে হুবহু এক রকম দুটি object আলাদা key.

শেষ কথাটি খুব গুরুত্বপূর্ণ: map.set({ id: 7 }, "x") লিখে পরে map.get({ id: 7 }) চাইলে undefined আসবে — কারণ দ্বিতীয় { id: 7 } একটি নতুন object. Object key-তে কাজ করতে চাইলে একই reference রেখে দিতে হয়। আর key যদি আসলে একটি id হয়, তবে object নয়, id-টিকেই key বানান।

এবার সব মিলিয়ে একটি বাস্তব কাজ — পণ্যের id ধরে কতবার দেখা হলো তা গোনা — আর সাথে object-এর ফাঁদগুলো নিজের চোখে দেখুন:

// পণ্যের id ধরে কতবার দেখা হলো
const views = new Map();
for (const id of [101, 205, 101, 307, 101, 205]) {
  views.set(id, (views.get(id) ?? 0) + 1);
}
console.log(views);
console.log(views.get(101), views.has("101"), views.size);

for (const [id, count] of views) console.log(`পণ্য ${id}: ${count} বার`);

const user = { name: "Rafi" };
const obj = {};
obj[user] = "লগইন";
const m = new Map([[user, "লগইন"], [NaN, "সংখ্যা নয়"]]);
console.log(Object.keys(obj), m.get(user), m.get(NaN));

console.log(JSON.stringify(views));
console.log(JSON.stringify(Object.fromEntries(views)));
console.log(typeof {}["constructor"], new Map().get("constructor"));

প্রথম লাইন Map(3) { 101 => 3, 205 => 2, 307 => 1 } — views.get(id) ?? 0 নতুন id-এর জন্য ০ ধরে নেয়, তারপর ১ যোগ। গোনার এই ধাঁচকে বলে frequency count. দ্বিতীয় লাইন 3 false 3: key সংখ্যা 101, তাই string "101" দিয়ে খুঁজলে পাওয়া যায় না। তারপর তিনটি লাইন ঢোকানোর ক্রমে — ১০১, ২০৫, ৩০৭. এরপর [ '[object Object]' ] লগইন সংখ্যা নয়: object-এ user হারিয়ে লেখা হয়ে গেছে, Map-এ object আর NaN দুটোই ঠিকঠাক key. শেষ তিনটি লাইন পরের অংশের বিষয়: JSON.stringify Map-কে দেয় {} — সব ডেটা চুপচাপ উধাও! Object.fromEntries দিয়ে আগে object বানালে ঠিক আসে। আর খালি object-ও "constructor" key-তে একটি function দেয়, Map দেয় undefined.

৩ · Map বনাম object — কখন কোনটি

পর্ব ১১-এ lookup table বানানোর সময় একটি ফাঁদ দেখেছিলেন: ব্যবহারকারী "constructor" লিখলে সাধারণ object বার্তার বদলে একটি function দিয়েছিল। কারণ প্রতিটি সাধারণ object তার prototype থেকে constructor, toString, hasOwnProperty-এর মতো property উত্তরাধিকারসূত্রে পায় (বিস্তারিত পর্ব ২৫-এ)। Map-এ এই সমস্যা নেই — এতে শুধু আপনার রাখা key থাকে। পুরো তুলনা একটি টেবিলে:

বিষয়সাধারণ object {}Map
Key-এর typeশুধু string আর Symbol; বাকি সব string হয়ে যায়যেকোনো মান — number, object, function, NaN
ক্রমপূর্ণসংখ্যার মতো key আগে, ছোট থেকে বড়; তারপর বাকি string ঢোকানোর ক্রমেসবসময় ঢোকানোর ক্রম
কতগুলো আছেObject.keys(o).length — প্রতিবার নতুন array বানায়map.size — সাথে সাথে
উত্তরাধিকারের keyconstructor, toString ইত্যাদি “আছে” মনে হয়; __proto__ key বিপজ্জনকনেই — শুধু আপনার রাখা key
ঘোরাসরাসরি for…of চলে না; Object.entries লাগেসরাসরি for…of
ঘনঘন যোগ-মোছাengine সাধারণত স্থির আকারের রেকর্ডের জন্য optimize করে; অনেক delete-এ ধীর হতে পারেএই কাজের জন্যই বানানো
JSONসরাসরি JSON.stringifyচুপচাপ {} হয়ে যায় — আগে রূপান্তর লাগে
লেখার সুবিধা{ a: 1 }, o.a, destructuringmap.get("a") — একটু লম্বা

সিদ্ধান্তের নিয়ম সহজ: key-গুলো যদি আপনি কোড লেখার সময় জানেন (রেকর্ড), object. যদি key চলার সময় আসে — বিশেষ করে ব্যবহারকারী বা বাইরের ডেটা থেকে — Map. পণ্যের রেকর্ড object; পণ্য-id থেকে view সংখ্যা Map. একটি ব্যবহারকারীর profile object; username থেকে profile Map.

__proto__ key-এর ফাঁদ: সাধারণ object-এ obj["__proto__"] = { isAdmin: true } লিখলে কোনো key তৈরি হয় না — বরং object-টির prototype বদলে যায়। তখন obj.isAdmin হয় true, অথচ Object.keys(obj) খালি। কেউ যদি ফর্মে বা URL-এ ইচ্ছা করে __proto__ নাম পাঠায়, আর আপনার কোড সেই নাম দিয়ে object-এ মান বসায় বা দুটি object “deep merge” করে, তবে এ ধরনের ত্রুটিকে বলে prototype pollution. প্রতিরক্ষা: বাইরের key-এর জন্য Map ব্যবহার করুন; object লাগলে Object.create(null) দিয়ে prototype ছাড়া object বানান, আর key আছে কি না দেখতে Object.hasOwn(obj, key). নিরাপত্তার পুরো আলোচনা পর্ব ৪৩-এ।

Map আর JSON — রূপান্তর লাগে

পর্ব ১৯-এ JSON বিস্তারিত দেখবেন। এখানে শুধু এটুকু জানুন: JSON-এ শুধু object, array, string, number, boolean আর null আছে — Map বলে কিছু নেই। JSON.stringify Map-এর নিজস্ব কোনো property পায় না, তাই লেখে {}. কোনো error নেই, সার্ভারে খালি ডেটা চলে যায়। দুই দিকের রূপান্তর:

// Map → JSON (key string হলে)
JSON.stringify(Object.fromEntries(views));   // '{"101":3,"205":2,"307":1}'
// Map → JSON (key যেকোনো type, ক্রমও থাকে)
JSON.stringify([...views]);                  // '[[101,3],[205,2],[307,1]]'

// JSON → Map
new Map(Object.entries(JSON.parse(text)));   // key হবে string!
new Map(JSON.parse(arrayText));              // [[k, v], …] থেকে

Object.fromEntries (ES2019) যেকোনো [key, value] জোড়ার iterable থেকে object বানায়; Object.entries ঠিক উল্টো। কিন্তু object-এ গেলে key string হয়ে যায় — ফেরত আনার পর views.get(101) আর পাবেন না, views.get("101") লাগবে। key-এর type রক্ষা করতে চাইলে জোড়ার array ([...views] বা Array.from(views)) হিসেবে পাঠান।

C/C++/Python/Java জানলে

Python-এর dict আর JavaScript-এর Map প্রায় একই চিন্তা — দুটোই ঢোকানোর ক্রম রাখে; তবে Python-এ list-এর মতো বদলযোগ্য মান key হতে পারে না, আর JavaScript Map object-কে reference দিয়ে মেলায়, মান দিয়ে নয় (Python-এর dict, Python-এর set). Java-তে HashMap-এর ক্রম নিশ্চিত নয়, JavaScript Map আচরণে LinkedHashMap-এর মতো; আর Java equals/hashCode নিজে লিখে key-এর সমতা ঠিক করতে দেয়, JavaScript দেয় না (Java-র Set ও Map). C++-এর std::map key সাজিয়ে রাখে, JavaScript-এ সাজানো Map নেই।

৪ · Set — শুধু অনন্য মানের সংগ্রহ

অনেক সময় key-value জোড়া লাগে না, শুধু জানতে হয় “এটি আছে কি না”। আজ কতজন আলাদা মানুষ সাইটে এসেছেন? একজন দশবার এলেও তিনি একজনই। এই কাজের জন্য Set — এমন একটি সংগ্রহ, যেখানে প্রতিটি মান একবারই থাকে। একই মান আবার add করলে কিছুই ঘটে না।

const visitors = new Set();
visitors.add("rafi").add("mim").add("rafi");  // দ্বিতীয় rafi উপেক্ষিত
visitors.size;            // 2
visitors.has("mim");      // true — খোঁজা খুব দ্রুত
visitors.delete("mim");   // true
for (const v of visitors) { }   // ঢোকানোর ক্রমে

API প্রায় Map-এর মতো, শুধু set/get-এর বদলে add, আর কোনো value নেই। “অনন্য” মাপা হয় সেই একই SameValueZero দিয়ে: NaN একবারই থাকে, 1 আর "1" দুটি আলাদা মান, আর দুটি আলাদা object — দেখতে একই হলেও — দুটি আলাদা সদস্য। has-এর গতি বড় সুবিধা: array-এর includes প্রতিটি উপাদান একে একে দেখে, Set সাধারণত hash table ব্যবহার করে প্রায় এক ধাপেই উত্তর দেয়।

Duplicate সরানো — প্রথম দেখার ক্রম ঠিক রেখে

Set-এর সবচেয়ে জনপ্রিয় ব্যবহার একটি লাইনে: [...new Set(arr)]. কিন্তু ফলের ক্রম কী হবে — সাজানো, নাকি অন্য কিছু? ধাপে ধাপে দেখুন:

1const arr = [3, 1, 3, 2, 1];2const seen = new Set();3for (const x of arr) seen.add(x);4console.log([...seen]);
প্রতিটি ধাপে Set
x
আগে থেকে আছে?
কী ঘটল
seen
  1. শুরু। পাঁচটি সংখ্যার array, যার মধ্যে ৩ আর ১ দুবার করে আছে। একটি খালি Set তৈরি। (new Set(arr) লিখলে ভেতরে ঠিক এই লুপটিই চলে।)
  2. x = 3. Set-এ ৩ নেই, তাই যোগ হলো। এটি প্রথম সদস্য, তাই ক্রমেও প্রথম।
  3. x = 1. ১ নেই, যোগ হলো — ৩-এর পরে।
  4. x = 3 আবার। SameValueZero অনুযায়ী ৩ আগেই আছে। add কিছুই করে না — কোনো error নেই, ক্রমও বদলায় না। ৩ তার প্রথম জায়গাতেই থাকে।
  5. x = 2. নতুন মান, যোগ হলো শেষে।
  6. x = 1 আবার। আগেই আছে, উপেক্ষিত।
  7. Array-তে ফেরা। Spread Set-কে ঢোকানোর ক্রমে ঘোরে: [ 3, 1, 2 ]. ফল সাজানো নয় — প্রথম দেখার ক্রমে। সাজানো চাইলে পরে আলাদা করে toSorted (পর্ব ১৬)।

দুটি বাস্তব ব্যবহার — unique visitor আর “দেখা হয়ে গেছে” চিহ্ন

Unique visitor: সার্ভারের log-এ প্রতিটি পাতা দেখার জন্য একটি করে লাইন থাকে। new Set(log.map((e) => e.userId)).size এক লাইনে বলে দেয় কতজন আলাদা মানুষ এসেছেন।

Visited tracking: ধরুন ঢাকার মেট্রো স্টেশনগুলোর একটি মানচিত্র, যেখানে প্রতিটি স্টেশন থেকে পাশের স্টেশনে যাওয়া যায়। একটি স্টেশন থেকে শুরু করে কোন কোন স্টেশনে পৌঁছানো যায়, তা খুঁজতে হলে প্রতিটি স্টেশন একবারই দেখতে হবে — নইলে দুই স্টেশনের মধ্যে চিরকাল ঘুরতে থাকবেন। এখানে Set হলো “ইতিমধ্যে দেখা” খাতা:

function reachable(graph, start) {
  const visited = new Set([start]);
  const queue = [start];
  while (queue.length > 0) {
    const station = queue.shift();
    for (const next of graph.get(station) ?? []) {
      if (!visited.has(next)) { visited.add(next); queue.push(next); }
    }
  }
  return visited;   // কাছের স্টেশন আগে, দূরেরটি পরে
}

graph একটি Map — প্রতিটি স্টেশনের নাম থেকে তার পাশের স্টেশনগুলোর array. প্রতিটি নতুন স্টেশন visited-এ ওঠে, আর queue-তে যায় পরে দেখার জন্য। একটি স্টেশন দ্বিতীয়বার এলে has সেটি আটকায়। এই কৌশলের নাম breadth-first search (BFS) — শুরুর কাছের জায়গাগুলো আগে, দূরেরগুলো পরে দেখা হয়। এটি এখানে শুধু একটি ঝলক; বড় তালিকায় shift ধীর হতে পারে, আর ইন্টারভিউর সমস্যায় এর পুরো ব্যবহার পর্ব ৫৯-এ।

৫ · Set-এর গণিত — union, intersection আর difference

স্কুলে Venn diagram-এ যে সেট পড়েছিলেন, Set ঠিক সেটিই। একটি অনলাইন পোশাকের দোকানে প্রতিটি পণ্যের কয়েকটি ট্যাগ থাকে। দুটি পণ্যের মধ্যে কোন ট্যাগ মিলে যায় (“এর মতো আরও পণ্য” দেখানোর জন্য), কোনগুলো শুধু একটিতে আছে — এসব প্রশ্নই সেটের গণিত। ES2025 থেকে Set-এ সরাসরি এই method-গুলো আছে। প্রতিটি একটি নতুন Set ফেরত দেয়, মূল দুটি অক্ষত থাকে:

1const a = new Set(['cotton', 'summer', 'men']);2const b = new Set(['summer', 'women', 'cotton']);3a.union(b);4a.intersection(b);5a.difference(b);6a.symmetricDifference(b);
Venn — কোন অংশ নেওয়া হলো
শুধু a-তে
দুটিতেই
শুধু b-তে
ফল
  1. তিনটি অঞ্চল। দুটি Set-কে দুটি বৃত্ত ভাবুন। men শুধু a-তে, women শুধু b-তে, আর cotton, summer মাঝের ছেদ অংশে। প্রতিটি অপারেশন শুধু ঠিক করে কোন অঞ্চলগুলো নেবে।
  2. union — সব অঞ্চল। যেকোনো একটিতে থাকলেই ফলে আছে। দুটি পণ্যের সব ট্যাগ মিলিয়ে একটি ফিল্টার তালিকা বানাতে কাজে লাগে। ফলের ক্রমে আগে a-এর সদস্য, তারপর b-এর নতুনগুলো।
  3. intersection — শুধু মাঝখান। দুটিতেই আছে এমন ট্যাগ — “এর মতো আরও পণ্য” খোঁজার ভিত্তি। মিল যত বেশি, পণ্য দুটি তত কাছাকাছি।
  4. difference — a থেকে b বাদ। a-তে আছে, b-তে নেই। খেয়াল করুন, এটি প্রতিসম নয়: b.difference(a) দিত { women }.
  5. symmetricDifference — মাঝখান বাদে সব। ঠিক একটিতে আছে, দুটিতে নয়। দুটি সংস্করণের ট্যাগ তালিকায় কী কী বদলেছে দেখাতে কাজে লাগে।

এর সাথে তিনটি প্রশ্ন-method আছে, যা true/false দেয়: a.isSubsetOf(b) (a-এর সব সদস্য কি b-তে আছে?), a.isSupersetOf(b) (উল্টোটা), আর a.isDisjointFrom(b) (কোনো মিল নেই তো?)।

কোথায় চলে, কোথায় চলে না: এই সাতটি method ES2025-এর অংশ। Chrome ও Edge 122 থেকে, Firefox 127 থেকে আর Safari 17 থেকে আছে; Node.js-এ এসেছে Node 22-এ। Node 20-এ নেই — সেখানে a.union লিখলে TypeError: a.union is not a function. পুরোনো পরিবেশের জন্য spread আর filter দিয়ে নিজে লিখুন:

const union        = new Set([...a, ...b]);
const intersection = new Set([...a].filter((x) => b.has(x)));
const difference   = new Set([...a].filter((x) => !b.has(x)));
const isSubset     = [...a].every((x) => b.has(x));

প্রতিটি লাইন Venn-এর একটি অঞ্চলকে কোডে লেখে। filter-এর ভেতরে b.has ব্যবহার করেছি, b-কে array বানিয়ে includes নয় — Set-এর দ্রুত খোঁজাটাই এখানে আসল সুবিধা। Built-in method-গুলো একই কাজ করে, তবে নামেই উদ্দেশ্য বোঝা যায়, আর ছোট Set-টিকে ঘুরে কাজ কমায়।

৬ · WeakMap আর WeakSet — key-কে বাঁচিয়ে না রাখা

এবার এই পর্বের সবচেয়ে গভীর অংশ। পর্ব ০৫-এ দেখেছেন, garbage collection সেই object-গুলো মুছে দেয়, যেগুলোতে প্রোগ্রাম থেকে আর পৌঁছানো যায় না। “পৌঁছানো যায়” মানে কোনো GC root (global variable, চলমান function-এর local variable) থেকে reference-এর শিকল ধরে object-টিতে যাওয়া যায়।

সমস্যা হলো, Map নিজেই একটি শক্ত reference. একটি object-কে Map-এর key বানালে Map সেটিকে ধরে রাখে — বাকি প্রোগ্রাম object-টিকে ভুলে গেলেও। ধরুন অর্ডারের ডেলিভারি চার্জ হিসাব একটু ভারী, তাই আপনি ফল একটি cache-এ রাখছেন: cache.set(order, charge). দিনে কয়েক হাজার অর্ডার আসে, প্রতিটি কয়েক মিনিট পরে স্ক্রিন থেকে চলে যায় — কিন্তু Map-এ তাদের entry থেকেই যায়। Map প্রতিদিন বড় হয়, ট্যাব বা সার্ভার ধীরে ধীরে মেমরি খেয়ে ফেলে। এটি একটি ক্লাসিক memory leak.

WeakMap ঠিক এই সমস্যার সমাধান। এর key-গুলোকে GC “গোনে না”: key object-এর আর কোনো শক্ত reference না থাকলে object-টি মুছে ফেলার যোগ্য হয়, আর তার সাথে WeakMap-এর entry-টিও চলে যায়। পাশাপাশি দেখুন:

1const mapCache = new Map();2const weakCache = new WeakMap();3let a = { id: 1 }; let b = { id: 2 };4mapCache.set(a, 60); weakCache.set(b, 120);5a = null; b = null;   // শেষ variable-reference বাদ6// … পরে কোনো এক সময় GC চলে7// এভাবে হাজার হাজার অর্ডার
কে কাকে ধরে রেখেছে
a / b
{ id: 1 }
{ id: 2 }
mapCache
weakCache
  1. দুটি অর্ডার। a আর b দুটি variable দুটি object দেখায়। এখন দুটিই GC root থেকে পৌঁছানো যায় — নিরাপদ।
  2. Cache-এ রাখা। প্রথমটি গেল সাধারণ Map-এ, দ্বিতীয়টি WeakMap-এ। Map-এর key একটি শক্ত reference — এখন { id: 1 }-এ পৌঁছানোর দুটি পথ। WeakMap-এর key দুর্বল — { id: 2 }-এ পৌঁছানোর পথ এখনো একটিই, b.
  3. Variable মুছে ফেলা। অর্ডার দুটি স্ক্রিন থেকে চলে গেছে, প্রোগ্রাম আর এদের চায় না। কিন্তু { id: 1 }-এ এখনো Map থেকে পৌঁছানো যায়, তাই সে জীবিত। { id: 2 }-এ পৌঁছানোর কোনো শক্ত পথ নেই — সে মুছে ফেলার যোগ্য।
  4. GC চলল — কিন্তু কখন, কেউ জানে না। Engine নিজের সুবিধামতো সময়ে GC চালায় — কয়েক মিলিসেকেন্ড পরে, কয়েক মিনিট পরে, বা মেমরির চাপ না থাকলে হয়তো কখনোই নয়। চললে { id: 2 } আর তার WeakMap entry একসাথে উধাও হয়। এটি ধারণাগত ছবি — কোড দিয়ে আপনি এটি “দেখতে” পারবেন না, আর তার ওপর নির্ভর করাও যাবে না।
  5. দীর্ঘমেয়াদি ফল। হাজার অর্ডারের পর Map-এ হাজার entry, যাদের কেউ আর ব্যবহার করবে না — leak. WeakMap নিজে থেকে পরিষ্কার থাকে। দাম হিসেবে WeakMap ঘোরা যায় না, size-ও নেই — কারণ কোন entry এই মুহূর্তে আছে, তা GC-র ওপর নির্ভর করে।

WeakMap-এর নিয়ম — কেন এত সীমাবদ্ধ

  • Key অবশ্যই object (ES2023 থেকে registry-তে না রাখা Symbol-ও চলে)। "order-7" বা 7-এর মতো primitive key দিলে TypeError — একটি string-এর “শেষ reference” বলে কিছু নেই, তাই দুর্বলভাবে ধরে রাখার মানেও নেই।
  • শুধু চারটি method: set, get, has, delete. কোনো size, clear, keys() বা for…of নেই।
  • কেন ঘোরা যায় না? ঘোরা গেলে ফল নির্ভর করত GC কখন চলেছে তার ওপর — একই কোড একেক বার একেক ফল দিত, আর পাশাপাশি GC-র ভেতরের সময়-তথ্য ফাঁস হতো। তাই ভাষা ইচ্ছা করেই এগুলো দেয়নি।

তিনটি বাস্তব ব্যবহার

🧮 Object-ভিত্তিক cache

প্রতিটি অর্ডার object-এর জন্য হিসাব করা ডেলিভারি চার্জ, বা প্রতিটি DOM element-এর মাপ। Object চলে গেলে cache-এর entry-ও যায়।

🔒 Object-এর গোপন তথ্য

একটি লাইব্রেরি বাইরের object-এ property না বসিয়েও তার সাথে নিজের তথ্য জুড়ে রাখতে পারে। #private field (পর্ব ২৬) আসার আগে এটাই ছিল প্রধান কৌশল — পুরোনো ব্রাউজারের জন্য Babel আজও #private-কে WeakMap-এ রূপান্তর করে।

✅ WeakSet — “হয়ে গেছে” চিহ্ন

WeakSet শুধু object রাখে: add, has, delete. কোন DOM element-এ ইতিমধ্যে event listener বসানো হয়েছে, কোন অর্ডার একবার প্রসেস হয়েছে — এমন চিহ্ন রাখার জন্য।

const initialized = new WeakSet();

function setupWidget(el) {
  if (initialized.has(el)) return;   // দ্বিতীয়বার listener বসবে না
  el.addEventListener("click", onClick);
  initialized.add(el);
}
// el পাতা থেকে সরিয়ে দিলে, আর কেউ না ধরলে, WeakSet-এর চিহ্নও চলে যায়

এবার Set আর WeakMap — দুটোই নিজে চালিয়ে দেখুন। শেষ দুটি লাইনে খেয়াল করুন: দেখতে একই রকম নতুন object cache-এ মেলে না, আর string key WeakMap-এ চলে না।

const visitors = ["rafi", "mim", "rafi", "tanim", "mim"];
const unique = new Set(visitors);
console.log(unique.size, [...unique]);
console.log([...new Set([3, 1, 3, 2, 1])]);

const a = new Set(["cotton", "summer", "men"]);
const b = new Set(["summer", "women", "cotton"]);
console.log([...a.intersection(b)], [...a.difference(b)]);
console.log([...a].filter((t) => b.has(t)));   // পুরোনো উপায়

const chargeCache = new WeakMap();
function deliveryCharge(order) {
  if (chargeCache.has(order)) return `${chargeCache.get(order)} (cache থেকে)`;
  const charge = order.city === "Dhaka" ? 60 : 120;
  chargeCache.set(order, charge);
  return charge;
}
const order = { id: 7, city: "Dhaka" };
console.log(deliveryCharge(order), deliveryCharge(order));
console.log(deliveryCharge({ id: 7, city: "Dhaka" }));
try { chargeCache.set("order-7", 60); }
catch (e) { console.log(`${e.name}: ${e.message}`); }

প্রথম দুটি লাইন 3 [ 'rafi', 'mim', 'tanim' ] আর [ 3, 1, 2 ] — duplicate বাদ, প্রথম দেখার ক্রম ঠিক। তৃতীয় লাইন [ 'cotton', 'summer' ] [ 'men' ] ES2025-এর method দিয়ে, চতুর্থ লাইনে একই intersection পুরোনো উপায়ে। এরপর 60 60 (cache থেকে): প্রথম কলে হিসাব হলো, দ্বিতীয় কলে একই object, তাই cache থেকে। পরের লাইন শুধু 60 — নতুন { id: 7, … } দেখতে একই হলেও আলাদা reference, তাই cache-এ মেলেনি, আবার হিসাব হলো। শেষ লাইন ব্রাউজারে (Edge/Chrome) TypeError: Invalid value used as weak map key — string-এর মতো primitive WeakMap-এর key হতে পারে না। GC-র ফল কোথাও ছাপা হয়নি, কারণ সেটি অনিশ্চিত।

৭ · WeakRef আর FinalizationRegistry — উন্নত, আর সাবধানে

WeakMap-এ object নিজেই key. কিন্তু কখনো কখনো আপনি একটি object-কে সরাসরি দুর্বলভাবে ধরে রাখতে চান — “থাকলে ব্যবহার করব, না থাকলে আবার বানাব।” ES2021 এজন্য দুটি টুল যোগ করে।

// বড় একটি ছবির ডেটা — রাখতে পারলে ভালো, না পারলেও চলবে
let ref = new WeakRef(loadBigImage("banner.png"));

function getBanner() {
  const img = ref.deref();          // object থাকলে সেটি, মুছে গেলে undefined
  if (img) return img;
  const fresh = loadBigImage("banner.png");
  ref = new WeakRef(fresh);
  return fresh;
}

const registry = new FinalizationRegistry((label) => {
  console.log(label, "মুছে গেছে");  // কখনো চলতেও পারে, না-ও পারে
});
registry.register(someObject, "report-42");

WeakRef একটি object-কে দুর্বলভাবে ধরে। deref() দেয় object-টি — যদি তখনো GC সেটি না মুছে থাকে; নইলে undefined. তাই প্রতিবার deref()-এর ফল যাচাই করতে হয়, আর হাতে পাওয়ার পর একটি সাধারণ variable-এ রেখে ব্যবহার করতে হয়। FinalizationRegistry আপনাকে একটি callback দিতে দেয়, যা কোনো নিবন্ধিত object মুছে যাওয়ার পরে কোনো এক সময় ডাকা হতে পারে। এই কোডটি ইচ্ছা করেই চালানোর বোতাম ছাড়া রাখা হয়েছে — এর আউটপুট নির্ভর করে GC কখন চলে তার ওপর।

এই দুটি টুলের নিয়ম: (১) GC কখন চলবে, আদৌ চলবে কি না — মানদণ্ড কোনো নিশ্চয়তা দেয় না; ভিন্ন ব্রাউজার, এমনকি একই ব্রাউজারের ভিন্ন সংস্করণ ভিন্ন আচরণ করে। (২) FinalizationRegistry-র callback প্রোগ্রাম বন্ধ হওয়ার আগে না-ও চলতে পারে — তাই ফাইল সেভ করা, লেনদেন শেষ করা বা সংযোগ বন্ধ করার মতো জরুরি কাজ কখনো এতে রাখবেন না; সেগুলো নিজে, স্পষ্টভাবে করুন। (৩) প্রস্তাবটির লেখকরাই সতর্ক করেছিলেন: সঠিকভাবে ব্যবহার করতে গভীর চিন্তা লাগে, আর সম্ভব হলে এড়িয়ে চলাই ভালো। সাধারণ অ্যাপে আপনার দরকার প্রায় সবসময় WeakMap বা স্পষ্ট মোছা (map.delete).

টুলকী দুর্বলভাবে ধরেকবে এসেছেসাধারণ ব্যবহার
WeakMapkey (object)ES2015object-ভিত্তিক cache, গোপন তথ্য
WeakSetসদস্য (object)ES2015“প্রসেস হয়েছে” চিহ্ন
WeakRefএকটি object সরাসরিES2021বড়, আবার বানানো যায় এমন ডেটার cache
FinalizationRegistry— (মোছার পরে খবর)ES2021বাড়তি পরিষ্কারের সুযোগ, নিশ্চয়তা নয়

৮ · প্রতিদিনের ধাঁচ — গোনা, দলে ভাগ আর কোনটি কখন

Map.groupBy — দলে ভাগ করে Map-এ

অর্ডারগুলোকে শহর অনুযায়ী দলে ভাগ করতে চান। ES2024 থেকে এর জন্য দুটি built-in আছে: Object.groupBy ফল দেয় object-এ, আর Map.groupBy দেয় Map-এ। দলের key যখন string নয় (যেমন একটি object বা সংখ্যা), অথবা আপনি ক্রম আর size চান, তখন Map সংস্করণটি ভালো।

const orders = [
  { id: 1, city: "Dhaka", total: 900 },
  { id: 2, city: "Rajshahi", total: 450 },
  { id: 3, city: "Dhaka", total: 300 },
];
const byCity = Map.groupBy(orders, (o) => o.city);
// প্রত্যাশিত: Map(2) { 'Dhaka' => [ {…id 1}, {…id 3} ], 'Rajshahi' => [ {…id 2} ] }
byCity.get("Dhaka").length;   // 2

Callback প্রতিটি অর্ডারের জন্য একটি দলের key ফেরত দেয়; একই key-এর অর্ডারগুলো একটি array-তে জমা হয়। Map.groupBy Chrome ও Edge 117 থেকে আছে, কিন্তু Node.js-এ এসেছে Node 21-এ — Node 20-এ নেই। সেখানে reduce বা একটি লুপ দিয়ে নিজে লিখুন: if (!m.has(k)) m.set(k, []); m.get(k).push(o);.

কোনটি কখন — সিদ্ধান্তের টেবিল

আপনার প্রশ্নবেছে নিনউদাহরণ
নির্দিষ্ট নামের কয়েকটি বৈশিষ্ট্যobjectএকটি পণ্যের নাম, দাম, স্টক
চলার সময় আসা key থেকে মানMapপণ্য-id → view সংখ্যা, username → profile
শুধু “আছে কি না”, duplicate নয়Setআজকের unique visitor, দেখা স্টেশন
ক্রম আর index দরকার, duplicate চলবেarrayকার্টের পণ্য, ক্রম অনুযায়ী
Object-এর সাথে বাড়তি তথ্য, object চলে গেলে তথ্যও যাবেWeakMapঅর্ডার → হিসাব করা চার্জ
Object-এর গায়ে শুধু একটি চিহ্নWeakSetযে element-এ listener বসানো হয়েছে
সার্ভারে পাঠাতে বা সেভ করতে হবেobject/array (বা রূপান্তর)JSON API-র ডেটা

সীমাহীন cache সাবধান: সব cache-এর key object নয় — পণ্য-id বা URL-এর মতো string key-তে WeakMap চলে না। তখন সাধারণ Map ব্যবহার করুন, কিন্তু একটি সীমা দিন: যেমন সর্বোচ্চ ৫০০টি entry, ভরে গেলে সবচেয়ে পুরোনোটি মুছুন। Map-এর ঢোকানোর ক্রম এখানে কাজে লাগে — map.keys().next().value সবসময় সবচেয়ে পুরোনো key. একটি entry পড়ার সময় মুছে আবার set করলে সে শেষে চলে যায়; এভাবে খুব অল্প কোডে একটি LRU (least recently used) cache বানানো যায়। মেমরি মাপা আর leak খোঁজার পুরো কৌশল পর্ব ৫৩-এ।

পরীক্ষা ও ইন্টারভিউতে
  • Map আর সাধারণ object-এর পার্থক্য কী? — Map-এ যেকোনো type-এর key, ঢোকানোর ক্রম নিশ্চিত, size আছে, prototype থেকে বাড়তি key নেই; object-এ key string/Symbol, আর সরাসরি JSON হয়।
  • একটি array থেকে duplicate সরানোর সবচেয়ে ছোট উপায়? — [...new Set(arr)]; প্রথম দেখার ক্রম থাকে, তবে object-কে reference দিয়ে মেলায়।
  • WeakMap-এর key primitive হতে পারে না কেন, আর একে ঘোরা যায় না কেন? — Primitive-এর “শেষ reference” নেই বলে দুর্বলভাবে ধরা অর্থহীন; আর কোন entry আছে তা GC-র ওপর নির্ভর করে, তাই ঘোরা বা size ফল অনিশ্চিত করত।
  • JSON.stringify(new Map([[1, 2]])) কী দেয়? — "{}"; আগে Object.fromEntries বা [...map] দিয়ে রূপান্তর করতে হয়।
  • new Set([NaN, NaN, 0, -0]).size কত? — ২; SameValueZero-তে NaN নিজের সমান আর 0 ও -0 একই।

৯ · AI-এর যুগে — AI-এর লেখা সংগ্রহের কোড যাচাই করা

AI-কে “ব্যবহারকারীর পছন্দ সেভ করার কোড লিখে দাও” বললে প্রায়ই পাবেন const prefs = {}; prefs[req.body.key] = req.body.value; — দেখতে নিরীহ, কিন্তু key-টি ব্যবহারকারীর হাতে। এই পর্বের বিষয়ে AI-এর সবচেয়ে সাধারণ ভুলগুলো:

AI-এর ভুলকেন সমস্যাকী চাইবেন
ব্যবহারকারীর দেওয়া key দিয়ে সাধারণ object-কে dictionary বানানোconstructor, __proto__-এর মতো key-তে ভুল ফল বা prototype pollutionMap, অথবা Object.create(null) + Object.hasOwn
Map সরাসরি JSON.stringify বা localStorage-এ রাখাচুপচাপ {} — ডেটা হারায়, কোনো error নেইObject.fromEntries বা [...map] দিয়ে রূপান্তর, ফেরত আনার কোডসহ
Module-স্তরে const cache = new Map(), কোনো সীমা বা মোছা নেইসার্ভার যত দিন চলে, মেমরি তত বাড়ে — leakobject key হলে WeakMap; নইলে সর্বোচ্চ আকার বা মেয়াদ
Duplicate খুঁজতে arr.includes লুপের ভেতরেবড় তালিকায় খুব ধীরSet আর has
Object key দিয়ে Map-এ খোঁজা, প্রতিবার নতুন literal দিয়েmap.get({ id: 7 }) সবসময় undefinedid-কে key বানানো
Node 20-এ a.union(b) বা Map.groupByTypeError — method নেইলক্ষ্য runtime বলে দেওয়া, দরকার হলে হাতে লেখা বিকল্প
FinalizationRegistry-তে জরুরি পরিষ্কারের কাজcallback না-ও চলতে পারেস্পষ্ট close()/delete

AI-কে সংগ্রহ-নির্ভর কোড লেখাতে চাইলে আগে থেকেই শর্তগুলো বলে দিন:

একটি JavaScript module লিখে দাও: একটি অনলাইন দোকানে কোন পণ্য কতবার দেখা হলো তা গুনবে, আর প্রতিটি অর্ডার object-এর ডেলিভারি চার্জ cache করবে। নিয়ম: ১. পণ্যের id বাইরে থেকে আসে — গোনার জন্য Map ব্যবহার করবে, সাধারণ object নয়। কেন, এক লাইনে বলো। ২. ডেলিভারি চার্জের cache-এর key অর্ডার object — WeakMap ব্যবহার করবে, যাতে অর্ডার চলে গেলে cache-ও পরিষ্কার হয়। ৩. string key-এর কোনো cache থাকলে তার সর্বোচ্চ আকার ৫০০, ভরে গেলে সবচেয়ে পুরোনো entry মুছবে। ৪. Map-কে JSON-এ পাঠানো আর ফেরত আনার দুটি function দেবে, আর বলবে key-এর type বদলায় কি না। ৫. কোড Node 20-এ চলবে — Set-এর union/intersection বা Map.groupBy ব্যবহার করবে না। ৬. GC কখন চলবে তার ওপর নির্ভর করে এমন কোনো কোড বা টেস্ট দেবে না। ৭. শেষে ৫টি console.log টেস্ট দাও — একই id বারবার, "constructor" নামের id, একই অর্ডার দুবার, দেখতে একই নতুন অর্ডার, আর JSON রূপান্তর — প্রত্যেকটির প্রত্যাশিত ফলসহ।

AI-এর লেখা সংগ্রহের কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১প্রায় ২০ বছর JavaScript-এ আসল map বা set ছিল না — মানুষ object-কে hash map হিসেবে চালাত। সেই যুগে কী কী সমস্যা হতো, আর ES2015-এর Map সেগুলো কীভাবে সমাধান করল?

১৯৯৫ সালে JavaScript তৈরির সময় object-ই ছিল একমাত্র “নাম থেকে মান” কাঠামো, আর ভাষার নকশায় এটিকে খুব নমনীয় রাখা হয়েছিল — চলার সময় যেকোনো property যোগ করা যায়, মোছা যায়। তাই স্বাভাবিকভাবেই মানুষ object-কে dictionary বা hash map হিসেবে ব্যবহার শুরু করল: var counts = {}; counts[word] = (counts[word] || 0) + 1;. ছোট স্ক্রিপ্টে এটি চমৎকার কাজ করত।

কিন্তু সমস্যাগুলো ধীরে ধীরে সামনে এল। প্রথমত, উত্তরাধিকারের key: শব্দ গোনার প্রোগ্রামে যদি কোনো লেখায় “constructor” বা “toString” শব্দটি থাকত, counts["constructor"] শুরু থেকেই একটি function — যোগফল হয়ে যেত অদ্ভুত একটি string. এজন্য প্রতিটি লাইব্রেরিতে লেখা হতো Object.prototype.hasOwnProperty.call(obj, key) — লম্বা, ভুলে যাওয়া সহজ। দ্বিতীয়ত, __proto__: অনেক engine-এ এই নামটি বিশেষ ছিল, এটিতে মান বসালে prototype বদলে যেত। ব্যবহারকারীর দেওয়া key দিয়ে object ভরলে এটি নিরাপত্তার ঝুঁকি হয়ে দাঁড়ায় — পরে যার নাম হয় prototype pollution, আর lodash-এর মতো বহুল ব্যবহৃত লাইব্রেরির merge-ধাঁচের function-এও এমন ত্রুটি ধরা পড়ে সংশোধন করতে হয়েছিল।

তৃতীয়ত, key শুধু string: DOM node বা অন্য object-এর সাথে তথ্য জুড়তে মানুষ প্রতিটি object-এ একটি গোপন id property বসাত (jQuery-র মতো লাইব্রেরি এ ধরনের কৌশল ব্যবহার করত), তারপর সেই id দিয়ে object-এ তথ্য রাখত — আর object চলে গেলেও তথ্য থেকে যেত। চতুর্থত, আকার জানতে প্রতিবার সব key গুনতে হতো।

ES5 (২০০৯) Object.create(null) এনে prototype-হীন object বানানোর সুযোগ দেয়, যা কিছুটা সাহায্য করে। কিন্তু আসল সমাধান আসে ES2015 (ES6)-এ: Map, Set, WeakMap আর WeakSet একসাথে — যেকোনো type-এর key, নিশ্চিত ঢোকানোর ক্রম, size, কোনো উত্তরাধিকারের key নেই, আর weak সংস্করণে object-এর সাথে তথ্য জোড়ার নিরাপদ উপায়। পুরোনো object ভাঙা হয়নি, শুধু পাশে একটি সঠিক টুল রাখা হয়েছে।

মূল উপলব্ধি: একটি জিনিস দিয়ে দুটি আলাদা কাজ (রেকর্ড আর সংগ্রহ) চালালে সীমানায় বাগ জন্মায়। ভালো নকশা প্রতিটি কাজের জন্য আলাদা, উদ্দেশ্য-নির্দিষ্ট কাঠামো দেয় — আর বাইরের ডেটার key-কে কখনো ভাষার নিজস্ব নামের সাথে একই জায়গায় রাখে না।

প্র ০২Map আর Set key মেলাতে === ব্যবহার না করে SameValueZero নামের একটি আলাদা নিয়ম ব্যবহার করে। এমন সূক্ষ্ম পার্থক্যের দরকার পড়ল কেন?

JavaScript-এর মানদণ্ডে সমতা যাচাইয়ের চারটি আলাদা নিয়ম আছে। == (loose equality, type বদলে তুলনা), === (strict equality), Object.is-এর পেছনের SameValue, আর SameValueZero. শেষ তিনটির পার্থক্য শুধু দুটি বিশেষ মানে — NaN আর -0. ===-তে NaN !== NaN আর 0 === -0; SameValue-তে NaN নিজের সমান আর 0 ও -0 আলাদা; SameValueZero-তে NaN নিজের সমান আর 0 ও -0 একই।

NaN !== NaN এসেছে IEEE 754 floating-point মানদণ্ড থেকে, যেখানে “সংখ্যা নয়” মানকে কোনো কিছুর সমান ধরা হয় না — গণিতের হিসাবে এর যুক্তি আছে। কিন্তু একটি সংগ্রহের জন্য এটি বিপর্যয়: === দিয়ে key মেলালে map.set(NaN, "x") লেখার পর সেই মান কোনোদিন পড়া যেত না, আর Set-এ NaN যতবার যোগ করতেন ততবার নতুন সদস্য হতো। Array-এর indexOf ঠিক এই সমস্যায় ভোগে — এটি === ব্যবহার করে, তাই [NaN].indexOf(NaN) দেয় −১. ES2016-এর includes SameValueZero ব্যবহার করে, তাই [NaN].includes(NaN) হলো true.

তাহলে SameValue (Object.is) কেন নয়? তাতে 0 আর -0 আলাদা key হতো। -0 খুব সহজেই তৈরি হয় — -1 * 0, Math.round(-0.4) — আর console-এ দুটোই প্রায়ই 0 দেখায়। কেউ হিসাব করে একটি key বানালে সেটি -0 হয়ে গেলে map.get(0) ব্যর্থ হতো, আর কারণ খুঁজে পাওয়া প্রায় অসম্ভব হতো। ES2015-এর খসড়ার সময় এই আলোচনা হয়েছিল, আর শেষে সিদ্ধান্ত হয় দুই বিপদ থেকেই বাঁচানোর: NaN খুঁজে পাওয়া যাবে, আর -0 key ঢোকার সময় +0-তে রূপান্তরিত হবে।

একটি জিনিস SameValueZero করে না — object-কে “বিষয়বস্তু” দিয়ে মেলানো। দুটি { id: 7 } আলাদা। কারণ বিষয়বস্তু দিয়ে মেলাতে প্রতিটি object-এর hash হিসাব করতে হতো, আর object বদলে গেলে hash-ও বদলাত — Java-তে এটি বহু বাগের উৎস। এ নিয়ে record ও tuple-এর মতো নতুন মান-ধরনের প্রস্তাব নিয়ে TC39-এ দীর্ঘ আলোচনা হয়েছে।

মূল উপলব্ধি: “সমান” শব্দটি একাধিক অর্থ বহন করে, আর প্রতিটি প্রসঙ্গে সঠিক অর্থ বাছাই করাই ভাষা-নকশার কাজ। সংগ্রহের জন্য সঠিক নিয়ম সেটি, যাতে রাখা জিনিস আবার খুঁজে পাওয়া যায় — গণিতের বিশুদ্ধতা এখানে গৌণ।

প্র ০৩WeakMap বাস্তবায়ন করা শুনতে সহজ লাগে, কিন্তু GC গবেষণায় এর পেছনে “ephemeron” নামে একটি ধারণা আছে। এটি কী, আর WeakMap কেন একটি সাধারণ “দুর্বল key-এর টেবিল” নয়?

সবচেয়ে সরল চিন্তা হলো: একটি টেবিল, যার key দুর্বল আর value শক্ত। কিন্তু এতে একটি সূক্ষ্ম ফাঁদ আছে। ধরুন value নিজেই key-কে reference করে — যেমন cache.set(order, { order, charge: 60 }). value শক্তভাবে ধরা, value ধরে আছে key-কে, তাই key কখনো “পৌঁছানো যায় না” হবে না — টেবিল নিজেই নিজের key-কে বাঁচিয়ে রাখছে। অর্থাৎ সাধারণ weak-key টেবিলে এমন ক্ষেত্রে leak থেকেই যায়, আর বাস্তবে এমন ক্ষেত্র খুব সাধারণ।

১৯৯৭ সালে Barry Hayes তাঁর “Ephemerons: A New Finalization Mechanism” গবেষণাপত্রে (OOPSLA সম্মেলনে) এর সমাধান দেন। Ephemeron হলো একটি key-value জোড়া, যার নিয়ম: value শুধু তখনই জীবিত, যখন key অন্য কোনো পথে জীবিত। অর্থাৎ value থেকে key-তে যাওয়া reference key-কে বাঁচানোর পক্ষে গোনা হয় না। GC আগে key-গুলো অন্য পথে পৌঁছানো যায় কি না দেখে; যেগুলো যায়, শুধু তাদের value-কে জীবিত চিহ্ন দেয়; সেই value থেকে নতুন কোনো key পৌঁছানো গেলে আবার পরীক্ষা করে — যতক্ষণ না কিছু বদলায়। বাকি জোড়াগুলো একসাথে মুছে যায়।

JavaScript-এর WeakMap ঠিক এই ephemeron-এর নিয়ম মেনে চলে — মানদণ্ড এমনভাবে লেখা যে value থেকে key-তে চক্র থাকলেও entry মুছে যেতে পারে। এর দাম আছে: সাধারণ mark-and-sweep একবার ঘুরেই শেষ হয়, কিন্তু ephemeron-এর জন্য বারবার ফিরে দেখতে হতে পারে। V8-এর মতো engine-এর দল বছরের পর বছর এই অংশকে দ্রুত করতে কাজ করেছে, কারণ বড় অ্যাপে (আর Babel-এর রূপান্তর করা #private field-এ) হাজার হাজার WeakMap entry থাকে।

WeakMap-কে ঘোরা না যাওয়ার নিয়মও এই তত্ত্বের সাথে মেলে: কোন জোড়া এই মুহূর্তে জীবিত, তা GC-র কাজের অবস্থার ওপর নির্ভর করে। ঘোরা গেলে প্রোগ্রাম GC-র ভেতরের অবস্থা দেখে ফেলত — যা অনিশ্চিত, আর নিরাপত্তার দিক থেকেও অবাঞ্ছিত।

মূল উপলব্ধি: “দুর্বল reference” শুনতে সহজ, কিন্তু সঠিক নিয়ম খুঁজে পেতে গবেষণা লেগেছিল। একটি API কেন সীমাবদ্ধ (WeakMap-এ size নেই, ঘোরা নেই), তা প্রায়ই তার নিচের তত্ত্ব থেকেই বোঝা যায় — সীমাবদ্ধতাটি অবহেলা নয়, নকশা।

প্র ০৪WeakRef আর FinalizationRegistry ES2021-এ যুক্ত হলেও এদের প্রস্তাবেই লেখা ছিল “সম্ভব হলে এড়িয়ে চলুন”। একটি ভাষা এমন টুল কেন যোগ করে, যা ব্যবহার না করতেই বলা হয়?

JavaScript-এর একটি বড় নীতি হলো নির্ধারিত আচরণ — একই কোড, একই input, প্রতিটি ব্রাউজারে একই ফল। GC এই নীতির বাইরে: কখন চলবে, কোন object আগে মুছবে, তা engine, মেমরির চাপ, এমনকি ট্যাব পটভূমিতে আছে কি না, তার ওপর নির্ভর করে। ES2015-এর WeakMap সাবধানে এমনভাবে বানানো হয়েছিল যাতে GC-র আচরণ প্রোগ্রাম থেকে কখনো দেখা না যায়। WeakRef সেই দেয়ালে প্রথম জানালা খুলল — deref() একবার object দেয়, পরে undefined; আর FinalizationRegistry-র callback সরাসরি জানায় যে কিছু মুছে গেছে।

তাহলে যোগ করা হলো কেন? কারণ কিছু বাস্তব সমস্যা অন্যভাবে সমাধান করা যায় না। WebAssembly-র সাথে কাজ করা লাইব্রেরি WebAssembly মেমরিতে জায়গা নেয়; JavaScript-এর wrapper object মুছে গেলে সেই জায়গা ফেরত দেওয়ার একটি উপায় দরকার ছিল। বড়, আবার বানানো যায় এমন ডেটার cache (ছবি, হিসাব করা ফল) — যা মেমরির চাপ বাড়লে নিজে থেকে ছেড়ে দেওয়া যায় — আরেকটি উদাহরণ। এই প্রস্তাবটি নিয়ে TC39-এ কয়েক বছর আলোচনা চলে, আর ২০২০ সালে এটি চূড়ান্ত হয়ে ES2021-এ আসে।

সতর্কতাগুলো মানদণ্ড আর প্রস্তাবের লেখাতেই আছে। Callback চলবে এমন নিশ্চয়তা নেই — প্রোগ্রাম বন্ধ হওয়ার সময় সাধারণত চলে না। একই কাজের পরে দুই engine-এ ফল আলাদা হতে পারে। আর বিপজ্জনক ব্যাপার: প্রোগ্রামের সঠিকতা যদি GC-র সময়ের ওপর নির্ভর করে, তবে engine-এর একটি নিরীহ উন্নতিও আপনার অ্যাপ ভেঙে দিতে পারে। আরও একটি সূক্ষ্ম নিয়ম আছে: একবার deref() object দিলে, সেই একই কাজের ধাপ (synchronous job) শেষ হওয়া পর্যন্ত engine object-টি জীবিত রাখে — যাতে এক লাইনে পাওয়া object পরের লাইনে হঠাৎ উধাও না হয়।

এই কারণেই নির্দেশনা হলো: সাধারণ অ্যাপে প্রায় কখনো দরকার নেই। পরিষ্কারের কাজ স্পষ্টভাবে করুন (close(), delete, try…finally), আর FinalizationRegistry-কে বড়জোর একটি নিরাপত্তা-জাল হিসেবে রাখুন, যা স্পষ্ট পরিষ্কার ভুলে গেলে কিছু মেমরি বাঁচায়।

মূল উপলব্ধি: শক্তিশালী কিন্তু অনিশ্চিত টুলের জন্য সতর্কতা নকশারই অংশ। একটি API থাকা মানেই সেটি ব্যবহার করা উচিত নয় — প্রশ্ন করুন, “এর ফল কি নির্ধারিত?” না হলে, প্রোগ্রামের সঠিকতা এর ওপর রাখবেন না।

প্র ০৫Set ES2015-এ এলেও union, intersection-এর মতো মৌলিক method আসতে আরও প্রায় দশ বছর লাগল। এত সময় লাগল কেন?

২০১৫ সালের Set ছিল খুব ন্যূনতম: add, has, delete, clear, size আর ঘোরার method. Python-এর set-এ যেখানে a | b, a & b লেখা যায়, JavaScript-এ union-এর জন্য লিখতে হতো new Set([...a, ...b]), আর intersection-এর জন্য filter. ES2015-এর লক্ষ্য ছিল কাঠামোগুলো দ্রুত ভাষায় আনা; সুবিধার method পরে যোগ হবে — এমনটাই ভাবা হয়েছিল।

Set-এর method-এর প্রস্তাব TC39-এ ২০১৮ সালের দিকে আসে। তারপর কয়েক বছর ধরে এমন প্রশ্নে বিতর্ক চলে, যা বাইরে থেকে ছোট মনে হয়, কিন্তু ভাষার জন্য স্থায়ী। argument কী হতে পারে? শুধু Set, নাকি যেকোনো iterable (array)? শেষে সিদ্ধান্ত হয় “set-এর মতো” object — যার size, has আর keys আছে — তাই Map-ও argument হিসেবে চলে। Subclass হলে ফল কী type-এর? class TagSet extends Set থেকে union করলে ফল TagSet হবে কি না — অন্য built-in-এ এই ধরনের নিয়ম (Symbol.species) জটিলতা আর নিরাপত্তা-সমস্যা তৈরি করেছিল, তাই নতুন method সবসময় সাধারণ Set ফেরত দেয়। দক্ষতা আর ক্রম: intersection ছোট Set-টিকে ঘুরলে দ্রুত হয়, কিন্তু তাতে ফলের ক্রম নির্ভর করে কোনটি ছোট তার ওপর — নির্ধারিত আচরণ রাখতে মানদণ্ডে এ নিয়ম সুনির্দিষ্টভাবে লেখা হয়।

এসব নিষ্পত্তির পর প্রস্তাবটি ২০২৩ সালে stage 3-তে ওঠে, ব্রাউজারগুলো বাস্তবায়ন শুরু করে (Chrome 122, ২০২৪ সালের শুরুতে), আর ES2025-এ এটি মানদণ্ডে যুক্ত হয়। এই ধীর গতিকে অনেকে সমালোচনা করেন, কিন্তু কারণটি স্পষ্ট: ওয়েবে একবার একটি method যোগ হলে তা আর কখনো বদলানো যায় না — পুরোনো সাইট ভেঙে যাবে। ভুল নকশা চিরকালের।

মূল উপলব্ধি: “এত সহজ জিনিস আসতে এত দেরি কেন” — এর উত্তর প্রায়ই ছোট সিদ্ধান্তগুলোর স্থায়িত্বে। নতুন method ব্যবহারের আগে দেখুন আপনার লক্ষ্য পরিবেশে (যেমন Node 20) সেটি আছে কি না; না থাকলে কয়েক লাইনের হাতে লেখা বিকল্প সবসময় নিরাপদ।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।

  1. Size অনুমান করুন: নিচের প্রতিটির size কত? (ক) new Set([1, "1", 1.0]) (খ) new Set([{}, {}]) (গ) new Map([[NaN, 1], [NaN, 2]]) (ঘ) new Set("banana")

    (ক) ২ — 1 আর 1.0 একই number, "1" string বলে আলাদা। (খ) ২ — দুটি আলাদা object, দেখতে একই হলেও reference আলাদা। (গ) ১ — SameValueZero-তে NaN নিজের সমান, তাই দ্বিতীয় জোড়া প্রথমটির মান বদলে দেয়: Map(1) { NaN => 2 }. (ঘ) ৩ — string একটি iterable, তাই অক্ষরগুলো আলাদা সদস্য: b, a, n.

  2. Map না object? প্রতিটি ক্ষেত্রে কোনটি বেছে নেবেন, এক লাইনে কারণসহ: (ক) একটি অর্ডারের id, তারিখ, মোট দাম (খ) ব্যবহারকারীর লেখা সার্চ শব্দ থেকে কতবার খোঁজা হয়েছে (গ) প্রতিটি DOM বোতাম থেকে তার ক্লিক সংখ্যা, বোতাম সরালে তথ্যও যাবে (ঘ) API থেকে আসা ও API-তে ফেরত পাঠানো সেটিংস

    (ক) object — নির্দিষ্ট, আগে থেকে জানা key; এটি রেকর্ড। (খ) Map — key ব্যবহারকারীর হাতে, যেকোনো শব্দ (এমনকি “constructor”) আসতে পারে, আর সংখ্যা চলার সময় বাড়ে। (গ) WeakMap — key DOM element (object), আর element চলে গেলে entry-ও যাওয়া উচিত। (ঘ) object — সরাসরি JSON হয়; ভেতরে Map ব্যবহার করলে পাঠানোর আগে রূপান্তর লাগবে।

  3. বাগ খুঁজুন: একজন লিখেছেন localStorage.setItem("cart", JSON.stringify(cartMap)); — পাতা নতুন করে খুললে কার্ট সবসময় খালি। আরেকজন লিখেছেন const seen = new WeakSet(); seen.add(productId); — এখানে productId একটি সংখ্যা। কী ঘটবে, আর সমাধান কী?

    প্রথমটিতে JSON.stringify(cartMap) দেয় "{}" — Map-এর entry JSON-এ যায় না, কোনো error-ও হয় না। সমাধান: JSON.stringify([...cartMap]) দিয়ে রাখুন, আর পড়ার সময় new Map(JSON.parse(text)). দ্বিতীয়টিতে TypeError — WeakSet-এ শুধু object (বা registry-বাইরের Symbol) রাখা যায়; ব্রাউজারে মেসেজ Invalid value used in weak set. সংখ্যা রাখতে সাধারণ Set ব্যবহার করুন।

  4. ছোট কোড লিখুন: topTags(products) নামে একটি function লিখুন — প্রতিটি পণ্যের একটি tags array আছে; function-টি একটি Map ফেরত দেবে, যেখানে প্রতিটি ট্যাগ থেকে কতগুলো পণ্যে সেটি আছে তার সংখ্যা। একটি পণ্যে একই ট্যাগ দুবার থাকলে একবারই গুনবে। কোড Node 20-এও চলতে হবে।

    function topTags(products) { const counts = new Map(); for (const p of products) { for (const tag of new Set(p.tags)) { counts.set(tag, (counts.get(tag) ?? 0) + 1); } } return counts; }. ভেতরের new Set(p.tags) একই পণ্যের duplicate ট্যাগ সরায়; বাইরের Map frequency গোনে। কোনো ES2025 method নেই, তাই Node 20-এ চলবে। সবচেয়ে বেশি থেকে কম ক্রমে চাইলে: [...counts].toSorted((x, y) => y[1] - x[1]).

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি object WeakMap-এর key হওয়ার পর তার entry কোন ক্রমে মুছে যায়, সাজান (▲▼ চেপে বা টেনে)।

  1. 📦 Object তৈরি হয়, একটি variable সেটিকে দেখায়
  2. 🔑 weakMap.set(obj, data) — entry যোগ হয়
  3. ✂️ শেষ শক্ত reference সরে যায় (variable-এ null বা scope শেষ)
  4. 🧹 Engine নিজের সময়ে GC চালায়
  5. 🫥 Object আর তার WeakMap entry একসাথে মুছে যায়

const o = {}; o[1] = "a"; o["1"] = "b"; আর const m = new Map(); m.set(1, "a"); m.set("1", "b"); — এরপর Object.keys(o).length আর m.size কত?

  • 2 আর 2
  • 1 আর 2
  • 1 আর 1
  • 2 আর 1

JavaScript-এ [...new Set([3, 1, 3, 2, 1])] কী দেয়?

  • [ 1, 2, 3 ]
  • [ 3, 1, 3, 2, 1 ]
  • [ 3, 1, 2 ]
  • [ 3, 2, 1 ]

JavaScript-এ JSON.stringify(new Map([["a", 1]])) কী ফেরত দেয়?

  • '{"a":1}'
  • '[["a",1]]'
  • TypeError
  • '{}'

WeakMap সম্পর্কে কোন কথাটি সঠিক?

  • Key object-এর অন্য কোনো reference না থাকলে entry-টি garbage collection-এ মুছে যেতে পারে
  • for…of দিয়ে এর সব entry ঘোরা যায়
  • String key-ও রাখা যায়
  • এর value-গুলো ঠিক এক সেকেন্ড পরে মুছে যায়

সাধারণ object-কে dictionary হিসেবে ব্যবহার করে dict[userKey] দিয়ে খুঁজলে ব্যবহারকারী "constructor" লিখলে কী পাওয়া যায়?

  • undefined
  • Object নামের একটি function
  • ReferenceError
  • "constructor" string

Node.js 20-এ চলবে এমন কোডে দুটি Set a আর b-এর intersection বের করার নিরাপদ উপায় কোনটি?

  • a.intersection(b)
  • new Set([...a, ...b])
  • new Set([...a].filter((x) => b.has(x)))
  • a & b

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।

মনে রাখুন

  1. রেকর্ড → object, সংগ্রহ → Map/Set. Object-এর key string হয়ে যায় আর prototype থেকে বাড়তি key আসে; Map যেকোনো key রাখে, ঢোকানোর ক্রম আর size দেয়।
  2. Map/Set key মেলায় SameValueZero দিয়ে: 1 ≠ "1", NaN = NaN, object মেলে শুধু একই reference হলে।
  3. [...new Set(arr)] duplicate সরায়, প্রথম দেখার ক্রমে। Set-এর গণিতের method ES2025-এর — Node 20-এ নেই, তখন filter + has.
  4. Map JSON হয় না — Object.fromEntries বা [...map] দিয়ে রূপান্তর করুন; বাইরের key-এর জন্য সাধারণ object নয়।
  5. WeakMap/WeakSet key-কে বাঁচিয়ে রাখে না — object-ভিত্তিক cache আর চিহ্নের জন্য, leak ছাড়া। WeakRef/FinalizationRegistry অনিশ্চিত; প্রোগ্রামের সঠিকতা কখনো GC-র সময়ের ওপর নয়।

পরের পর্বে: এই পর্বে দেখলেন Map চুপচাপ {} হয়ে যায়। পরের পর্বে JSON-এর পুরো গল্প — JSON.parse আর JSON.stringify-এর ভেতরের নিয়ম, কোন কোন মান হারিয়ে যায়, replacer ও reviver দিয়ে Map বা তারিখ রক্ষা করা, আর structuredClone দিয়ে একটি object-এর সত্যিকারের গভীর কপি বানানো।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান