JavaScript-এ অ্যারে নিয়ে কাজ করার মানে এখন আর খুব একটা for লুপ লেখা নয়। ভাষাটি এখন আপনাকে এমন কিছু প্রকাশভঙ্গি সম্পন্ন (expressive) টুলস দেয় যা জটিল ও অগোছালো ডেটাকে পরিষ্কার এবং পঠনযোগ্য রূপান্তরে পরিণত করে। এই গাইডটি বেসিক বা প্রাথমিক বিষয়গুলোর পরের ধাপগুলো নিয়ে আলোচনা করবে—ফ্ল্যাটেনিং (flattening), রিডিউসিং (reducing), কুয়েরি করা এবং নতুন ইমিউটেবল (immutable) প্যাটার্নগুলোর ওপর গুরুত্ব দেবে যা আপনার মূল ডেটাকে অপরিবর্তিত রাখে।
flat() এবং flatMap() দিয়ে নেস্টেড স্ট্রাকচার ফ্ল্যাটেন করা
বাস্তব জগতের ডেটা প্রায়ই নেস্টেড বা স্তরে স্তরে সাজানো থাকে। কোনো API থেকে রেকর্ড আনা, একটি CSV পার্স করা বা কনফিগারেশন ফাইল পড়া—আপনি প্রায়ই অ্যারের ভেতরে অ্যারে পাবেন। flat() ব্যবহার করা হয় সেই স্তরগুলোকে একটি একক রৈখিক তালিকায় (linear list) নিয়ে আসার জন্য।
একটি নেস্টেড অ্যারের ওপর flat() কল করলে এটি ডিফল্টভাবে একটি স্তর পর্যন্ত ফ্ল্যাটেন করে:
const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]
মূল nested অ্যারেটি ঠিক আগের মতোই থাকে। যদি আপনার নেস্টিং আরও গভীর হয়, তবে flat(2) বা স্তর সংখ্যা না জানলে flat(Infinity) এর মতো একটি ডেপথ (depth) আর্গুমেন্ট পাস করুন। এটি হাতে লিখে রিকার্সিভ হেল্পার ফাংশন লেখার চেয়ে অনেক সহজ।
যখন আপনার এলিমেন্টগুলোকে ট্রান্সফর্ম করা এবং একই সাথে ফলাফল ফ্ল্যাটেন করার প্রয়োজন হয়, তখন flatMap() একটি মাত্র পাসে (pass) উভয় কাজই করে। এটি প্রতিটি এলিমেন্টের ওপর একটি ম্যাপিং ফাংশন চালায় এবং তারপর রিটার্ন করা অ্যারেগুলোকে এক স্তর পর্যন্ত ফ্ল্যাটেন করে। আলাদাভাবে .map().flat() কল করে এটি করা সম্ভব, তবে flatMap() মধ্যবর্তী অ্যারেটি এড়িয়ে চলে, যা মেমরি বাঁচায় এবং দ্বিতীয়বার ইটারেশন করার প্রয়োজন কমিয়ে দেয়। একটি ক্লাসিক উদাহরণ হলো বাক্যগুলোর একটি অ্যারে থেকে আলাদা আলাদা শব্দ বের করা:
const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']
ফিল্টারিং এবং ম্যাপিং: পার্থক্যটি জেনে নিন
filter() একটি নতুন অ্যারে তৈরি করে যাতে শুধুমাত্র সেই আইটেমগুলো থাকে যা আপনার টেস্ট বা শর্ত পূরণ করে। এলিমেন্টটি রাখতে হলে কলব্যাক ফাংশনকে একটি truthy ভ্যালু রিটার্ন করতে হবে, আর বাদ দিতে চাইলে একটি falsy ভ্যালু। এটি কখনোই সোর্স অ্যারেটিকে পরিবর্তন (mutate) করে না।
const users = [
{ name: 'Ajay', active: true },
{ name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);
filter() এবং map() এর মধ্যে গুলিয়ে ফেলা সহজ কারণ উভয়ই নতুন অ্যারে রিটার্ন করে এবং কলব্যাক গ্রহণ করে। পার্থক্যটি হলো তাদের কাজের চুক্তিতে (contract)। map() একই দৈর্ঘ্যের একটি নতুন অ্যারে রিটার্ন করে। প্রতিটি এলিমেন্টকে ট্রান্সফর্ম করুন বা একটি কন্ডিশনাল চেক চালান, আপনি প্রতিটি স্লটের জন্য true বা false দেখতে পাবেন, কারণ map() কখনোই আইটেম বাদ দেয় না—এটি কেবল সেগুলোকে পরিবর্তন করে। অন্যদিকে, filter() একটি সম্ভাব্য ছোট অ্যারে রিটার্ন করে যাতে শুধুমাত্র সেই এলিমেন্টগুলো থাকে যা আপনার টেস্ট পাস করেছে। আপনি যদি ডেটা রিমুভ করতে চান, তবে filter() ব্যবহার করুন। আর যদি ডেটার সংখ্যা ঠিক রেখে প্রতিটি ডেটাকে ট্রান্সফর্ম করতে চান, তবে map() ব্যবহার করুন।
reduce() দিয়ে অ্যারে সংকুচিত করা
যেখানে map() এবং filter() অ্যারে রিটার্ন করে, সেখানে reduce() একটি সম্পূর্ণ লিস্টকে একটি মাত্র মানে (value) কমিয়ে আনে। এটি বাম থেকে ডানে চলে এবং প্রতিটি ধাপে একটি অ্যাকুমুলেটর (accumulator) বহন করে।
const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60
দ্বিতীয় আর্গুমেন্টটি—উপরের উদাহরণে 0—হলো প্রাথমিক অ্যাকুমুলেটর ভ্যালু। এটি বাদ দিলে JavaScript প্রথম এলিমেন্টটিকে শুরুর পয়েন্ট হিসেবে ধরে নেয় এবং ইনডেক্স ১ থেকে ইটারেশন শুরু করে। অ্যারেটি খালি হলে এটি মানুষকে বিভ্রান্ত করতে পারে, কারণ একটি খালি অ্যারের ওপর প্রাথমিক ভ্যালু ছাড়া reduce() কল করলে TypeError থ্রো করে।
reduce শুধুমাত্র গাণিতিক কাজের জন্য নয়। আপনি এটি ফ্রিকোয়েন্সি টেবিল তৈরি করতে, অবজেক্ট মার্জ করতে, সর্বোচ্চ মান খুঁজে পেতে বা একাধিক ট্রান্সফরমেশনের মাধ্যমে ডেটা পাস করতে ব্যবহার করতে পারেন। এটি অত্যন্ত নমনীয় হওয়ায় কলব্যাক ফাংশনটি ছোট রাখা এবং ভেরিয়েবলগুলোর নাম স্পষ্টভাবে দেওয়া বুদ্ধিমানের কাজ। একটি জটিল reduce() ব্লক তার পরিবর্তে ব্যবহৃত লুপের চেয়েও দ্রুত পড়া কঠিন হয়ে উঠতে পারে।
বুলিয়ান কুয়েরি: every() এবং some()
মাঝে মাঝে আপনার নতুন অ্যারের প্রয়োজন হয় না; বরং সংগ্রহের বিষয়ে একটি হ্যাঁ বা না উত্তর প্রয়োজন হয়। every() শুধুমাত্র তখনই true রিটার্ন করে যদি সবগুলো এলিমেন্ট আপনার শর্ত পূরণ করে। যদি একদম প্রথম এলিমেন্টটিই শর্ত পূরণ না করে, তবে এটি সাথে সাথে থেমে যায় এবং false রিটার্ন করে।
const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);
some() হলো এর ঠিক উল্টো। এটি true রিটার্ন করে যদি অন্তত একটি এলিমেন্ট টেস্ট পাস করে। কোনো ম্যাচ পাওয়া গেলেই এটি সাথে সাথে থেমে যায়।
const hasPerfectScore = scores.some(s => s === 100);
ভ্যালিডেশন চেকের জন্য এই মেথডগুলো ম্যানুয়াল লুপের বিকল্প হিসেবে কাজ করে। প্রতিটি ফর্ম ফিল্ড পূরণ করা হয়েছে কিনা তা নিশ্চিত করতে চান? every() ব্যবহার করুন। কোনো ইউজারের অ্যাডমিন রাইটস আছে কিনা তা জানতে চান? some() ব্যবহার করুন।
মিউটেশন ছাড়াই ইন্সপেকশন এবং আপডেট করা
আধুনিক JavaScript ইমিউটেবিলিটির (immutability) ওপর অনেক বেশি গুরুত্ব দেয়। এই মেথডগুলো আপনাকে মূল ডেটা স্পর্শ না করেই অ্যারে ইন্সপেকশন বা আপডেট করতে সাহায্য করে।
keys() একটি ইটারেটর অবজেক্ট রিটার্ন করে যাতে অ্যারের ইনডেক্সগুলো থাকে। entries() রিটার্ন করে [index, value] জোড়াগুলোর একটি ইটারেটর। যখন আপনি ক্লিন ডিস্ট্রাকচারিং (destructuring) করতে চান, তখন for...of লুপে এগুলো দারুণ কাজ করে:
const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
console.log(index, fruit);
}
with() হলো ভাষার একটি নতুন সংযোজন যা একটি নির্দিষ্ট ইনডেক্সে একটি এলিমেন্ট পরিবর্তন করে এবং একটি সম্পূর্ণ নতুন অ্যারে রিটার্ন করে। এটি ব্র্যাকেট অ্যাসাইনমেন্ট—arr[index] = value—এর মতোই কাজ করে, তবে পার্থক্য হলো এটি মূল অ্যারেটিকে অপরিবর্তিত রাখে। React-এর মতো ফ্রেমওয়ার্কের ক্ষেত্রে, যেখানে সরাসরি মিউটেশন (direct mutation) চেঞ্জ ডিটেকশন (change detection) নষ্ট করে দেয়, সেখানে with() ব্যবহার করা অনেক বেশি নিরাপদ:
const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated: [10, 99, 30]
Spread and Rest: ...-এর দুটি দিক
spread syntax এবং rest parameter একই তিনটি ডট ব্যবহার করে, কিন্তু তারা ডেটাকে বিপরীত দিকে নিয়ে যায়।
একটি অ্যারেকে আলাদা আলাদা এলিমেন্টে প্রসারিত করতে spread operator ব্যবহার করুন। অ্যারে মার্জ (merge) বা ক্লোন (clone) করার এটিই সবচেয়ে পরিচ্ছন্ন উপায়:
const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];
const copy = [...merged];
আপনি সহজেই অতিরিক্ত আইটেমও যোগ করতে পারেন: [0, ...front, 99, ...back]।
rest parameter একাধিক এলিমেন্টকে একটি একক অ্যারেতে সংগ্রহ করে। একটি ফাংশন সিগনেচারের (function signature) ভেতরে, এটি বাকি থাকা সমস্ত আর্গুমেন্ট সংগ্রহ করে:
function logFirst(first, ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']
একত্রে তারা ভেরিয়াডিক ইনপুট (variadic input) গ্রহণ করা, সেটিকে একটি অ্যারে হিসেবে প্রসেস করা এবং তারপর ফাংশন কল বা নতুন ডেটা স্ট্রাকচারে পুনরায় ছড়িয়ে দেওয়ার মতো সাধারণ কাজগুলো সম্পন্ন করে।
আসল সারমর্ম
এই মেথডগুলোর লক্ষ্য ওয়ান-লাইনার দিয়ে সহকর্মীদের প্রভাবিত করা নয়। বরং এর লক্ষ্য হলো আপনার উদ্দেশ্য স্পষ্ট করা। যখন অন্য কোনো ডেভেলপার filter() দেখে, তারা বোঝেন আপনি আইটেমগুলো সরিয়ে ফেলছেন। যখন তারা reduce() দেখে, তারা বোঝেন আপনি ডেটাকে সংকুচিত (collapsing) করছেন। যখন তারা with() দেখে, তারা বোঝেন মূল অ্যারেটি কোথাও অক্ষত আছে। এই টুলগুলো আয়ত্ত করার অর্থ হলো এমন কোড লেখা যা নিজেই নিজের ব্যাখ্যা দেয়, যা যেকোনো চতুর লুপের চেয়ে অনেক বেশি সময় বাঁচায়।
