এই পর্বে যা শিখবেন
- Array destructuring — অবস্থান, বাদ দেওয়া, default, দুটি variable অদলবদল আর rest
- Object destructuring — নাম ধরে নেওয়া, নতুন নাম দেওয়া, default, nested আর বাকি property জড়ো করা
- Function parameter-এ destructuring — options object, আর
= {}কেন বাদ দেওয়া যায় না - Spread দিয়ে array ও object কপি, জোড়া লাগানো, আর মূলটি না বদলে নতুন মান তৈরি (immutable update)
- Shallow copy-র ফাঁদ, rest বনাম spread-এর পার্থক্য, আর কখন destructuring পড়া কঠিন করে তোলে
১ · Destructuring কী — literal দিয়ে বানানোর উল্টো কাজ
পর্ব ১৪ আর ১৫-এ আপনি object আর array বানিয়েছেন: { name: "রহিম", district: "রাজশাহী" }, [120, 80, 250]. এই বাঁকা আর চৌকো বন্ধনীর লেখাকে বলে literal. Literal কয়েকটি আলাদা মানকে একটি বাক্সে ভরে।
কিন্তু প্রোগ্রামে উল্টো কাজটি প্রায় সমান ঘন ঘন লাগে: একটি বাক্স হাতে এসেছে — server থেকে আসা একটি অর্ডার, একটি সেটিংসের object — আর আপনার দরকার তার ভেতরের দুই-তিনটি মান, আলাদা নামে। পুরোনো উপায়ে প্রতিটির জন্য একটি লাইন:
const id = order.id;
const items = order.items;
const district = order.customer.address.district;ES2015 (ES6) থেকে একই কাজ এক লাইনে করা যায় — একে বলে destructuring:
const { id, items, customer: { address: { district } } } = order;বাম পাশের { … } দেখতে object literal-এর মতো, কিন্তু এটি কোনো object বানায় না। এটি একটি pattern — একটি ছাঁচ। Engine ডান পাশের order-কে এই ছাঁচের ওপর বসিয়ে দেখে: “id নামের property আছে? তার মান id variable-এ রাখো। customer-এর ভেতরে address, তার ভেতরে district? সেটি district variable-এ রাখো।” ফল: তিনটি নতুন variable, এক লাইনে।
ঈদের আগে গ্রামের বাড়িতে একটি পার্সেল এল। প্যাক করার সময় দোকানি সব জিনিস একটি বাক্সে ভরেছিলেন — এটা literal. বাড়িতে আপনি বাক্স খুলে শাড়িটি মায়ের আলমারিতে, পাঞ্জাবিটি বাবার হাতে, আর চকলেট ছোট ভাইয়ের কাছে দিলেন — এটা destructuring. একই বাক্স, উল্টো দিকের কাজ। আর বাক্সে কী কী আছে না দেখে “বাকি সব আমার ঘরে রাখো” বললে — সেটা rest.
তিন ডটের দুই চেহারা — আগে মূল নিয়মটি
পর্ব ১৩-এ দেখেছেন, function sum(...prices)-এ তিন ডট বাড়তি argument জড়ো করে (rest parameter), আর Math.max(...prices)-এ একটি array-কে আলাদা আলাদা argument-এ ছড়িয়ে দেয় (spread). এই পর্বে দুটোই অনেক জায়গায় দেখবেন। গুলিয়ে না ফেলার জন্য একটি নিয়ম মনে রাখুন:
... = spread (ছড়ায়)খোলার বা নেওয়ার জায়গায়
... = rest (জড়ো করে)“বানানোর জায়গা” মানে array literal [ ], object literal { } আর function কলের বন্ধনী — যেখানে নতুন কিছু তৈরি হচ্ছে বা argument পাঠানো হচ্ছে। “খোলার বা নেওয়ার জায়গা” মানে =-এর বাম পাশের destructuring pattern, আর function-এর parameter তালিকা। ডট একই, দিক উল্টো।
২ · Array destructuring — অবস্থান ধরে খোলা
Array-এর মানের কোনো নাম নেই, আছে শুধু অবস্থান (index). তাই array destructuring মেলায় অবস্থান দিয়ে: pattern-এর প্রথম নাম পায় প্রথম মান, দ্বিতীয় নাম দ্বিতীয় মান।
const scores = [92, 85, 78, 64];
const [first, second] = scores; // 92, 85 — বাকিগুলো উপেক্ষিত
const [gold, , bronze] = scores; // 92, 78 — খালি কমা = বাদ
const [a, b, c, d, e = 0] = scores; // e পেল default 0
const [best, ...others] = scores; // 92, [85, 78, 64]দ্বিতীয় লাইনে মাত্র দুটি নাম, তাই প্রথম দুটি মান নেওয়া হলো; বাকি দুটি নিয়ে কেউ মাথা ঘামায় না — কোনো error নেই। তৃতীয় লাইনে দুই কমার মাঝে কোনো নাম নেই — এর মানে “দ্বিতীয়টি বাদ দাও”। চতুর্থ লাইনে পঞ্চম মান নেই, তাই e পেত undefined; কিন্তু = 0 লেখা থাকায় পেল ০. পঞ্চম লাইনে ...others বাকি সব মান একটি নতুন array-তে জড়ো করল — এটি destructuring-এর ভেতরের rest, যাকে বলে rest element.
কয়েকটি নিয়ম, যা পর্ব ১৩-এর default parameter-এর সাথে হুবহু মেলে:
- Default খাটে শুধু
undefined-এ।const [x = 5] = [null];দিলেxহয়null, ৫ নয় — ঠিক default parameter-এর মতো। - Rest থাকবে একটিই, আর সবশেষে।
const [...a, b] = arr;লিখলে SyntaxError. Rest-এর পরে কমাও দেওয়া যায় না। - যেকোনো iterable খোলা যায়, শুধু array নয়।
const [ch1, ch2] = "ঢাকা";দেয়"ঢ"আর"া"— string-কে code point ধরে খোলা হয়, তাই দ্বিতীয়টি আকার-চিহ্ন। Set বা Map-ও খোলা যায় (পর্ব ১৮)।
অদলবদল — দুই আসনের যাত্রী বদলানো
মেট্রো রেলের একটি বুকিং অ্যাপ ভাবুন। রাফি আর নীলা পাশাপাশি দুই আসনে; নীলা জানালার পাশে বসতে চায়। দুটি variable-এর মান অদলবদল (swap) করতে পুরোনো দিনে একটি তৃতীয় অস্থায়ী variable লাগত: const temp = seatA; seatA = seatB; seatB = temp;. Destructuring দিয়ে এক লাইনে হয়। কিন্তু এক লাইনে হলেও ভেতরে কী ঘটে, ধাপে ধাপে দেখুন — নইলে মনে হতে পারে প্রথম ঘর বদলে যাওয়ার পর দ্বিতীয়টি ভুল মান পাবে।
1let seatA = "রাফি", seatB = "নীলা";2[seatA, seatB] = [seatB, seatA];3console.log(seatA, seatB);
| seatA | |
| seatB | |
| অস্থায়ী array |
- শুরুর অবস্থা।
seatA-তে রাফি,seatB-তে নীলা। লক্ষ্য: দুজনের জায়গা বদলানো। - আগে ডান পাশ। যেকোনো assignment-এর মতো এখানেও engine আগে
=-এর ডান পাশ পুরোটা হিসাব করে। ডান পাশে একটি array literal — তাই একটি নতুন অস্থায়ী array তৈরি শুরু হলো। - পুরোনো মানের ছবি তোলা। অস্থায়ী array-তে বসল এই মুহূর্তের মান: প্রথমে
seatB(নীলা), তারপরseatA(রাফি)। কোনো variable এখনো বদলায়নি — এই array-টিই আসলে “temp” variable-এর কাজ করছে। - Pattern-এর প্রথম ঘর। এবার বাম পাশের pattern
[seatA, seatB]অবস্থান ধরে মান নেয়। অস্থায়ী array-এর ঘর ০ (নীলা) গেলseatA-তে। - দ্বিতীয় ঘর — মান আসে array থেকে। ঘর ১ (রাফি) গেল
seatB-তে।seatAইতিমধ্যে বদলে গেলেও সমস্যা নেই, কারণ মান নেওয়া হচ্ছে অস্থায়ী array থেকে,seatAথেকে নয়। - শেষ। অস্থায়ী array-এর আর কেউ খোঁজ রাখে না; garbage collector পরে সরিয়ে নেবে। আউটপুট
নীলা রাফি. একই কৌশলে তিনটিও ঘোরানো যায়:[a, b, c] = [c, a, b].
সেমিকোলন ছাড়া লিখলে swap ভেঙে যায়। আগের লাইনের শেষে সেমিকোলন না থাকলে [ দিয়ে শুরু হওয়া লাইনটি আগের লাইনের সাথে জুড়ে যায়:
let seatA = "রাফি", seatB = "নীলা"
[seatA, seatB] = [seatB, seatA]
পর্ব ০৪-এর ASI নিয়ম এখানে সেমিকোলন বসায় না, তাই engine পড়ে "নীলা"[seatA, seatB] = … — অর্থহীন একটি লাইন। এখানে seatB নিজের মান পাওয়ার আগেই পড়া হয়, তাই Node.js-এ আসে ReferenceError: Cannot access 'seatB' before initialization. যাঁরা সেমিকোলন ছাড়া লেখেন, তাঁরা এমন লাইনের শুরুতে একটি ; দেন: ;[seatA, seatB] = [seatB, seatA]. সবচেয়ে সহজ সমাধান: সেমিকোলন দিন, আর Prettier (পর্ব ৪৬) চালু রাখুন।
৩ · Object destructuring — নাম ধরে খোলা
Object-এর property-র নাম আছে, তাই object destructuring মেলায় নাম দিয়ে, অবস্থান দিয়ে নয়। const { name, phone } = customer; লিখলে ক্রম যা-ই হোক, name variable পায় customer.name, phone পায় customer.phone. Object-এ না থাকা নাম চাইলে মান হয় undefined — কোনো error নয়।
| লেখা | মানে | কোন variable তৈরি হয় |
|---|---|---|
{ name } | name property, একই নামে | name |
{ name: customerName } | name property, নতুন নাম customerName-এ (rename) | শুধু customerName |
{ phone = "নেই" } | না থাকলে (বা undefined হলে) default | phone |
{ name: n = "অতিথি" } | rename আর default একসাথে | n |
{ address: { district } } | nested — address-এর ভেতরে ঢুকে district | শুধু district (address নয়!) |
{ id, ...rest } | id আলাদা, বাকি সব property একটি নতুন object-এ | id, rest |
সবচেয়ে বেশি গুলিয়ে যায় rename-এর কোলন। Object literal-এ { name: "রহিম" } মানে “বাম পাশ নাম, ডান পাশ মান”। Pattern-এ { name: customerName } মানে “বাম পাশ কোথা থেকে নেবে, ডান পাশ কোথায় রাখবে”। মনে রাখার উপায়: কোলনের ডান পাশে যা আছে, শুধু সেটিই variable হয়। এবার একটি পূর্ণ pattern ধাপে ধাপে দেখুন — server থেকে আসা একটি অর্ডারের উত্তর:
1// res = { id: 501, customer: { name: "রহিম",2// address: { district: "রাজশাহী" } }, items: [...] }3const {4 id,5 customer: {6 name: customerName,7 phone = "নেই",8 address: { district },9 },10 coupon = "NONE",11} = res;
| id | |
| customerName | |
| phone | |
| district | |
| coupon | |
| customer, name, address |
- ডান পাশ আগে। Engine প্রথমে
res-এর মান নেয় — একটি object, যারcustomer-এর ভেতরে আরেকটি object, তার ভেতরেaddress. তারপর pattern ওপর থেকে নিচে, বাম থেকে ডানে মেলানো শুরু। id— সবচেয়ে সরল রূপ।res.idপড়ে501রাখা হলো একই নামের variableid-তে।{ id }আসলে{ id: id }-এর সংক্ষিপ্ত রূপ।customer: { … }— পথ, variable নয়। কোলনের ডান পাশে আরেকটি pattern, তাই engineres.customer-এর ভেতরে ঢুকল।customerনামে কোনো variable তৈরি হয় না। ভেতরেname: customerName—nameথেকে নাও,customerName-এ রাখো। ফল:customerName = "রহিম";nameনামেও কোনো variable নেই।phone = "নেই"— default.res.customer.phoneনেই, অর্থাৎundefined. তাই default চলল:phone = "নেই". Server যদিphone: nullপাঠাত, default চলত না —phoneহতোnull.address: { district }— আরও এক তলা নিচে। আবার পথ:res.customer.address-এ ঢুকেdistrictনেওয়া হলো।district = "রাজশাহী";addressvariable তৈরি হয়নি। দরকার হলে দুটোই নেওয়া যায়:address, address: { district }— একই property দুবার লেখা বৈধ।coupon = "NONE"— ওপরের তলায় ফিরে।res.couponনেই, তাই default"NONE". শেষে মোট পাঁচটি variable:id,customerName,phone,district,coupon.customerলিখে ব্যবহার করতে গেলেReferenceError: customer is not defined— শুধু পথ ছিল।
Pattern-এ কোলনের বাম পাশ = উৎস (কোন property), ডান পাশ = গন্তব্য। গন্তব্য একটি নাম হলে variable তৈরি হয়; গন্তব্য আরেকটি { } বা [ ] হলে সেটি শুধু ভেতরে ঢোকার পথ। Default = খাটে শুধু undefined-এ।
বাকি property জড়ো করা — rest properties
একটি পণ্যের object থেকে sku আলাদা করে নিতে চান, আর বাকি সব (রং, মাপ, যা-ই থাকুক) একসাথে আরেক জায়গায় পাঠাতে চান। তখন: const { sku, ...extra } = product;. extra একটি নতুন object, যাতে sku বাদে বাকি সব নিজস্ব (own) ও enumerable property আছে। বাস্তবে এর দুটি সাধারণ ব্যবহার:
- একটি property বাদ দেওয়া, মূলটি না বদলে। ব্যবহারকারীর তথ্য browser-এ পাঠানোর আগে পাসওয়ার্ডের hash সরাতে:
const { passwordHash, ...safeUser } = user;—safeUser-এ hash নেই, আরuserঅক্ষত। (delete user.passwordHashমূল object-টিই বদলে দিত।) - যা চিনি না, তা পার করে দেওয়া। একটি বোতাম বানানোর function নিজের দরকারি
labelআরcolorনিয়ে বাকি সব option (id,title, …) সরাসরি HTML element-এ বসিয়ে দেয়:function makeButton({ label, color, ...attrs }). React-এর component-এ এই ধাঁচ প্রচুর দেখবেন (পর্ব ৫৪)।
আগে থেকে থাকা variable-এ object destructuring: let total; ({ total } = bill); — পুরো লাইনটি গোল বন্ধনীতে মুড়তে হয়। বন্ধনী ছাড়া লাইনের শুরুর {-কে engine একটি block ভাবে, আর = bill দেখে SyntaxError দেয়। Array-তে এই সমস্যা নেই। এটি সেই একই কারণ, যার জন্য পর্ব ১৩-এ arrow function থেকে object ফেরত দিতে () => ({ … }) লিখতে হয়েছিল।
৪ · Function parameter, লুপ আর callback-এ destructuring
পর্ব ১৩-এ বলেছিলাম, তিনটির বেশি parameter হলে একটি object নিন। কেন? createOrder(2, true, false, "bKash") দেখে কেউ বলতে পারবে না কোন true-টি কী। একই কল option object দিয়ে: createOrder({ qty: 2, express: true, payment: "bKash" }) — প্রতিটি মানের পাশে তার নাম, ক্রমের কোনো ঝামেলা নেই, যেটি দরকার নেই সেটি বাদ। একে বলে options object. আর parameter-এর জায়গায় সরাসরি destructuring লিখলে function-এর প্রথম লাইনেই বোঝা যায় সে কোন কোন option চেনে:
function createOrder({ qty = 1, express = false, payment = "cash" } = {}) {
// এখানে qty, express, payment সাধারণ variable
}
createOrder({ express: true }); // qty=1, express=true, payment="cash"
createOrder(); // সব defaultশেষের = {} কেন বাদ দেওয়া যায় না
এখানে দুই স্তরের default আছে, আর দুটির কাজ আলাদা:
- ভেতরের default (
qty = 1, …) — object এসেছে, কিন্তু তাতে কোনো একটি property নেই। - বাইরের default (
= {}) — কোনো object-ই আসেনি। তখন parameter-এর মানundefined, আরundefined-কে destructure করা যায় না।
= {} না দিলে createOrder() ডাকলেই TypeError. ঠিক কোন বার্তা আসে, নিচের প্রথম runnable কোডে নিজেই দেখবেন। অনেক function-এর সব option ঐচ্ছিক — সেখানে কেউ খালি হাতে ডাকবেই। তাই নিয়ম: parameter-এ object destructuring করলে, আর সব property ঐচ্ছিক হলে, শেষে = {} দিন।
null আর undefined খোলা যায় না
Destructuring আসলে ভেতরে ভেতরে property পড়া — { name } = x মানে প্রায় name = x.name. আর null বা undefined-এর কোনো property নেই। তাই:
const { name } = null;
TypeError: Cannot destructure property 'name' of 'null' as it is null.
const [first] = null;
TypeError: null is not iterable
বার্তাগুলো V8-এর (Chrome, Edge, Node.js); Firefox-এর ভাষা আলাদা হলেও ধরন একই। বাস্তবে এটি ঘটে যখন API কোনো ফিল্ড null পাঠায় — যেমন নতুন গ্রাহকের address: null — আর কোডে লেখা থাকে { address: { district } }. Nested pattern-এর প্রতিটি তলা এক একটি সম্ভাব্য TypeError. বাঁচার তিন উপায়: nested অংশে default দিন (address: { district } = {} — শুধু undefined-এ খাটে), null-এর সম্ভাবনা থাকলে আগে ?? দিয়ে বদলে নিন (const address = res.address ?? {};), বা গভীর মান পড়তে পর্ব ০৮-এর optional chaining res.address?.district ব্যবহার করুন।
for…of আর callback-এ destructuring
Destructuring যেকোনো জায়গায় চলে যেখানে নতুন variable তৈরি হয়। দুটি জায়গায় এটি প্রায় অভ্যাসে পরিণত হয়েছে:
- Object-এর key আর value একসাথে ঘোরা:
Object.entries(stock)দেয়[key, value]জোড়ার array;for (const [sku, qty] of Object.entries(stock))প্রতিটি জোড়াকে সাথে সাথে দুটি নামে খোলে (পর্ব ১২-এর for…of). Map-এর ওপরfor (const [key, value] of map)একই ধাঁচ (পর্ব ১৮)। - Array method-এর callback-এ:
orders.map(({ id, total }) => `${id}: ${total}`)— callback-এর parameter-এ সরাসরি destructuring. পর্ব ১৬-এরmap,filter,sort-এ এটি খুব কাজের। খেয়াল করুন, arrow-এর parameter-এ destructuring থাকলে গোল বন্ধনী বাধ্যতামূলক।
এবার এই অংশ আর আগের অংশগুলোর সব কিছু একসাথে চালিয়ে দেখুন:
const order = {
id: 501,
customer: { name: "রহিম", address: { district: "রাজশাহী" } },
items: [{ sku: "TEA", qty: 2 }, { sku: "BUN", qty: 3 }],
};
const { id, customer: { name: customerName, address: { district } }, items } = order;
console.log(id, customerName, district, items.length);
const [first, , third = "খালি"] = ["A1", "A2"];
let seatA = "রাফি", seatB = "নীলা";
[seatA, seatB] = [seatB, seatA];
console.log(first, third, seatA, seatB);
function createOrder({ qty = 1, express = false } = {}) {
return `qty=${qty} express=${express}`;
}
console.log(createOrder({ express: true }), "|", createOrder());
function createOrderBad({ qty = 1 }) { return qty; }
try { createOrderBad(); } catch (e) { console.log(e.name + ": " + e.message); }
for (const [sku, qty] of Object.entries({ TEA: 2, BUN: 3 })) console.log(sku, qty);প্রথম লাইনে 501 রহিম রাজশাহী 2 — এক pattern-এ তিন তলা গভীর থেকে মান এল। দ্বিতীয় লাইনে A1 খালি নীলা রাফি: দ্বিতীয় আসন কমা দিয়ে বাদ, তৃতীয়টি নেই বলে default, আর swap কাজ করেছে। তৃতীয় লাইনে qty=1 express=true | qty=1 express=false — খালি কলেও = {} বাঁচিয়ে দিল। চতুর্থ লাইনে TypeError: Cannot read properties of undefined (reading 'qty') — = {} ছাড়া খালি কল। লক্ষ করুন, V8 এখানে “Cannot destructure” না বলে সাধারণ property পড়ার বার্তা দেয়, কারণ parameter-এর destructuring ভেতরে ভেতরে undefined.qty পড়ার চেষ্টা। বার্তা দেখে “destructuring” শব্দ না খুঁজে parameter-এর default দেখুন। শেষ দুই লাইনে TEA 2 আর BUN 3 — Object.entries-এর প্রতিটি জোড়া খোলা হলো।
৫ · Array spread — কপি, জোড়া, মাঝে বসানো আর রূপান্তর
এবার উল্টো দিক। Array literal [ ]-এর ভেতরে ...x লিখলে x-এর প্রতিটি মান একে একে বের হয়ে সেই জায়গায় বসে। “বানানোর জায়গা” — তাই spread.
const morning = ["চা", "পরোটা"];
const evening = ["সিঙ্গারা", "চা"];
const copy = [...morning]; // নতুন array, একই মান
const allDay = [...morning, ...evening]; // জোড়া: 4টি মান
const lunch = [...morning, "ভাত", ...evening]; // মাঝে বসানো
const letters = [..."বাংলা"]; // [ 'ব', 'া', 'ং', 'ল', 'া' ]
const unique = [...new Set(allDay)]; // [ 'চা', 'পরোটা', 'সিঙ্গারা' ]copy নতুন array — copy.push("ডিম") করলে morning বদলায় না (পর্ব ১৫-এর reference-এর কথা মনে করুন: const copy = morning লিখলে নতুন array হতো না, একই array-এর দুটি নাম হতো)। allDay-তে দুটি array পাশাপাশি — পুরোনো concat-এর কাজ। lunch-এ মাঝখানে একটি নতুন মান — splice-এর মতো মূল array না বদলে। letters: string একটি iterable, তাই spread করা যায়; ভাগ হয় code point ধরে, তাই “বাংলা” থেকে ৫টি টুকরো, যদিও চোখে দেখা অক্ষর-গুচ্ছ দুটি (বাং, লা) — পর্ব ০৯-এর grapheme-এর গল্প। unique: Set ডুপ্লিকেট রাখে না, আর spread তাকে আবার array বানায় — ডুপ্লিকেট সরানোর সবচেয়ে প্রচলিত এক লাইন।
Spread যেকোনো iterable নেয় — array, string, Set, Map, arguments, DOM-এর NodeList (পর্ব ৩৭)। কিন্তু সাধারণ object iterable নয়: [...{ a: 1 }] লিখলে TypeError — Node.js-এ বার্তা আসে {(intermediate value)} is not iterable. Object-এর মান array-তে চাইলে Object.values(obj) বা Object.entries(obj) দিয়ে আগে array বানিয়ে নিন।
Function কলে spread
পর্ব ১৩-এ দেখেছেন: Math.max(...prices) হলো Math.max(120, 80, 250). যে function আলাদা আলাদা argument চায় কিন্তু আপনার হাতে একটি array, সেখানে spread সেতু। Spread-এর আগে-পরে সাধারণ argument-ও দেওয়া যায়: Math.max(0, ...prices, 100). ES2015-এর আগে এই কাজ হতো Math.max.apply(null, prices) দিয়ে — পর্ব ২৪-এ apply দেখবেন।
বিশাল array spread করে function-এ পাঠাবেন না। Function কলে spread করলে প্রতিটি উপাদান একটি করে আলাদা argument হয়, আর engine-এর argument রাখার জায়গা সীমিত। আমাদের পরীক্ষায় Node.js-এ (ডিফল্ট সেটিং) ১ লাখ সংখ্যার Math.max(...arr) চলেছে, কিন্তু ২ লাখে এসেছে RangeError: Maximum call stack size exceeded. সঠিক সীমা engine, সংস্করণ আর stack-এর আকারভেদে বদলায় — তাই অজানা আকারের ডেটায় লুপ বা reduce ব্যবহার করুন: arr.reduce((m, x) => Math.max(m, x), -Infinity). বিস্তারিত ভাবনার প্রশ্ন ৫-এ।
৬ · Object spread — কপি, merge আর মূলটি না বদলে হালনাগাদ
Object literal { }-এর ভেতরেও spread চলে (ES2018 থেকে)। { ...obj } লিখলে obj-এর প্রতিটি নিজস্ব (own) enumerable property একটি নতুন object-এ কপি হয়। এর তিনটি বড় ব্যবহার:
📋 কপি
const draft = { ...invoice }; — নতুন object, একই property. draft-এ ওপরের তলার কোনো property বদলালে invoice অক্ষত।
🔀 Merge
{ ...defaults, ...userSettings } — দুটি object মিলিয়ে একটি। একই নাম দুই জায়গায় থাকলে পরেরটি জেতে।
✏️ হালনাগাদ কপি
{ ...item, qty: 4 } — সব কপি, শুধু qty নতুন মানে। মূল item বদলায় না।
“পরেরটি জেতে” নিয়মটিই object spread-এর হৃদয়। একটি অ্যাপের ডিফল্ট সেটিংস আর ব্যবহারকারীর নিজের পছন্দ — কোনটি কাকে ঢাকবে, তা ঠিক হয় শুধু লেখার ক্রম দিয়ে। ধাপে ধাপে দেখুন:
1const a = { theme: "light", lang: "bn", x: 0 };2const b = { theme: "dark", size: 16 };3const r = { ...a, ...b, x: 1 };4console.log(r);
| theme | |
| lang | |
| x | |
| size |
- খালি object দিয়ে শুরু। লাইন ৩-এর
{ }একটি নতুন, খালি object তৈরি করল। এরপর ভেতরের অংশগুলো বাম থেকে ডানে একে একে চলবে — ঠিক যেন পরপর কয়েকটি assignment. ...a— a-র সব property কপি।theme: "light",lang: "bn",x: 0— a-তে যে ক্রমে ছিল, সেই ক্রমে বসল।...b— একই নাম পেলে ঢেকে দেয়। b-রtheme: "dark"আগের"light"-এর ওপর লেখা হলো।sizeনতুন, তাই শেষে যোগ হলো। লক্ষ করুন,themeতার পুরোনো জায়গাতেই থাকল — শুধু মান বদলাল।x: 1— সাধারণ property-ও একই নিয়মে। শেষে লেখাx: 1a থেকে আসা0-কে ঢেকে দিল। যদি লিখতেন{ x: 1, ...a }, তাহলে উল্টো — a-রx: 0পরে এসে ১-কে ঢেকে দিত।- ফল।
{ theme: 'dark', lang: 'bn', x: 1, size: 16 }. Key-এর ক্রম = প্রতিটি নাম প্রথম যখন এসেছিল; মান = শেষে যে লিখেছে। a আর b দুটোই অক্ষত — নতুন object শুধু r.
{ ...defaults, ...userSettings } → ব্যবহারকারীর পছন্দ জেতে{ ...userSettings, ...defaults } → ডিফল্ট সব ঢেকে দেয় (প্রায় সবসময় ভুল)মূলটি না বদলে হালনাগাদ — immutable update
একটি অনলাইন দোকানের কার্টে বনরুটির সংখ্যা ৩ থেকে ৪ করতে হবে। সহজ উপায় cart[1].qty++ — কিন্তু এতে মূল কার্টটিই বদলে যায় (mutation). যে কোড আগের কার্ট মনে রেখেছিল (যেমন “undo” বোতাম, বা পর্দা আবার আঁকা দরকার কি না যাচাই), সে বুঝতেই পারবে না কিছু বদলেছে। বিকল্প হলো immutable update: মূলটি না ছুঁয়ে একটি নতুন কার্ট বানানো, যাতে শুধু বদলানো অংশ নতুন:
const updated = cart.map((item) =>
item.sku === "BUN" ? { ...item, qty: item.qty + 1 } : item
);map (পর্ব ১৬) একটি নতুন array দেয়। বনরুটির জায়গায় { ...item, qty: … } — একটি নতুন object, যার সব property পুরোনোটির মতো, শুধু qty নতুন (মনে রাখুন: পরেরটি জেতে)। বাকি পণ্যগুলোর object একই থাকে, নতুন করে কপি হয় না। ফলে cart !== updated, কিন্তু চায়ের object দুই কার্টেই এক — বদলানো জিনিসটুকুই নতুন। React আর Redux-এর মতো লাইব্রেরি ঠিক এই ধাঁচের ওপর দাঁড়িয়ে: “reference বদলেছে মানে কিছু বদলেছে” — ভাবনার প্রশ্ন ৩-এ এর ইতিহাস।
Object spread-এর তিনটি সূক্ষ্ম নিয়ম
undefined-ও একটি মান — ঢেকে দেয়।{ ...defaults, ...{ fontSize: undefined } }-এfontSizeহয়undefined; default parameter-এর মতো “undefined হলে বাদ” নিয়ম এখানে নেই। Form থেকে আসা ফাঁকা ঘর merge করার আগেundefinedমানের key সরিয়ে নিন। নিচের দ্বিতীয় runnable কোডে দেখবেন।null,undefinedspread করলে কিছু হয় না।{ ...null }দেয়{}, কোনো error নয় — তাই{ ...defaults, ...maybeSettings }নিরাপদ। (Array spread-এ[...null]কিন্তু TypeError.) String spread করলে প্রতিটি অক্ষর একটি সংখ্যা-key পায়:{ ..."hi" }দেয়{ '0': 'h', '1': 'i' }.- শুধু নিজস্ব, enumerable property কপি হয়। Prototype থেকে পাওয়া method কপি হয় না। তাই একটি class-এর object spread করলে (পর্ব ২৬) সাধারণ object পাবেন — method হারিয়ে যায়। Getter থাকলে তার বর্তমান মান কপি হয়, getter নিজে নয়।
৭ · Shallow copy-র ফাঁদ — ভেতরের object ভাগাভাগি
এই পর্বের সবচেয়ে গুরুত্বপূর্ণ সতর্কবাণী: spread কপি করে শুধু ওপরের তলা। একে বলে shallow copy. কোনো property-র মান নিজেই একটি object বা array হলে, কপি হয় শুধু তার reference — ঠিকানা। ফলে নতুন আর পুরোনো object দুটিই একই ভেতরের object-কে দেখায়। ধাপে ধাপে দেখুন:
1const original = { name: "রহিম",2 address: { district: "রাজশাহী" } };3const copy = { ...original };4copy.name = "করিম";5copy.address.district = "খুলনা";6console.log(original.name, original.address.district);
| original → | |
| copy → | |
| #1.name | |
| #2.name | |
| #1.address, #2.address | |
| #3.district |
- দুটি object তৈরি, একটি আরেকটির ভেতরে। Heap-এ বাইরের object #1 (
nameআরaddress), আর ঠিকানার জন্য আলাদা object #3. #1-এরaddress-এ আসলে রাখা আছে #3-এর ঠিকানা, পুরো object নয়। - Spread — নতুন বাইরের object.
{ ...original }একটি নতুন object #2 বানাল, আর #1-এর প্রতিটি property-র মান কপি করল।name-এর মান একটি string — সেটি কপি হলো।address-এর মান একটি ঠিকানা — কপি হলো সেই ঠিকানাটি। ফলে #1 আর #2 দুজনেই একই #3-কে দেখাচ্ছে। Object #3-এর কোনো কপি তৈরি হয়নি। - ওপরের তলা বদলানো — নিরাপদ।
copy.name = "করিম"শুধু #2-এরnameবদলাল। #1-এরnameআগের মতো রহিম। এই পর্যন্ত মনে হয় spread একটি পূর্ণ কপি দিয়েছে। - ভেতরের তলা বদলানো — ফাঁদ।
copy.address.district = "খুলনা": engine প্রথমেcopy.addressপড়ে পেল #3-এর ঠিকানা, তারপর #3-এরdistrictবদলাল। কিন্তু #3 তোoriginal-এরও ঠিকানা! - ফল — মূলটিও বদলে গেছে। আউটপুট
রহিম খুলনা: নাম আলাদা, কিন্তু জেলা দুজনেরই খুলনা। কেউoriginal-এ হাতই দেয়নি, তবুও বদলেছে। বড় প্রজেক্টে এমন বাগ খুঁজে পেতে ঘণ্টার পর ঘণ্টা লাগে।
সমাধান দুই রকম। (ক) প্রতিটি তলায় spread: { ...original, address: { ...original.address, district: "খুলনা" } } — যে পথে বদল, সেই পথের প্রতিটি object নতুন। Immutable update-এ এটাই প্রচলিত ধাঁচ। (খ) পুরো গভীর কপি: structuredClone(original) — প্রতিটি তলার নতুন কপি, একে বলে deep copy. এটি কোন কোন জিনিস কপি করতে পারে আর কোনগুলো পারে না (function, DOM node), পুরোটা পর্ব ১৯-এ। পুরোনো কৌশল JSON.parse(JSON.stringify(x))-এর দুর্বলতাও সেখানে।
এবার spread-এর সব দিক নিজে চালিয়ে দেখুন — settings merge, কার্ট হালনাগাদ, shallow copy-র ফাঁদ আর rest properties:
const defaults = { theme: "light", lang: "bn", fontSize: 16 };
const userSettings = { theme: "dark", fontSize: undefined };
console.log({ ...defaults, ...userSettings, lang: "en" });
const cart = [{ sku: "TEA", qty: 2 }, { sku: "BUN", qty: 3 }];
const updated = cart.map((item) =>
item.sku === "BUN" ? { ...item, qty: item.qty + 1 } : item
);
console.log(cart[1].qty, updated[1].qty, cart === updated, cart[0] === updated[0]);
const original = { name: "রহিম", address: { district: "রাজশাহী" } };
const copy = { ...original };
copy.name = "করিম";
copy.address.district = "খুলনা";
console.log(original.name, original.address.district);
const prices = [120, 80, 250];
console.log(Math.max(...prices), [..."বাংলা"].length, [...new Set([3, 1, 3])]);
const { sku, ...extra } = { sku: "TEA", color: "লাল", size: "M" };
console.log(sku, extra);প্রথম লাইনে { theme: 'dark', lang: 'en', fontSize: undefined } — ব্যবহারকারীর theme জিতেছে, শেষে লেখা lang জিতেছে, আর ফাঁদ: fontSize: undefined ডিফল্ট ১৬-কে মুছে দিয়েছে। দ্বিতীয় লাইনে 3 4 false true — মূল কার্টে এখনো ৩, নতুনটিতে ৪; কার্ট দুটি আলাদা, কিন্তু চায়ের object ভাগাভাগি। তৃতীয় লাইনে রহিম খুলনা — shallow copy-র ফাঁদ, ঠিক স্টেপারের মতো। চতুর্থ লাইনে 250 5 [ 3, 1 ]. শেষ লাইনে TEA { color: 'লাল', size: 'M' } — rest properties একটি নতুন object-এ বাকিগুলো জড়ো করেছে।
৮ · Rest বনাম spread এক টেবিলে, আর কখন destructuring ক্ষতি করে
| কোথায় | উদাহরণ | কাজ | ফল |
|---|---|---|---|
| Parameter তালিকা | function f(a, ...rest) | rest — বাকি argument জড়ো | আসল array (খালি হলে []) |
| Array pattern | const [x, ...rest] = arr | rest — বাকি মান জড়ো | নতুন array |
| Object pattern | const { id, ...rest } = obj | rest — বাকি property জড়ো | নতুন সাধারণ object |
| Function কল | f(...arr) | spread — আলাদা argument-এ ছড়ানো | যেকোনো iterable চলে |
| Array literal | [...a, ...b] | spread — মানগুলো বসানো | নতুন array; iterable লাগবে |
| Object literal | { ...a, x: 1 } | spread — property কপি | নতুন object; পরেরটি জেতে; null চলে |
কোন জিনিস কোথায় spread করা যায় — একটি ছোট টেবিল, কারণ এখানে প্রায়ই ভুল হয়:
| মান | [...x] | { ...x } |
|---|---|---|
Array [1, 2] | [ 1, 2 ] | { '0': 1, '1': 2 } |
String "hi" | [ 'h', 'i' ] (code point ধরে) | { '0': 'h', '1': 'i' } |
| Set | মানগুলো, ঢোকানোর ক্রমে | {} — কাজের নয় |
| Map | [key, value] জোড়ার array | {} — কাজের নয় |
| সাধারণ object | TypeError — iterable নয় | property কপি |
null / undefined | TypeError | {} |
Map-কে array বানাতে [...map], আবার Map-এ ফেরাতে new Map(arr); object থেকে Map-এ যেতে new Map(Object.entries(obj)) — পর্ব ১৮-এ এই সেতুগুলো কাজে লাগবে।
কখন destructuring পড়া কঠিন করে
Destructuring ছোট লেখার যন্ত্র, কিন্তু ছোট মানেই পরিষ্কার নয়। নিচের লাইনটি বৈধ JavaScript:
const { data: { order: { customer: { address: { district: d = "?" } = {} } = {} } = {} } = {} } = res;
এটি কী করে, বুঝতে আপনার কত সেকেন্ড লাগল? একই কাজ পর্ব ০৮-এর অস্ত্র দিয়ে: const d = res.data?.order?.customer?.address?.district ?? "?"; — বাম থেকে ডানে পড়া যায়, আর null-ও সামলায় (destructuring-এর default সামলায় শুধু undefined). কয়েকটি ব্যবহারিক নিয়ম:
- এক বা দুই তলা পর্যন্ত destructuring ঠিক আছে; তার বেশি হলে optional chaining বা কয়েকটি আলাদা লাইন।
- Rename-এর কারণে মূল নাম হারালে পাঠক বিভ্রান্ত হন।
{ name: n, price: p }-এর চেয়ে{ name, price }ভালো; rename করুন শুধু নামের সংঘর্ষ এড়াতে (যেমন দুটি object-এরইnameআছে)। - অনেক মানের array destructuring (
[ , , , x]) এড়িয়ে চলুন। চারটি কমা গুনে কেউ পড়তে চায় না;arr[3]বাarr.at(-1)বেশি স্পষ্ট। Array destructuring সবচেয়ে ভালো ছোট, নির্দিষ্ট আকারের “tuple”-এ —[key, value],[lat, lng], React-এর[count, setCount].
Python-এর tuple unpacking (a, b = b, a, first, *rest = items) JavaScript-এর array destructuring-এর সবচেয়ে কাছের আত্মীয় — Python-এর *rest এখানে ...rest, আর Python-এর **kwargs/{**a, **b} এখানে object rest/spread (Python-এর tuple ও unpacking). C++17-এর structured bindings (auto [x, y] = point;) একই ধারণা, তবে সেখানে অবস্থান বা member-এর ক্রম ধরে মেলে, নাম ধরে নয়, আর default নেই (C++-এর enum ও struct). Java-তে সাধারণ destructuring নেই; Java 21-এর record pattern (if (o instanceof Point(int x, int y))) কাছাকাছি (Java-র record). C-তে এমন কিছু নেই — struct-এর প্রতিটি member আলাদা করে পড়তে হয়।
- Rest আর spread-এর পার্থক্য কী? — চিহ্ন একই
...; pattern বা parameter-এ থাকলে rest (বাকিগুলো জড়ো করে), literal বা function কলে থাকলে spread (ছড়িয়ে দেয়)। - Spread কি deep copy দেয়? — না, shallow copy; ভেতরের object-এর reference ভাগাভাগি হয়। গভীর কপির জন্য
structuredClone. - দুটি variable কোনো তৃতীয় variable ছাড়া কীভাবে swap করবেন? —
[a, b] = [b, a];— ডান পাশে আগে অস্থায়ী array তৈরি হয়। { ...a, ...b }-এ দুটিতেই একই key থাকলে কোনটি থাকে? — পরে লেখা, অর্থাৎb-এর মান।function f({ x } = {})-এ= {}কেন? — argument ছাড়া ডাকলে parameterundefinedহয়, আরundefineddestructure করলে TypeError;= {}খালি object বসিয়ে দেয়।
৯ · AI-এর যুগে — AI-এর লেখা destructuring আর spread যাচাই
AI টুলগুলো destructuring আর spread খুব পছন্দ করে — আধুনিক দেখায়, লাইন কমে। সমস্যা হলো, এগুলোর ভুল প্রায়ই চোখে পড়ে না: কোড চলে, শুধু বিশেষ কোনো ইনপুটে (খালি কল, null ফিল্ড, nested বদল) ভাঙে। এই বিষয়ে AI-এর সাধারণ ভুলগুলো:
| AI-এর ভুল | কেন সমস্যা | কী চাইবেন |
|---|---|---|
| তিন-চার তলা গভীর nested destructuring | পড়া কঠিন; যেকোনো তলায় null এলে TypeError | এক-দুই তলা; বাকিটা ?. আর ?? |
Parameter object-এ = {} নেই | Argument ছাড়া ডাকলেই TypeError | function f({ … } = {}) |
ধরে নেওয়া যে { ...state } গভীর কপি | ভেতরের object বদলালে মূল state-ও বদলায় | বদলের পথের প্রতিটি তলায় spread, বা structuredClone |
Merge-এ ভুল ক্রম { ...user, ...defaults } | ব্যবহারকারীর পছন্দ ডিফল্টে ঢেকে যায় | ডিফল্ট আগে, ব্যবহারকারী পরে |
Destructuring default দিয়ে null ধরার চেষ্টা | Default শুধু undefined-এ খাটে | ?? দিয়ে আগে বদলানো |
লুপের ভেতরে acc = [...acc, x] | প্রতিবার পুরো array কপি — বড় ডেটায় খুব ধীর | push একটি স্থানীয় array-তে, বা map |
AI-কে এই ধরনের কোড লেখাতে চাইলে সীমাগুলো আগেই বলে দিন:
AI-এর লেখা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Destructuring কোথা থেকে এল? ML, Haskell আর Python-এর “pattern matching” আর “unpacking”-এর সাথে এর সম্পর্ক কী, আর JavaScript-এ এটি আসতে এত দেরি হলো কেন?
ধারণাটির শিকড় functional programming-এ। ১৯৭০-এর দশকে এডিনবরায় Robin Milner-এর দল ML ভাষা তৈরি করে, যেখানে একটি মানকে তার “আকার” দিয়ে মেলানো যেত: একটি জোড়া হলে (x, y), একটি তালিকা হলে “প্রথম উপাদান আর বাকিটা”। একে বলে pattern matching. পরে Haskell (১৯৯০) এই ধারণাকে ভাষার কেন্দ্রে বসায় — function-এর সংজ্ঞাই লেখা হয় pattern দিয়ে। এই ভাষাগুলোতে pattern দুটি কাজ করে: মান খুলে নাম দেয়, আর আকার না মিললে অন্য শাখায় যায়।
মূলধারার ভাষায় এর সরল রূপটি জনপ্রিয় করে Python. শুরু থেকেই Python-এ a, b = b, a লেখা যেত — tuple unpacking. Python 3.0 (২০০৮) যোগ করে extended unpacking: first, *rest = items. JavaScript-এর [first, ...rest] প্রায় হুবহু এর প্রতিচ্ছবি।
JavaScript-এ প্রথম destructuring আসে মানদণ্ডের বাইরে: ২০০৬ সালে Mozilla তাদের নিজস্ব “JavaScript 1.7”-এ (Firefox 2) destructuring assignment যোগ করে — অনেকটা Python দেখে। কিন্তু অন্য ব্রাউজারে না থাকায় ওয়েবে এটি ব্যবহার করা যেত না। মানদণ্ডে আনার বড় চেষ্টা ছিল ES4-এ, যা ২০০৮ সালে মতবিরোধে বাতিল হয়। তারপর TC39 ছোট ধাপে এগোনোর পথ নেয়: ES5 (২০০৯) আনে তুলনামূলক ছোট, সতর্ক উন্নতি (strict mode, JSON, নতুন array method), আর destructuring, default, rest, spread (array-এর জন্য) — সব একসাথে মানদণ্ডে আসে ES2015-এ। প্রায় দশ বছরের দেরির কারণ ভাষার প্রযুক্তি নয়, বরং ওয়েবের নিয়ম: সব ব্রাউজার রাজি না হলে কোনো বৈশিষ্ট্য কার্যত অস্তিত্বহীন।
খেয়াল করুন, JavaScript নিয়েছে pattern matching-এর শুধু প্রথম অর্ধেক — খোলা আর নাম দেওয়া। আকার না মিললে অন্য শাখায় যাওয়ার অংশটি নেয়নি; না মিললে শুধু undefined বা TypeError. পূর্ণ pattern matching (match expression) নিয়ে TC39-এ একটি প্রস্তাব বহু বছর ধরে আলোচনায় আছে, তবে এখনো মানদণ্ডে আসেনি — আর অনেক ভাষা (Rust, Scala, Python 3.10-এর match, Java 21-এর record pattern) ইতিমধ্যে সেই পথে গেছে।
মূল উপলব্ধি: Destructuring কোনো সাজসজ্জা নয় — এটি “ডেটার আকার দিয়ে ভাবা”-র একটি পুরোনো, গবেষণায় পরীক্ষিত ধারণা। JavaScript এটি দেরিতে পেয়েছে কারণ ওয়েবে নতুন ব্যাকরণ আনতে সব ব্রাউজারের সম্মতি লাগে।
প্র ০২Array spread ES2015-এ এলেও object rest/spread এসেছে ES2018-এ। মাঝের তিন বছর মানুষ কী করত, আর Babel ও React সম্প্রদায় কীভাবে এটিকে মানদণ্ডে ঠেলে নিয়ে গেল?
ES2015-এ array spread আর array/object destructuring এসেছিল, কিন্তু object literal-এর ভেতরে ... আর object pattern-এ ...rest ছিল না। Object কপি বা merge করার মানদণ্ডের উপায় ছিল ES2015-এরই Object.assign({}, defaults, settings). কাজ চলে, কিন্তু একটি সাধারণ ভুল ছিল: প্রথম argument-এ {} দিতে ভুলে গেলে Object.assign(defaults, settings) মূল defaults-কেই বদলে দেয়।
এই সময়েই React (২০১৩) আর পরে Redux (২০১৫) জনপ্রিয় হয়, যাদের মূল নিয়ম “state বদলাবেন না, নতুন state বানান”। ফলে প্রতিদিন হাজার হাজার লাইনে object কপি আর হালনাগাদ লিখতে হতো। Facebook-এর React দলের Sebastian Markbåge object rest/spread-এর প্রস্তাবটি TC39-এ এগিয়ে নেন। আর এখানে আসে Babel — একটি টুল যা নতুন JavaScript লিখে পুরোনো JavaScript-এ রূপান্তর করে। Babel-এর একটি plugin দিয়ে প্রস্তাব অবস্থাতেই { ...state, loading: true } লেখা যেত। React-এর শেখার উপকরণ আর জনপ্রিয় starter প্রজেক্টে এটি ব্যাপকভাবে ব্যবহার হতে থাকে — মানদণ্ডের আগেই লাখো কোডবেসে।
TC39-এর প্রক্রিয়ায় (Stage 0 থেকে 4) একটি বৈশিষ্ট্য চূড়ান্ত হতে বাস্তব ব্যবহারের অভিজ্ঞতা আর অন্তত দুটি স্বাধীন বাস্তবায়ন লাগে। Babel-এর মাধ্যমে বিপুল ব্যবহার এই প্রমাণ জোগায়, আর প্রস্তাবটি ES2018-এ মানদণ্ডে যুক্ত হয়।
একটি সূক্ষ্ম নকশাগত পার্থক্যও আছে। Object.assign লক্ষ্য object-এ সাধারণ assignment করে — তাই লক্ষ্যে setter থাকলে সেটি চলে। Object spread প্রতিটি property নতুন করে সংজ্ঞায়িত করে (define), তাই কোনো setter চলে না। আর Object.assign একটি বিদ্যমান object-কে বদলাতে পারে; spread সবসময় নতুন object দেয়।
মূল উপলব্ধি: আধুনিক JavaScript-এর অনেক বৈশিষ্ট্য “নিচ থেকে ওপরে” এসেছে — সম্প্রদায় আগে টুল দিয়ে ব্যবহার করেছে, তারপর মানদণ্ড সেটিকে স্বীকৃতি দিয়েছে। Transpiler ভাষার বিবর্তনের পরীক্ষাগার হয়ে উঠেছে।
প্র ০৩Redux আর React কেন “state বদলাবেন না, নতুন কপি বানান” নিয়মের ওপর এত জোর দেয়? প্রতিবার নতুন object বানানো কি অপচয় নয়?
২০১৫ সালে Dan Abramov আর Andrew Clark Redux তৈরি করেন। এর কেন্দ্রে একটি সরল ধারণা: পুরো অ্যাপের অবস্থা (state) একটি object-এ, আর state বদলায় শুধু একটি pure function (reducer) দিয়ে, যা পুরোনো state আর একটি action নিয়ে নতুন state ফেরত দেয় — পুরোনোটি না ছুঁয়ে।
কেন? মূল কারণ বদল সস্তায় চেনা। একটি বড় state object-এ কিছু বদলেছে কি না জানতে, mutation-এর জগতে প্রতিটি তলার প্রতিটি property তুলনা করতে হয় (deep comparison) — ব্যয়বহুল। কিন্তু নিয়ম যদি হয় “বদল হলে নতুন object”, তাহলে প্রশ্নটি হয়ে যায় prev !== next — একটি মাত্র reference তুলনা। React-এর React.memo, useMemo আর state হালনাগাদের পর আবার আঁকা হবে কি না — সবই এই সস্তা তুলনার ওপর নির্ভর করে। Mutation করলে reference একই থাকে, তাই React ভাবে কিছুই বদলায়নি, আর পর্দা আর হালনাগাদ হয় না — নতুনদের সবচেয়ে সাধারণ React বাগগুলোর একটি।
দ্বিতীয় লাভ: ইতিহাস বিনামূল্যে। পুরোনো state কখনো বদলায় না, তাই প্রতিটি পুরোনো state রেখে দিলেই undo/redo বা “time-travel debugging” — Redux DevTools-এ পেছনে গিয়ে আগের যেকোনো মুহূর্ত দেখা যায়।
অপচয়ের প্রশ্নের উত্তর হলো structural sharing: এই পর্বের কার্ট উদাহরণের মতো, শুধু বদলের পথের object নতুন হয়; বাকি সব পুরোনো object-ই নতুন state-এ ভাগাভাগি হয়। ১০০০ পণ্যের কার্টে একটির সংখ্যা বদলালে নতুন হয় একটি array আর একটি object, বাকি ৯৯৯টি নয়। তবে গভীর nested state-এ হাতে লেখা spread-এর সিঁড়ি ({ ...s, a: { ...s.a, b: { ...s.a.b, c: 1 } } }) বিরক্তিকর আর ভুলপ্রবণ। তাই Facebook-এর Immutable.js-এর মতো লাইব্রেরি এসেছিল, আর পরে Michel Weststrate-এর Immer — যেখানে আপনি mutation-এর মতো করে লেখেন, আর Immer পেছনে Proxy (পর্ব ২৮) দিয়ে নতুন immutable কপি বানায়। Redux Toolkit ভেতরে Immer ব্যবহার করে।
মূল উপলব্ধি: Immutable update-এ কিছু বাড়তি object তৈরি হয়, কিন্তু বিনিময়ে “কী বদলেছে” প্রশ্নটি একটি reference তুলনায় নেমে আসে। Spread এই ধাঁচকে ভাষার স্তরে সহজ করেছে — আর তার shallow প্রকৃতিই structural sharing-কে সম্ভব করে।
প্র ০৪অনেকে “spread operator” বলেন, কিন্তু মানদণ্ড আর MDN একে “spread syntax” বলে। পার্থক্যটি কি শুধু নামের, নাকি এর পেছনে সত্যিকারের কারণ আছে?
পার্থক্যটি আসল। একটি operator (যেমন +, typeof, !) এক বা একাধিক operand নিয়ে একটি মান তৈরি করে, আর তাকে একটি expression হিসেবে যেকোনো জায়গায় বসানো যায়: const x = a + b;, console.log(typeof y). এবার চেষ্টা করুন const x = ...arr; — SyntaxError. (...arr)-ও SyntaxError. কারণ ...arr কোনো একক মান তৈরি করে না; এটি “এখানে শূন্য, এক বা অনেকগুলো মান বসাও” — এমন একটি নির্দেশ, যার অর্থ আছে শুধু নির্দিষ্ট কিছু প্রসঙ্গে।
ECMAScript মানদণ্ডের ব্যাকরণেও এটি দেখা যায়। ... কোনো operator-এর তালিকায় নেই; বরং array literal-এর ব্যাকরণে আছে SpreadElement, function কলের argument তালিকায় আছে ... AssignmentExpression, আর object literal-এর property সংজ্ঞায় আছে আলাদা একটি রূপ। Rest-এর জন্যও আলাদা নিয়ম: parameter তালিকায় FunctionRestParameter, আর pattern-এ BindingRestElement বা AssignmentRestElement. অর্থাৎ প্রতিটি প্রসঙ্গের নিজস্ব ব্যাকরণ আর নিজস্ব অর্থ — array-তে iterable লাগে, object-এ লাগে না, null object spread-এ চলে কিন্তু array spread-এ চলে না। একটি “operator” হলে সব জায়গায় এক আচরণ হতো।
এই কারণেই MDN কয়েক বছর আগে তাদের পাতার নাম “Spread operator” থেকে বদলে “Spread syntax” করেছে। তবে কথ্য ভাষায় “spread operator” এত প্রচলিত যে চাকরির পরীক্ষা বা ইন্টারভিউতেও শুনবেন — বুঝলেই চলবে।
এর একটি ব্যবহারিক শিক্ষাও আছে। Spread-কে একটি সাধারণ মান ভাবলে মানুষ cond ? ...a : ...b-এর মতো অসম্ভব জিনিস লিখতে চায়। সঠিক রূপ: ...(cond ? a : b) — বন্ধনীর ভেতরে একটি মান তৈরি হলো, তারপর spread. শর্তসাপেক্ষে property যোগ করার প্রচলিত কৌশল { ...(isAdmin && { role: "admin" }) }-ও এই কারণে কাজ করে: false spread করলে কিছুই যোগ হয় না।
মূল উপলব্ধি: ... একটি মান নয়, বরং array, argument বা object বানানোর ব্যাকরণের একটি অংশ। তাই এর অর্থ নির্ভর করে কোথায় বসেছে তার ওপর — আর সেজন্যই একই চিহ্ন কোথাও spread, কোথাও rest.
প্র ০৫Math.max(...hugeArray) কেন RangeError দিতে পারে? Spread-এর পারফরম্যান্স নিয়ে আর কী কী জানা দরকার?
Function কলে spread করলে engine array-এর প্রতিটি উপাদানকে একটি করে আলাদা argument বানায়। Argument-গুলো রাখা হয় call stack-এর frame-এ (পর্ব ০৫) — আর call stack-এর আকার সীমিত, সাধারণত এক মেগাবাইটের কাছাকাছি (engine আর সেটিংভেদে)। লাখো argument সেখানে ধরে না। তখন V8 দেয় RangeError: Maximum call stack size exceeded — একই বার্তা যা অসীম recursion-এ আসে, যদিও এখানে কোনো recursion নেই। এই পর্বের পরীক্ষায় Node.js-এ ১ লাখ সংখ্যা চলেছে আর ২ লাখে ভেঙেছে; অন্য ব্রাউজারে, অন্য সংস্করণে বা অন্য stack সেটিংয়ে সংখ্যাটি আলাদা হবে। মানদণ্ড কোনো নির্দিষ্ট সীমা বেঁধে দেয় না — তাই এই সীমার ওপর ভরসা করে কোড লেখা যায় না। পুরোনো Math.max.apply(null, arr)-এরও একই সমস্যা, আর arr.push(...other)-এরও — বড় array জুড়তে concat বা লুপ নিরাপদ।
দ্বিতীয় বিষয় হলো কপির খরচ। [...arr] বা { ...obj } প্রতিবার সব উপাদান কপি করে — n উপাদানে n কাজ। একবার করলে সমস্যা নেই, কিন্তু লুপের ভেতরে করলে বিপদ: items.reduce((acc, x) => [...acc, x * 2], []) প্রতিটি ধাপে বাড়তে থাকা পুরো array কপি করে। ১০ হাজার উপাদানে মোট কপি প্রায় ৫ কোটি (১ + ২ + … + ১০,০০০) — একটি O(n²) অ্যালগরিদম, যা দেখতে নিরীহ। Object-এ reduce((acc, x) => ({ ...acc, [x.id]: x }), {})-ও একই ফাঁদ; JavaScript সম্প্রদায়ে একে “reduce…spread anti-pattern” বলা হয়। সমাধান সহজ: map ব্যবহার করুন, বা reducer-এর ভেতরে একটি স্থানীয় array/object-এ push বা সরাসরি property বসান — যেটি আপনিই তৈরি করেছেন, বাইরের কেউ দেখে না, তাই তাকে বদলানো নিরাপদ।
তৃতীয়ত, engine-গুলো সাধারণ ক্ষেত্রে spread-কে বেশ দ্রুত করেছে — ছোট array কপিতে [...arr], arr.slice() আর Array.from(arr)-এর পার্থক্য সাধারণত নগণ্য। তাই পাঠযোগ্যতার জন্য spread বেছে নিন, আর পারফরম্যান্স নিয়ে মাথা ঘামান শুধু তখন, যখন মাপজোখ (পর্ব ৫৩) সমস্যা দেখায় বা ডেটার আকার অজানা ও বড়।
মূল উপলব্ধি: Spread দেখতে এক টুকরো ব্যাকরণ, কিন্তু ভেতরে লুপ চলে — function কলে সেটি stack-এ জায়গা নেয়, আর literal-এ প্রতিটি উপাদান কপি করে। ছোট ডেটায় এটি নিখুঁত, বড় বা অজানা আকারের ডেটায় হিসাব করে ব্যবহার করুন।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।
-
কোন variable কী পাবে?
const { a, b: { c = 5 } = {}, d: e = "x" } = { a: 1, b: { c: null }, d: undefined };—a,c,e-এর মান কী?bআরdনামে কি কোনো variable তৈরি হয়েছে?a = 1.c = null—bআছে বলে বাইরের= {}চলেনি, আরc-এর মানnull, যাundefinedনয়, তাই ৫ বসেনি।e = "x"—d-এর মানundefined, তাই default চলল, আর rename-এর কারণে মানটি গেলe-তে।bআরdশুধু উৎসের নাম/পথ, কোনো variable নয় — ব্যবহার করলে ReferenceError. -
Merge-এর ফল অনুমান করুন:
const base = { size: "M", color: "লাল" };— তাহলে{ color: "নীল", ...base },{ ...base, color: "নীল" }আর{ ...base, ...{ size: undefined } }— তিনটির ফল কী?প্রথমটি
{ color: 'লাল', size: 'M' }—colorপ্রথমে লেখা হয়েছিল বলে key-এর ক্রমে প্রথমে, কিন্তু পরে আসাbaseমানটি ঢেকে দিয়েছে। দ্বিতীয়টি{ size: 'M', color: 'নীল' }— শেষে লেখা নীল জিতেছে। তৃতীয়টি{ size: undefined, color: 'লাল' }—undefined-ও একটি মান, তাই M মুছে গেছে। নিয়ম একটাই: key-এর ক্রম প্রথম আবির্ভাবে, মান শেষ লেখায়। -
বাগ খুঁজুন: একজন লিখেছেন
function showProfile({ name, address: { district } }) { … }আর একটি অ্যাপে কোনো কোনো নতুন ব্যবহারকারীর পাতা খুললেই TypeError আসছে, আবার কখনোshowProfile()ডাকলেও। দুটি কারণ বলুন আর ঠিক করে লিখুন।(১) নতুন ব্যবহারকারীর
addressনেই বাnull— nested patternundefined/nullখুলতে গিয়ে TypeError. (২) argument ছাড়া ডাকলে পুরো parameterundefined, আর বাইরের= {}নেই। একটি সমাধান:function showProfile({ name = "অতিথি", address } = {}) { const district = address?.district ?? "অজানা"; … }— এক তলার destructuring, বাকিটা?.আর??, যাnull-ও সামলায়। শুধুaddress: { district } = {}লিখলেundefinedসামলাত, কিন্তুnullনয়। -
Immutable update লিখুন:
const state = { user: { name: "নীলা", prefs: { theme: "light" } }, cart: [] };— মূলstate-এর কিছু না বদলে এমন একটিnextবানান যেখানেthemeহবে"dark". তারপর যাচাই করুন:state.user.prefs.themeকী, আরnext.cart === state.cartকী দেয়?const next = { ...state, user: { ...state.user, prefs: { ...state.user.prefs, theme: "dark" } } };— বদলের পথের তিনটি object (state, user, prefs) নতুন।state.user.prefs.themeএখনো"light".next.cart === state.cartদেয়true— কার্ট বদলায়নি, তাই একই array ভাগাভাগি (structural sharing). শুধু{ ...state }করেnext.user.prefs.theme = "dark"লিখলে মূল state-ও বদলে যেত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJavaScript-এ const r = { ...a, ...b, x: 1 }; চলার সময় ঘটনাগুলো কোন ক্রমে ঘটে, সাজান (▲▼ চেপে বা টেনে)।
- 🆕 একটি নতুন, খালি object তৈরি হয়
- 📋 a-র নিজস্ব enumerable property-গুলো কপি হয়
- 🔀 b-র property কপি হয়, একই নামের মান ঢেকে দেয়
- ✏️ x-এর মান 1 লেখা হয়, আগের যেকোনো x ঢেকে
- 📦 তৈরি হওয়া object-টি r-এ রাখা হয়
JavaScript-এ const { name: customerName } = { name: "রহিম" }; চলার পর কোন কথাটি সঠিক?
- name নামের variable-এ "রহিম" রাখা হয়েছে
- শুধু customerName variable তৈরি হয়েছে, যার মান "রহিম"
- name আর customerName দুটি variable-ই তৈরি হয়েছে
- এটি SyntaxError দেয়
JavaScript-এ const [x = 5] = [null]; চলার পর x-এর মান কী?
- 5
- undefined
- null
- TypeError
JavaScript-এ const copy = { ...original }; লেখার পর copy.address.city = "খুলনা"; করলে কী হয়, যেখানে original.address একটি object?
original.address.city-ও "খুলনা" হয়ে যায়- শুধু copy বদলায়, original অক্ষত থাকে
- TypeError, কারণ spread করা object বদলানো যায় না
- copy.address undefined, কারণ spread nested object কপি করে না
JavaScript-এ function f({ a = 1 }) { return a; } — f() ডাকলে কী হয়?
- 1 ফেরত দেয়
- undefined ফেরত দেয়
- SyntaxError দেয়
- TypeError দেয়, কারণ undefined destructure করা যায় না
JavaScript-এ { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } }-এর ফল কী?
- { a: 1, b: 2, c: 4 }
- { a: 1, b: 3, c: 4 }
- { a: 1, b: 2, b: 3, c: 4 }
- TypeError, কারণ b দুই জায়গায় আছে
JavaScript-এ তিন ডট ... কোন ক্ষেত্রে rest হিসেবে কাজ করে (spread নয়)?
- Math.max(...prices)
- [...a, ...b]
- const [first, ...others] = list
- { ...defaults, theme: "dark" }
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।
মনে রাখুন
- Destructuring literal-এর আয়না: array মেলে অবস্থান দিয়ে, object মেলে নাম দিয়ে। কমা দিয়ে বাদ,
= মানদিয়ে default (শুধুundefined-এ),[a, b] = [b, a]দিয়ে swap. - Object pattern-এ কোলনের বাম পাশ উৎস, ডান পাশ গন্তব্য; nested-এর মাঝের নাম (
customer,address) variable হয় না।null/undefinedখুললে TypeError. - Parameter-এ options object:
function f({ qty = 1 } = {})— শেষের= {}ছাড়া খালি কলে TypeError. - একই
...: বানানোর জায়গায় spread (ছড়ায়), খোলার/নেওয়ার জায়গায় rest (জড়ো করে)। Object merge-এ পরেরটি জেতে,undefined-ও। - Spread shallow copy — ভেতরের object ভাগাভাগি। বদলের পথের প্রতিটি তলায় spread করুন, বা পুরো গভীর কপির জন্য
structuredClone(পর্ব ১৯)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৮ · Map, Set, WeakMap আর WeakRefপরবর্তী পর্বObject-এর বাইরে আরও দুটি collection আর weak reference.
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python-এর tuple আর unpackingতুলনা করুন
a, b = b, aআর*rest— destructuring-এর Python রূপ। - C++-এর enum, struct আর structured bindingsতুলনা করুনType-নির্দিষ্ট জগতে
auto [x, y]দিয়ে খোলা। - Java-র record আর record patternপাশের ট্র্যাকImmutable ডেটা আর pattern দিয়ে খোলার Java রূপ।