সরাসরি পর্বে যান
পর্ব ০৭ · ৫৯-এর মধ্যে · মডিউল ২

let, const, var — scope, hoisting আর TDZ

Variables in JavaScript — let, const, var, Scope, Hoisting & the Temporal Dead Zone

Taught in Bangla, this lesson covers JavaScript variables as name-to-value bindings: let, const and var, why const freezes the binding but not the value, naming conventions, block, function and global scope, globalThis and var's hidden window properties, hoisting and the temporal dead zone, the classic var-in-a-loop bug, and a const-first habit.

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

  • Variable আসলে কী — ঘোষণা, মান বসানো, আর const কেন “মান স্থির” নয়, “বাঁধন স্থির”
  • নাম রাখার নিয়ম ও রীতি — camelCase, UPPER_CASE ধ্রুবক, reserved word
  • Block, function, module আর global scope; scope chain আর shadowing
  • Global object (window/globalThis) আর top-level var-এর লুকোনো প্রভাব
  • Hoisting আর temporal dead zone, লুপে var-এর বাগ, আর আধুনিক কোডের ভালো অভ্যাস

১ · Variable আসলে কী — একটি নাম, একটি মান, মাঝখানে বাঁধন

পর্ব ০৬-এ আপনি দেখেছেন JavaScript-এ কী কী ধরনের মান আছে — number, string, boolean, object ইত্যাদি। কিন্তু একটি মান তৈরি করে ফেলে রাখলে তো আবার খুঁজে পাওয়া যায় না। মানকে পরে ব্যবহার করতে হলে তার একটি নাম দিতে হয়। এই নামটিই variable.

অনেক বইয়ে variable-কে “একটি বাক্স, যার ভেতরে মান রাখা হয়” বলা হয়। শুরুর জন্য ছবিটা চলে, কিন্তু JavaScript-এ এর চেয়ে নির্ভুল ছবি হলো নামের ট্যাগ। মানগুলো মেমরিতে আলাদা থাকে; variable হলো একটি ট্যাগ, যা সুতো দিয়ে একটি মানের সাথে বাঁধা। এই নাম-মানের সংযোগকে বলে binding বা বাঁধন। “Variable-এ নতুন মান বসানো” মানে ট্যাগের সুতো খুলে অন্য মানের সাথে বাঁধা — পুরোনো মানটি নিজে বদলায় না।

আপনার ফোনের contact list ভাবুন। “আম্মু” নামটি একটি নম্বরের সাথে যুক্ত। আম্মু নতুন সিম নিলে আপনি নামটি রেখে নম্বর বদলে দেন — এটাই let-এর নতুন মান বসানো। আবার, “অফিস হেল্পলাইন” নামটি এমনভাবে লক করা যে নম্বর বদলানো যায় না — এটা const. কিন্তু খেয়াল করুন, লক করা হয়েছে নাম আর নম্বরের সংযোগ, ওই নম্বরের পেছনের অফিসটি নয়; অফিসে নতুন লোক আসতেই পারে। const-ও ঠিক এমন।

ঘোষণা আর মান বসানো

একটি নতুন নাম পরিচয় করিয়ে দেওয়াকে বলে declaration বা ঘোষণা। আধুনিক JavaScript-এ ঘোষণার দুটি প্রধান keyword:

let score;              // ঘোষণা — মান এখনো undefined
score = 10;             // প্রথম মান বসানো (initialization)
score = score + 5;      // নতুন মান বসানো (reassignment) — let-এ চলে

const VAT_RATE = 0.15;  // const-এ ঘোষণা আর মান একসাথে দিতেই হবে
let cartTotal = 0, itemCount = 0;   // এক লাইনে একাধিক

এই ছোট টুকরোটি চালানোর জন্য নয়, শুধু গঠন দেখানোর জন্য। let দিয়ে ঘোষণা করলে মান না দিলেও চলে — তখন মান থাকে undefined, আর পরে যতবার খুশি নতুন মান বসানো যায়। const দিয়ে ঘোষণা করলে সাথে সাথে মান দিতে হয়; const x; লিখলে parser-ই আটকে দেয়: SyntaxError: Missing initializer in const declaration (V8-এর বার্তা, Node.js আর Chrome/Edge-এ একই)। আর একবার বাঁধা হয়ে গেলে নতুন মান বসাতে চাইলে চলার সময় আসে TypeError: Assignment to constant variable.

আর তৃতীয় একটি keyword আছে — var. ১৯৯৫ সাল থেকে ২০১৫ পর্যন্ত JavaScript-এ variable বানানোর একমাত্র উপায় ছিল এটিই। ES2015 (ES6) থেকে let আর const এসেছে, কারণ var-এর কিছু আচরণ এক প্রজন্মের প্রোগ্রামারকে বাগে ভুগিয়েছে। এই পর্বের বড় অংশ জুড়ে দেখবেন সেই আচরণগুলো কী, আর কেন। পুরোনো কোড, পুরোনো টিউটোরিয়াল আর AI-এর দেওয়া কোডে var এখনো প্রচুর দেখবেন — তাই এটি পড়তে জানতেই হবে, লিখতে নয়।

বৈশিষ্ট্যconstletvar (পুরোনো)
নতুন মান বসানো যায়?না (TypeError)হ্যাঁহ্যাঁ
Scopeblockblockfunction
ঘোষণার আগে ব্যবহার করলেReferenceError (TDZ)ReferenceError (TDZ)undefined, কোনো error নয়
একই scope-এ একই নামে আবার ঘোষণাSyntaxErrorSyntaxErrorচুপচাপ চলে
Script-এর top level-এ window-এর property হয়?নানাহ্যাঁ

২ · const মানে “বাঁধন স্থির” — মান বরফ নয়

নতুনদের সবচেয়ে সাধারণ ভুল ধারণা: “const দিয়ে বানানো জিনিস আর বদলানো যায় না।” এটি আধা-সত্য। নিচের কোডটি কোনো error ছাড়াই চলে:

const cart = [];               // cart নামটি একটি array-এর সাথে বাঁধা
cart.push({ item: "চাল", price: 650 });   // ✔ array-এর ভেতর বদলানো
cart[0].price = 620;           // ✔ ভেতরের object বদলানো

const shop = { name: "রহিম স্টোর" };
shop.name = "করিম স্টোর";      // ✔ property বদলানো

cart = [];                     // ✘ TypeError — নামটিকে নতুন array-এর সাথে বাঁধা যাবে না

const cart বলে: “cart নামটি সবসময় এই array-টিকেই দেখাবে।” Array-এর ভেতরে জিনিস যোগ করা, মোছা বা বদলানোকে বলে mutation — এতে নামটি তো একই array-কেই দেখাচ্ছে, তাই const-এর আপত্তি নেই। শুধু শেষ লাইনে নামটিকে অন্য একটি array-এর দিকে ঘোরাতে চাওয়া হয়েছে — সেটিই নিষেধ। Number বা string-এর মতো primitive মান নিজেই বদলানো যায় না (পর্ব ০৬), তাই primitive-এর ক্ষেত্রে const কার্যত পুরো স্থির।

তাহলে object-কে সত্যিই বদলের অযোগ্য করতে চাইলে? তার জন্য আছে Object.freeze() (ES5, ২০০৯ থেকে)। Freeze করা object-এ নতুন property যোগ, মোছা বা মান বদলানো যায় না। Strict mode-এ চেষ্টা করলে আসে TypeError: Cannot assign to read only property 'a' of object '#<Object>'; strict mode ছাড়া চেষ্টাটি নীরবে ব্যর্থ হয় — কোনো error নেই, মানও বদলায় না, যা আরও বিপজ্জনক। আর মনে রাখুন, freeze হলো shallow: শুধু ওপরের স্তর জমে, ভেতরের object আলাদা করে freeze না করলে সেটি বদলানো যায়। Object আর reference নিয়ে বিস্তারিত পর্ব ১৪-এ।

মূল ধারণা

const নিশ্চয়তা দেয় নামটি নিয়ে — “এই নাম সবসময় একই জিনিসকে দেখাবে।” জিনিসটির ভেতরটা নিয়ে কোনো নিশ্চয়তা দেয় না। পুরো object অপরিবর্তনীয় চাইলে Object.freeze(), আর ভেতরের স্তরের জন্য গভীরে গিয়ে freeze, বা মূল object না বদলে নতুন কপি বানানোর অভ্যাস (পর্ব ১৭, ১৯)।

৩ · নাম রাখার নিয়ম আর রীতি

Variable-এর নামকে বলে identifier. এর কিছু নিয়ম আছে, যা না মানলে কোড চলবেই না, আর কিছু রীতি, যা না মানলে কোড চলবে কিন্তু অন্য প্রোগ্রামার (আর ছয় মাস পরের আপনি) বিরক্ত হবেন।

📏 নিয়ম — ভাঙলে SyntaxError

অক্ষর, _ বা $ দিয়ে শুরু; পরে অক্ষর, অঙ্ক, _, $. অঙ্ক দিয়ে শুরু নয় (2nd ✘), মাঝে space বা - নয় (cart-total ✘)। বড়-ছোট হাতের অক্ষর আলাদা: total আর Total দুটি ভিন্ন নাম।

reserved word নাম হবে না: if, for, class, const…

✨ রীতি — ভালো কোডের ভাষা

সাধারণ variable আর function: camelCase — cartTotal, getUserName. Class: PascalCase — ShoppingCart. আগে থেকে জানা, কখনো না বদলানো ধ্রুবক: UPPER_SNAKE_CASE — VAT_RATE, MAX_RETRY.

boolean-এর নাম প্রশ্নের মতো: isPaid, hasStock

একটি সূক্ষ্ম বিষয়: const দিয়ে ঘোষণা করা সব কিছুই UPPER_CASE নয়। const cart = [] বা const user = getUser() — এগুলো সাধারণ camelCase, কারণ এদের মান চলার সময় তৈরি হয়। UPPER_CASE রাখা হয় শুধু সেই মানের জন্য যা কোড লেখার সময়েই জানা আর পুরো প্রোগ্রামে একই থাকে — যেমন বাংলাদেশে বহু পণ্যে প্রযোজ্য ১৫% VAT হার, const VAT_RATE = 0.15. (হারটি এখানে শুধু উদাহরণ; আসল প্রজেক্টে করের হার আইন অনুযায়ী যাচাই করে নেবেন।)

আরেকটি মজার তথ্য: JavaScript-এর identifier-এ Unicode অক্ষর চলে, তাই const দাম = 650; সম্পূর্ণ বৈধ কোড — চালালে ঠিকই চলে। তবে বাস্তব প্রজেক্টে নাম ইংরেজিতেই রাখা রীতি: কীবোর্ডে টাইপ করা সহজ, সব টুল ও দলের সবাই পড়তে পারে। বাংলা থাকুক string-এ আর comment-এ।

কিছু শব্দ শুধু বিশেষ জায়গায় সংরক্ষিত। যেমন sloppy mode-এ let নামের একটি var বানানো যায় (var let = 2 চলে!), কিন্তু strict mode-এ যায় না। এসব ফাঁকফোকর পুরোনো কোড না ভাঙার জন্য রাখা হয়েছে — আপনি কখনো এগুলো ব্যবহার করবেন না। নিয়ম সহজ: কোনো keyword-এর মতো দেখতে নাম রাখবেন না।

৪ · Scope — কোন নাম কোথা থেকে দেখা যায়

একটি বড় প্রোগ্রামে হাজার হাজার নাম থাকে। সব নাম যদি সব জায়গা থেকে দেখা যেত, তাহলে এক function-এর total আরেক function-এর total-কে নষ্ট করে দিত। তাই প্রতিটি নামের একটি এলাকা আছে, যার বাইরে সেটি অদৃশ্য। এই এলাকাকে বলে scope.

একটি বাড়ির কথা ভাবুন। নিজের ঘরের আলমারিতে রাখা জিনিস শুধু আপনি দেখেন। বসার ঘরের টিভি বাড়ির সবাই দেখে। আর মহল্লার মোড়ের ল্যাম্পপোস্ট সব বাড়ির মানুষ দেখে। কোনো কিছু খুঁজতে হলে আপনি আগে নিজের ঘরে দেখেন, না পেলে বসার ঘরে, তারপর মহল্লায় — উল্টো দিকে নয়। পাশের বাড়ির ভেতরের ঘরে আপনি ঢুকতে পারেন না। Scope-ও ঠিক এভাবে স্তরে স্তরে সাজানো।

JavaScript-এ চার রকম scope আছে:

  • Block scope: যেকোনো { } — if, for, while-এর শরীর, বা শুধু একজোড়া বন্ধনী। let, const আর class এখানে আটকে থাকে।
  • Function scope: একটি function-এর পুরো শরীর। Parameter আর var দিয়ে ঘোষিত নাম এখানে থাকে — var ভেতরের কোনো block-এ লেখা হলেও।
  • Module scope: একটি ES module ফাইলের top level. এখানকার নাম শুধু ওই ফাইলে দেখা যায়, export না করলে বাইরে যায় না (পর্ব ৪৫)।
  • Global scope: সবচেয়ে বাইরের স্তর। এখানকার নাম সব জায়গা থেকে দেখা যায় — সুবিধাজনক, কিন্তু ঠিক এজন্যই বিপজ্জনক।

Scope chain — নাম খোঁজার পথ

কোড যখন কোনো নাম ব্যবহার করে, engine সেটি খোঁজে একটি নির্দিষ্ট পথে: আগে চলতি scope-এ, না পেলে তাকে ঘিরে থাকা scope-এ, এভাবে একদম global পর্যন্ত। এই পথকে বলে scope chain. প্রথম যেখানে নামটি পায়, সেখানেই খোঁজা থামে। কোথাও না পেলে ReferenceError. নিচে একটি দোকানের হিসাবের function-এ চারটি নাম খোঁজা দেখুন:

1const VAT_RATE = 0.15;2function checkout(prices) {3  let total = 0;4  for (const p of prices) {5    total += p + p * VAT_RATE;6  }7  return total + deliveryFee;8}
খোঁজার পথ: block → function → global
খুঁজছি
① for-এর block
② checkout-এর scope
③ global / module
ফল
  1. সবচেয়ে কাছের scope আগে। লাইন ৫-এ p দরকার। Engine প্রথমে চলতি scope — for-এর block — দেখে। const p এখানেই ঘোষিত, তাই খোঁজা সাথে সাথে শেষ। প্রতিটি পুনরাবৃত্তিতে এই block নতুন করে তৈরি হয়, তাই p-ও প্রতিবার নতুন।
  2. এক স্তর বাইরে। একই লাইনে total. Block-এ নেই, তাই engine এক স্তর বাইরে — checkout-এর function scope-এ যায়। লাইন ৩-এর let total পাওয়া গেল। ভেতর থেকে বাইরের variable শুধু পড়াই নয়, বদলানোও যায় — += ঠিক তাই করছে।
  3. একদম বাইরে পর্যন্ত। VAT_RATE block-এ নেই, function-এ নেই। শেষ স্তর global (বা ফাইলটি module হলে module scope) — লাইন ১-এ পাওয়া গেল। ভেতরের কোড বাইরের সব স্তর দেখতে পায়।
  4. কোথাও নেই → ReferenceError. লাইন ৭-এ deliveryFee. Function-এ নেই, global-এও নেই — পথ শেষ। Engine থেমে বলে ReferenceError: deliveryFee is not defined. খেয়াল করুন, খোঁজা শুধু ভেতর থেকে বাইরে যায়: global কোড কখনো total বা p দেখতে পায় না।

Shadowing — ভেতরের নাম বাইরেরটিকে আড়াল করে

ভেতরের scope-এ যদি বাইরের একটি নামের সমান নাম ঘোষণা করা হয়, তাহলে খোঁজা ভেতরেই থেমে যায় — বাইরের variable-টি সেই এলাকায় আড়ালে পড়ে যায়। একে বলে shadowing:

const discount = 50;           // বাইরের discount

function eidPrice(price) {
  const discount = 200;        // ভেতরের discount — বাইরেরটিকে আড়াল করে
  return price - discount;     // এখানে 200
}

eidPrice(1000);                // 800
discount;                      // 50 — বাইরেরটি অক্ষত

দুটি আলাদা variable, শুধু নাম এক। eidPrice-এর ভেতরে discount খুঁজলে প্রথমেই ভেতরেরটি পাওয়া যায়, তাই বাইরে যাওয়ার প্রশ্নই আসে না। Shadowing বৈধ এবং কখনো কাজের, কিন্তু অনেক সময় বাগের উৎস — আপনি ভাবছেন বাইরের মান বদলাচ্ছেন, আসলে বদলাচ্ছেন ভেতরেরটি। ESLint-এর no-shadow নিয়ম এজন্যই আছে (পর্ব ৪৬)।

Lexical scope — কোথায় লেখা, সেটাই আসল

একটি গুরুত্বপূর্ণ প্রশ্ন: function-এর ভেতরে বাইরের কোন discount দেখা যাবে — যেখানে function লেখা হয়েছে তার চারপাশেরটি, না যেখান থেকে ডাকা হয়েছে তার চারপাশেরটি? JavaScript-এর উত্তর: যেখানে লেখা হয়েছে। একে বলে lexical scope (বা static scope). কোডের দিকে তাকিয়েই আপনি বলে দিতে পারেন কোন নাম কোন variable-কে বোঝায় — প্রোগ্রাম চালাতে হয় না। এই নিয়মটিই closure-এর ভিত্তি: একটি function তার জন্মস্থানের scope সাথে নিয়ে ঘোরে, অন্য কোথাও চললেও। Closure-এর পুরো গল্প পর্ব ২২-এ; এই পর্বের ৭ নম্বর অংশে তার প্রথম ঝলক দেখবেন।

৫ · Global object — window, globalThis আর var-এর লুকোনো property

প্রতিটি runtime একটি বিশেষ object দেয়, যাকে বলে global object. এর property-গুলো সব জায়গা থেকে শুধু নাম দিয়েই পাওয়া যায়। ব্রাউজারে এর নাম window — তাই setTimeout আর window.setTimeout একই জিনিস। Node.js-এ এর নাম global, Web Worker-এ self. সব জায়গায় চলে এমন কোড লেখার জন্য ES2020 যোগ করেছে একটি সাধারণ নাম: globalThis.

এখানেই var-এর একটি পুরোনো অভ্যাস চোখে পড়ে। ব্রাউজারের সাধারণ (classic) <script>-এর top level-এ var দিয়ে ঘোষণা করলে সেটি global object-এর একটি property-ও হয়ে যায়। let আর const top level-এ global scope-এ থাকে ঠিকই — অন্য script থেকেও দেখা যায় — কিন্তু window-এর property হয় না।

কোড কোথায় চলছেTop-level var xTop-level let x / const x
ব্রাউজারের classic <script> (এই কোর্সের ইন-পেজ runner-ও)window.x তৈরি হয়global, কিন্তু window-এ নয়
<script type="module">শুধু module-এর ভেতরেশুধু module-এর ভেতরে
Node.js-এর সাধারণ ফাইল (CommonJS)শুধু ফাইলের ভেতরেশুধু ফাইলের ভেতরে

Module আর Node.js-এর ফাইল নিজেই একটি আলাদা scope-এ মোড়া থাকে, তাই সেখানে var-ও global object-এ পৌঁছায় না। নিচের প্রথম runnable উদাহরণে আপনি এই পার্থক্য নিজের চোখে দেখবেন।

কেন এটি বিপদ: window-এ আগে থেকেই শত শত property আছে — name, status, top, parent, length… একটি classic script-এর top level-এ var name = 42; লিখলে আপনি আসলে window.name-এ লিখছেন, যা ব্রাউজার সবসময় string বানিয়ে রাখে — পরে typeof name দেখাবে "string". আর var top = 5 লিখলে কিছুই বদলায় না, কারণ window.top বদলানো যায় না। এমন বাগ খুঁজে পেতে ঘণ্টার পর ঘণ্টা যায়।

ঘোষণা ছাড়া variable — accidental global

আরও খারাপ একটি পুরোনো আচরণ: কোনো ঘোষণা ছাড়াই একটি নামে মান বসালে, আর নামটি কোথাও না পাওয়া গেলে, sloppy mode-এ JavaScript চুপচাপ global object-এ একটি নতুন property বানিয়ে দেয়। একে বলে implicit global বা accidental global.

function addToCart(price) {
  totl = price;      // টাইপো! total নয়, totl — কোনো ঘোষণা নেই
}
addToCart(500);
console.log(window.totl);   // sloppy mode-এ 500 — function থেকে global-এ ফাঁস হয়ে গেল

একটি ছোট টাইপো, আর কোনো error ছাড়াই একটি global variable তৈরি। দুটি আলাদা function একই ভুল নামে লিখলে একে অপরের ডেটা নষ্ট করবে। পর্ব ০৫-এ দেখেছেন strict mode; সেখানে ঠিক এই কাজটিতেই আসে ReferenceError: totl is not defined. ES module আর class-এর কোড নিজে থেকেই strict, তাই আধুনিক কোডে এই ফাঁদ অনেক কম। দ্বিতীয় runnable উদাহরণে এটিও চালিয়ে দেখবেন।

৬ · Hoisting আর TDZ — কোড চলার আগেই নামগুলো তৈরি

পর্ব ০৫-এ দেখেছেন, একটি function ডাকা হলে engine তার জন্য একটি execution context তৈরি করে। এখন সেই কথার ভেতরে একটু ঢুকি। একটি scope-এর কোড চালানো দুই ধাপে হয়:

  1. তৈরির ধাপ (creation phase): একটি লাইনও চলার আগে engine পুরো scope-টি দেখে নেয় এবং সেখানে ঘোষিত প্রতিটি নাম নিবন্ধন করে।
  2. চলার ধাপ (execution phase): এরপর কোড ওপর থেকে নিচে লাইন ধরে চলে।

তৈরির ধাপে কী হয়, তা নির্ভর করে ঘোষণার ধরনের ওপর:

  • Function declaration (function greet() {}): পুরোপুরি তৈরি — ঘোষণার আগের লাইন থেকেই ডাকা যায়।
  • var: নাম তৈরি, মান undefined. আগে পড়লে কোনো error নেই, শুধু undefined.
  • let, const, class: নাম তৈরি, কিন্তু “অপ্রস্তুত” (uninitialized) অবস্থায়। ঘোষণার লাইন না চলা পর্যন্ত একে পড়া বা লেখা যায় না।

কোড দেখে মনে হয় যেন ঘোষণাগুলো scope-এর মাথায় তুলে আনা হয়েছে — তাই এর নাম hoisting (hoist মানে টেনে ওপরে তোলা, যেমন পতাকা)। আসলে কোড কোথাও সরে না; শুধু নামগুলো আগেভাগে নিবন্ধিত হয়। আর scope শুরু হওয়া থেকে let/const-এর লাইন চলা পর্যন্ত যে সময়ে নামটি আছে কিন্তু ছোঁয়া যায় না, তার নাম temporal dead zone (TDZ). “Temporal” মানে সময়গত — এটি কোডের জায়গা দিয়ে নয়, সময় দিয়ে মাপা।

1console.log(greet());2console.log(count);3var count = 1;4let total = 10;5console.log(total);6function greet() { return "হ্যালো"; }
Scope-এর নামগুলোর অবস্থা
ধাপ
greet
count (var)
total (let)
  1. তৈরির ধাপ — একটি লাইনও চলেনি। Engine পুরো scope দেখে তিনটি ঘোষণা পেল (লাইন ৩, ৪, ৬)। greet পুরো function হিসেবে তৈরি, count-এ বসল undefined, আর total নিবন্ধিত হলো কিন্তু অপ্রস্তুত — TDZ শুরু।
  2. লাইন ১ — ঘোষণার আগেই function কল। greet লেখা আছে একদম শেষে, তবু ডাকা গেল, ছাপা হলো হ্যালো. Function declaration পুরোপুরি hoist হয় — তাই অনেকে ফাইলের ওপরে মূল কাজ আর নিচে সাহায্যকারী function রাখেন।
  3. লাইন ২ — var-এর নীরব undefined. count-এর মান এখনো বসেনি, কিন্তু error নেই — ছাপা হলো undefined. এটাই var-এর বিপদ: ভুলটা চুপচাপ এগিয়ে যায়। এই লাইনে যদি total পড়তে চাইতেন, পেতেন ReferenceError: Cannot access 'total' before initialization.
  4. লাইন ৩ — var-এ মান বসল। এখন count = 1. খেয়াল করুন, ঘোষণার নাম hoist হয়েছিল, মান বসানো হয়নি — মান বসে ঠিক এই লাইনে এসে।
  5. লাইন ৪ — TDZ শেষ। let total = 10 চলল। এই মুহূর্ত থেকে total প্রস্তুত, মান 10. TDZ ছিল scope শুরু থেকে এই লাইন পর্যন্ত — সময় ধরে মাপা।
  6. লাইন ৫ — স্বাভাবিক ব্যবহার। total ছাপা হলো 10. ঘোষণার পরে ব্যবহার করলে let/const-এ কোনো চমক নেই — আর চমক না থাকাটাই ভালো নকশা।

এবার নিজে চালিয়ে দেখুন — hoisting, TDZ, const-এর mutation আর global object-এর পার্থক্য, সব একসাথে। এখানে একটি দোকানের কার্টের মোট দাম VAT সহ হিসাব করা হয়েছে:

console.log(typeof greet);   // function — পুরোটা আগেই তৈরি
console.log(oldCount);       // var — নাম আছে, মান undefined
try {
  console.log(total);        // let — TDZ-এ, ছোঁয়া যায় না
} catch (e) {
  console.log(e.name + ": " + e.message);
}
var oldCount = 5;
let total = 0;
function greet() { return "হ্যালো"; }
const VAT_RATE = 0.15;
const cart = [250, 120];
cart.push(80);               // ভেতরের মান বদলানো যায়
for (const price of cart) total += price;
console.log("মোট:", total * (1 + VAT_RATE));

try {
  VAT_RATE = 0.2;            // নতুন মান বসানো যায় না
} catch (e) {
  console.log(e.name + ": " + e.message);
}
console.log("var global-এ?", "oldCount" in globalThis);
console.log("let global-এ?", "total" in globalThis);

প্রথম তিন লাইনের আউটপুট: function, undefined, আর ReferenceError: Cannot access 'total' before initialization — তিন রকম ঘোষণার তিন রকম hoisting. এই বার্তাটি V8-এর, তাই Chrome, Edge আর Node.js-এ হুবহু একই; Firefox-এ লেখা ভিন্ন (can't access lexical declaration 'total' before initialization), অর্থ একই। এরপর const cart-এ push নির্বিঘ্নে চলে, আর লুপে let total-এ যোগ হয়ে মোট ৪৫০, VAT সহ মোট: 517.5. VAT_RATE-এ নতুন মান বসাতে গেলে TypeError: Assignment to constant variable.

শেষ দুই লাইন সবচেয়ে মজার। এই পাতার runner কোডটি চালায় ব্রাউজারের একটি classic <script>-এ, তাই আউটপুট var global-এ? true আর let global-এ? false. একই ফাইল Node.js-এ (node hoisting.js) চালালে দুটিই false আসে — কারণ Node.js প্রতিটি CommonJS ফাইলকে একটি function-এ মুড়ে চালায়, তাই var সেখানে ফাইলের ভেতরেই থাকে।

TDZ-এর আরও দুটি ফাঁদ। (১) সাধারণত typeof নিরাপদ: একদম অঘোষিত নামে typeof nothere দেয় "undefined". কিন্তু TDZ-এর ভেতরে typeof-ও ReferenceError দেয়। (২) const greet = function () {} বা arrow function — এগুলো function declaration নয়, সাধারণ const. তাই ঘোষণার আগে ডাকলে TDZ-এর error আসে; পুরো hoisting পায় শুধু function name() {} রূপ। Function-এর বিভিন্ন রূপ পর্ব ১৩-এ।

৭ · লুপে var-এর বিখ্যাত বাগ — 3 3 3 বনাম 0 1 2

JavaScript ইন্টারভিউতে সবচেয়ে বেশি জিজ্ঞাসিত প্রশ্নগুলোর একটি এটি, আর এক প্রজন্মের ওয়েব ডেভেলপার বাস্তব প্রজেক্টে এই বাগে পড়েছেন। ধরুন একটি পাতায় তিনটি বোতাম, আর প্রতিটির ক্লিকে তার নম্বর দেখাতে চান। লুপ দিয়ে তিনটির জন্য callback বানালেন — কিন্তু যেকোনো বোতাম চাপলেই দেখায় “3”! নিচে একই সমস্যা setTimeout দিয়ে, কারণ callback পরে চলার ব্যাপারটি দুই জায়গাতেই এক।

মূল কথা: callback তৈরি হয় লুপের সময়, কিন্তু চলে লুপ শেষ হওয়ার পরে (পর্ব ০৫-এর event loop মনে করুন)। তখন সে variable-টির যে মান দেখে, সেটাই ছাপে। প্রশ্ন হলো — তিনটি callback কি একই variable দেখে, না আলাদা আলাদা?

1for (var i = 0; i < 3; i++) {2  setTimeout(() => console.log(i), 10);3}4for (let j = 0; j < 3; j++) {5  setTimeout(() => console.log(j), 20);6}
কোন callback কোন ঘর দেখে
var i (একটিই ঘর)
var-এর callback
let j-এর ঘরগুলো
let-এর callback
  1. var লুপ, প্রথম পুনরাবৃত্তি। var i function scope-এর (এখানে global-এর) একটিমাত্র ঘর, মান 0. setTimeout একটি callback (A) জমা রাখে, যা চলবে ১০ ms পরে। A মনে রাখে “i নামের ঘরটি” — মান 0 নয়।
  2. দ্বিতীয় পুনরাবৃত্তি — একই ঘর। i++ সেই একই ঘরের মান বদলে 1 করে। নতুন callback B-ও দেখে সেই একই ঘর। var-এর কাছে লুপের block-এর কোনো মানে নেই, তাই নতুন ঘর তৈরির প্রশ্নই আসে না।
  3. তৃতীয় পুনরাবৃত্তি। i = 2, callback C — আবারও একই ঘর। তিনটি callback এখন একটি variable-এর দিকে তাকিয়ে আছে।
  4. লুপ শেষ — i = 3. শেষবার i++ মান করল 3, শর্ত 3 < 3 মিথ্যা, লুপ থামল। কোনো callback এখনো চলেনি — call stack ব্যস্ত। লুপের পরেও i বেঁচে আছে, কারণ এটি block-এর নয়।
  5. let লুপ — প্রতি পুনরাবৃত্তিতে নতুন বাঁধন। let j-এর ক্ষেত্রে ভাষার নিয়ম আলাদা: প্রতিটি পুনরাবৃত্তি পায় নিজের নতুন j (আগের মান কপি করে, তারপর j++). প্রথম পুনরাবৃত্তির ঘর j₀ = 0, আর callback D দেখে শুধু j₀.
  6. আরও দুটি আলাদা ঘর। দ্বিতীয় পুনরাবৃত্তিতে নতুন ঘর j₁ = 1 (callback E), তৃতীয়তে j₂ = 2 (callback F). তিনটি callback, তিনটি আলাদা ঘর — কেউ কারও মান বদলায় না।
  7. Timer চলল। Sync কোড শেষ, call stack খালি। ১০ ms-এ A, B, C চলে — সবাই একই ঘর দেখে, যার মান এখন 3: 3 3 3. ২০ ms-এ D, E, F চলে — নিজের নিজের ঘর: 0 1 2. Callback মান নয়, ঘর মনে রাখে — এটাই closure-এর মূল কথা (পর্ব ২২)।

নিজে চালিয়ে দেখুন। শেষে strict mode-এ accidental global-এর পরীক্ষাও আছে:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 10);
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let:", j), 20);
}

console.log("লুপ শেষ, var i এখনো আছে:", i);
console.log("let j আছে?", typeof j);

function strictCount() {
  "use strict";
  try {
    counter = 1;             // ঘোষণা ছাড়া মান বসানো
  } catch (e) {
    console.log(e.name + ": " + e.message);
  }
}
strictCount();

আগে আসে sync কোডের তিনটি লাইন: লুপ শেষ, var i এখনো আছে: 3 (block-এর বাইরেও var বেঁচে), let j আছে? undefined (j লুপের block-এ আটকে ছিল, বাইরে অঘোষিত — আর অঘোষিত নামে typeof error দেয় না), তারপর ReferenceError: counter is not defined — "use strict" থাকায় accidental global তৈরি না হয়ে error. এরপর timer: ১০ ms-এ তিনবার var: 3, ২০ ms-এ let: 0, let: 1, let: 2. দুই লুপের timer-এর সময় আলাদা রাখা হয়েছে যাতে ক্রম সবসময় একই থাকে।

let আসার আগে মানুষ কী করত — IIFE

২০১৫-র আগে let ছিল না। তখন প্রতিটি পুনরাবৃত্তির জন্য নতুন scope বানাতে মানুষ একটি function লিখে সাথে সাথে ডেকে ফেলত — যার নাম IIFE:

for (var i = 0; i < 3; i++) {
  (function (n) {                                 // প্রতিবার নতুন function scope
    setTimeout(() => console.log(n), 10);         // n এই scope-এর নিজের
  })(i);                                          // সাথে সাথে ডাকা, i-এর এখনকার মান দিয়ে
}
// প্রত্যাশিত আউটপুট: 0 1 2

Function ডাকলেই নতুন function scope তৈরি হয়, আর parameter n সেই scope-এর নিজস্ব variable. তাই তিনটি কলে তিনটি আলাদা n. কাজ করে, কিন্তু দেখতে জটিল — let ভাষার ভেতরেই এই সমাধান দিয়ে দিয়েছে। পুরোনো কোডে এই (function () { ... })() নকশা দেখলে এখন চিনবেন।

৮ · ভালো অভ্যাস — const আগে, let দরকারে, var কখনো না

আধুনিক JavaScript কমিউনিটির প্রায় সর্বসম্মত নিয়ম এখন তিন লাইনের:

ডিফল্ট: const
মান সত্যিই বদলাতে হলে: let
নতুন কোডে: var কখনো নয়

কেন const ডিফল্ট? কারণ এটি পাঠককে একটি প্রতিশ্রুতি দেয়: “এই নাম পুরো scope জুড়ে একই জিনিস দেখাবে।” ২০০ লাইনের একটি function পড়ার সময় প্রতিটি const আপনার মাথা থেকে একটি চিন্তা সরিয়ে দেয়। আর যে কয়েকটি let থাকে, সেগুলো আলাদা করে চোখে পড়ে — “সাবধান, এটি বদলায়।” বাস্তব কোডে দেখবেন, বেশিরভাগ variable আসলে কখনো নতুন মান পায় না।

কখন let? যখন মান সত্যিই ধাপে ধাপে বদলায় — যেমন কার্টের মোট দাম জমানো, গণনার counter, বা একটি অবস্থা যা পরে ঠিক হয়:

const VAT_RATE = 0.15;                     // কখনো বদলায় না → const, UPPER_CASE
const prices = [650, 120, 80];             // array নিজে বদলায় না → const

let subtotal = 0;                          // ধাপে ধাপে বাড়বে → let
for (const price of prices) {              // প্রতিবার নতুন price → const চলে
  subtotal += price;
}

let label;                                 // পরে ঠিক হবে → let
if (subtotal > 500) label = "ফ্রি ডেলিভারি";
else label = "ডেলিভারি ৬০ টাকা";

খেয়াল করুন for (const price of prices) — লুপের variable-ও const হতে পারে, কারণ প্রতিটি পুনরাবৃত্তি একটি নতুন বাঁধন পায় আর লুপের ভেতরে price-এ নতুন মান বসানো হয় না। কিন্তু for (let i = 0; i < n; i++)-এ i++ মান বদলায়, তাই সেখানে let লাগবেই। শেষের label-কে ternary দিয়ে const বানানো যেত — সে আলোচনা পর্ব ১১-এ।

আর var কেন কখনো নয়? কারণ var এমন কোনো কাজ করে না যা let পারে না, কিন্তু বাড়তি চারটি ফাঁদ আনে: block না মানা, ঘোষণার আগে নীরব undefined, একই নামে আবার ঘোষণা করলে চুপ থাকা, আর top level-এ window-এ ঢুকে পড়া। একটি বাস্তব counter বাগ দেখুন:

var count = 0;                       // পাতার ভিজিটর গণনা

function resetCartBadge() {
  if (true) {
    var count = 0;                   // লেখক ভাবলেন: শুধু এই block-এর
  }
  count++;                           // কোন count? function-এর! global-টি নয়
  return count;
}
// প্রত্যাশিত আউটপুট: resetCartBadge() দেয় 1, আর global count থাকে 0

Block-এর ভেতরের var count আসলে পুরো function-এর variable — hoist হয়ে function-এর মাথায় চলে যায়। ফলে function-এর সব জায়গায় count মানে এই local-টি, global-এর counter কখনো বাড়ে না। লেখক ভেবেছিলেন একটি অস্থায়ী variable বানাচ্ছেন, আসলে পুরো function-এর জন্য global-কে আড়াল (shadow) করে ফেলেছেন। let হলে ভেতরের count block-এর বাইরে যেত না, আর count++ global-টিকেই বাড়াত।

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

C, C++ আর Java-তে variable স্বাভাবিকভাবেই block-scoped, আর ঘোষণার আগে ব্যবহার compile-এর সময়েই ধরা পড়ে — let/const ঠিক এই আচরণটিই JavaScript-এ আনে (C-তে scope আর storage class, Java-র টাইপ আর variable). Java-র final আর JavaScript-এর const প্রায় একই: reference স্থির, object নয়। C/C++-এর const আরও গভীর — type-এর অংশ, তাই const object-এর ভেতরও বদলানো যায় না। Python-এর scope আবার var-এর মতো function-ভিত্তিক — if বা for নতুন scope বানায় না, আর লুপে closure-এর একই “শেষ মান” বাগ সেখানেও হয় (Python-এর scope আর closure).

পরীক্ষা ও ইন্টারভিউতে
  • let, const আর var-এর পার্থক্য কী? — var function-scoped, hoist হয়ে undefined পায়, আবার ঘোষণা করা যায়; let/const block-scoped, ঘোষণার আগে TDZ; const-এ নতুন মান বসানো যায় না।
  • const দিয়ে বানানো array-তে push করা যায়? — হ্যাঁ। const বাঁধন স্থির রাখে, মান নয়; সম্পূর্ণ স্থির করতে Object.freeze() (তাও shallow).
  • Hoisting কী? — scope চলার আগে সব ঘোষণা নিবন্ধিত হয়: function declaration পুরো তৈরি, var = undefined, let/const/class অপ্রস্তুত (TDZ).
  • লুপে var আর setTimeout দিলে 3 3 3 কেন? — var-এর একটিই বাঁধন, callback চলে লুপ শেষে যখন মান 3; let প্রতি পুনরাবৃত্তিতে নতুন বাঁধন দেয়, তাই 0 1 2.
  • TDZ কী? — scope শুরু থেকে let/const-এর ঘোষণার লাইন চলা পর্যন্ত সময়; তখন নামটি ছুঁলে ReferenceError.

৯ · AI-এর যুগে — পুরোনো টিউটোরিয়ালের var আর অদৃশ্য global

AI কোড লেখা শিখেছে ইন্টারনেটের কোটি কোটি লাইন থেকে, আর তার বড় অংশ লেখা হয়েছিল ২০১৫-র আগে — যখন var ছাড়া উপায় ছিল না। তাই AI-এর দেওয়া JavaScript-এ, বিশেষত ছোট snippet বা পুরোনো jQuery-ধাঁচের কোডে, এই পর্বের সমস্যাগুলো প্রায়ই ফিরে আসে:

AI-এর কোডে যা দেখবেনসমস্যাযা করবেন
var দিয়ে variable, বিশেষ করে লুপেblock না মানা, লুপ-callback-এ শেষ মানের বাগconst/let-এ বদলান, লুপের callback আলাদা করে পরীক্ষা করুন
সব জায়গায় let, কখনো বদলায় না এমন মানেওপাঠক ভাবে মানটি বদলাবে; কোড বোঝা কঠিনযেগুলোতে নতুন মান বসে না, সেগুলো const
ঘোষণা ছাড়া result = ...sloppy mode-এ accidental global, অন্য কোডের সাথে সংঘর্ষঘোষণা যোগ করুন; module বা strict mode ব্যবহার করুন
Top-level-এ var name, var statuswindow.name/window.status-এর সাথে সংঘর্ষconst/let, বা type="module"
“const মানে immutable” বলে ব্যাখ্যাভুল ধারণা — ভেতরের mutation আটকায় নাদরকার হলে Object.freeze() বা কপি করে বদলানো

AI-কে কোড লিখতে বা পুরোনো কোড আধুনিক করতে বলার সময় নিয়মগুলো শুরুতেই বলে দিন:

নিচের JavaScript কোডটি আধুনিক করে দাও: [এখানে কোড পেস্ট করুন] নিয়ম: ১. var একদম ব্যবহার করবে না। ডিফল্ট const, শুধু যেখানে নতুন মান বসে সেখানে let. ২. প্রতিটি let-এর পাশে এক লাইনের comment দাও — কেন এটি const হতে পারল না। ৩. কোনো variable ঘোষণা ছাড়া ব্যবহার করবে না; কোনো নতুন global variable বানাবে না। ৪. var থেকে let-এ বদলানোর ফলে কোনো আচরণ বদলে গেলে (যেমন লুপের callback বা block-এর বাইরে variable ব্যবহার) সেটি আলাদা করে তালিকা দাও। ৫. const-কে "immutable" বলবে না — object-এর ভেতর বদলানো হলে সেটি স্পষ্ট লিখবে।

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

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১var কেন function-scoped আর hoisted হলো? আর ES2015-এ var-কে “ঠিক” না করে নতুন let/const কেন আনা হলো?

১৯৯৫ সালের মে মাসে Netscape-এ Brendan Eich প্রায় দশ দিনে JavaScript-এর প্রথম রূপ তৈরি করেন। তাঁকে নিয়োগ দেওয়া হয়েছিল মূলত “ব্রাউজারে Scheme” বানানোর আশায়, পরে ব্যবস্থাপনা চাইল ভাষাটি দেখতে Java-র মতো হোক। ফলে JavaScript পেল Java/C-এর মতো বাঁকা বন্ধনীর চেহারা, কিন্তু ভেতরে Scheme-এর মতো চিন্তা: function হলো প্রথম শ্রেণির মান, আর scope-এর মূল একক হলো function. Scheme-এ নতুন scope মানেই একটি নতুন function (বা let, যা আসলে function-এর আরেক রূপ)। তাড়াহুড়োর মধ্যে সরল একটি ব্যবস্থা — “প্রতিটি function একটি scope, তার ভেতরের সব var শুরুতেই তৈরি” — বাস্তবায়ন করা সহজ ছিল। Hoisting আসলে এই সরল বাস্তবায়নেরই ফল: engine function শুরুর সময় তার সব নামের জায়গা বানিয়ে নিত।

সমস্যা হলো, চেহারা C-এর মতো হওয়ায় প্রোগ্রামাররা ধরে নিতেন { } নতুন scope বানায়, যেমন C বা Java-তে। সেই ভুল ধারণা থেকেই দুই দশকের বাগ।

তাহলে var-কেই block-scoped বানিয়ে দেওয়া হলো না কেন? কারণ ওয়েবের একটি মৌলিক নীতি: “don't break the web”. ইন্টারনেটে কোটি কোটি পাতা আছে, যার অনেকগুলো আর কেউ রক্ষণাবেক্ষণ করে না। কোনো পাতা হয়তো block-এর ভেতরের var বাইরে ব্যবহার করে। ব্রাউজার যদি var-এর অর্থ বদলাত, সেই পাতাগুলো রাতারাতি ভেঙে যেত — আর ব্যবহারকারী দোষ দিত ব্রাউজারকে, যে ব্রাউজার আগে বদলাত সে বাজার হারাত। তাই TC39-এর নিয়ম হলো, পুরোনো বৈশিষ্ট্যের অর্থ প্রায় কখনো বদলানো হয় না; ভুল শোধরাতে হলে নতুন বৈশিষ্ট্য যোগ করা হয়।

এর ইতিহাসও শিক্ষণীয়। ২০০০-এর দশকে ES4 নামে একটি বিশাল সংস্করণের চেষ্টা হয়েছিল, যা ভাষাকে অনেক বদলে দিত। মতবিরোধে ২০০৮ সালে তা পরিত্যক্ত হয়, আর “Harmony” নামে ধীর, সামঞ্জস্যপূর্ণ পথ বেছে নেওয়া হয় — যার ফল ২০০৯-এর ES5 আর ২০১৫-এর ES2015. let শব্দটি নিজেও Mozilla-র JavaScript 1.7-এ (২০০৬) পরীক্ষামূলকভাবে ছিল, কিন্তু মানদণ্ডে আসতে প্রায় এক দশক লেগেছে। Strict mode-ও একই দর্শনের উদাহরণ: পুরোনো আচরণ বদলানো হয়নি, শুধু যে চায় তার জন্য কড়া নিয়ম চালুর সুইচ।

মূল উপলব্ধি: জনপ্রিয় প্ল্যাটফর্মে একবার ছাড়া ভুল প্রায় চিরস্থায়ী। ভাষা তাই পুরোনো জিনিস মুছে না, পাশে নতুন ও ভালো বিকল্প রাখে — আর ভালো প্রোগ্রামারের কাজ পুরোনোটি চিনে নতুনটি বেছে নেওয়া।

প্র ০২TDZ কেন? ঘোষণার আগে let পড়লে var-এর মতো undefined দিলেই তো সহজ হতো — TC39 কেন error বেছে নিল?

ES2015 নকশার সময় TC39-এ এ নিয়ে লম্বা বিতর্ক হয়েছিল। বিকল্প ছিল মোটামুটি তিনটি, আর প্রতিটির একটি দাম ছিল।

  • var-এর মতো undefined দিয়ে hoist: সহজ, কিন্তু const-এর জন্য সর্বনাশ। একটি const প্রথমে undefined, পরে অন্য মান — তাহলে “const কখনো বদলায় না” প্রতিশ্রুতি মিথ্যা হয়ে যায়। আর var-এর নীরব undefined-এর বাগ, যা দূর করাই উদ্দেশ্য, তা থেকেই যেত।
  • একদম hoist না করা (ঘোষণার লাইনের আগে নামটি নেই, বাইরের scope-এর একই নাম দেখা যাবে): এতে একই block-এ একটি নাম লাইনের আগে এক জিনিস, পরে আরেক জিনিস বোঝাত — lexical scope-এর স্পষ্টতা নষ্ট। আর একে অপরকে ডাকে এমন function-গুলো, যারা পরে ঘোষিত variable ব্যবহার করে, সমস্যায় পড়ত।
  • Hoist করা কিন্তু ঘোষণার আগে ছুঁলে error (TDZ): নামটি পুরো block জুড়ে একই variable-কে বোঝায় (স্পষ্ট scope), কিন্তু প্রস্তুত হওয়ার আগে ব্যবহার করলে জোরে আওয়াজ — নীরব ভুল নয়।

শেষ বিকল্পটি জেতে। Function-এর ভেতরে পরে ঘোষিত variable ব্যবহার করা যায়, যতক্ষণ function টি ডাকা হয় ঘোষণার পরে — কারণ TDZ মাপা হয় সময় দিয়ে, কোডের জায়গা দিয়ে নয়। এজন্যই নাম “temporal”.

তবে TDZ-এরও দাম আছে। Engine-কে অনেক জায়গায় পরীক্ষা বসাতে হয়: “এই variable কি এখনো অপ্রস্তুত?” বেশিরভাগ ক্ষেত্রে optimizer প্রমাণ করতে পারে যে পরীক্ষাটি অপ্রয়োজনীয় আর সেটি সরিয়ে দেয়, কিন্তু সবসময় নয়। খুব বড় কোডবেসে এর প্রভাব মাপা গেছে — যেমন TypeScript দল তাদের compiler-এর বিল্ড আউটপুটে এই পরীক্ষার খরচ নিয়ে লিখেছিল। সাধারণ অ্যাপে এটি নিয়ে চিন্তার কিছু নেই।

class declaration-ও একই কারণে TDZ মানে: একটি class-কে তার ঘোষণার আগে ব্যবহার করা প্রায় সবসময় ভুল, আর ভুলটা যত আগে ধরা পড়ে তত ভালো।

মূল উপলব্ধি: ভাষার নকশায় “নীরবে ভুল ফল” আর “জোরে error” এর মধ্যে বেছে নিতে হলে আধুনিক ভাষাগুলো সাধারণত error বেছে নেয়। একটি দ্রুত ব্যর্থতা (fail fast) ঘণ্টার পর ঘণ্টা ডিবাগিং বাঁচায়।

প্র ০৩const বাঁধন স্থির করে, মান নয়। JavaScript-এ সত্যিকারের অপরিবর্তনীয় ডেটা পাওয়ার পথ কী — Object.freeze থেকে Records & Tuples প্রস্তাব পর্যন্ত?

অপরিবর্তনীয় (immutable) ডেটার বড় সুবিধা: একটি object কোথাও পাঠালে নিশ্চিত থাকেন, কেউ চুপচাপ তার ভেতর বদলে দেবে না। React-এর মতো framework এই ধারণার ওপর দাঁড়িয়ে — state বদলাতে নতুন object বানান, পুরোনোটি বদলান না, যাতে “কিছু বদলেছে কি না” শুধু reference তুলনা করেই বোঝা যায়।

JavaScript-এ এর হাতিয়ারগুলো ধাপে ধাপে এসেছে:

  • Object.freeze() (ES5, ২০০৯): object-এর property যোগ, মোছা, বদলানো বন্ধ। কিন্তু shallow — ভেতরের object আলাদা করে freeze করতে হয় (“deep freeze” নিজে লিখতে হয়)। আর strict mode ছাড়া বদলানোর চেষ্টা নীরবে ব্যর্থ হয়, যা ভুল লুকিয়ে রাখে।
  • Object.seal() আর Object.preventExtensions(): হালকা রূপ — নতুন property যোগ বন্ধ, কিন্তু (seal-এ) মান বদলানো যায়।
  • কপি করে বদলানোর অভ্যাস: spread দিয়ে নতুন object (পর্ব ১৭), আর ES2023-এ যুক্ত toSorted, toReversed, with — মূল array না বদলে নতুন array দেয়।
  • Library: Immutable.js, Immer — বড় অ্যাপে কার্যকর, কিন্তু ভাষার অংশ নয়।

ভাষার ভেতরেই গভীরভাবে অপরিবর্তনীয় মান আনার একটি বড় চেষ্টা ছিল Records & Tuples প্রস্তাব: #{ x: 1 } আর #[1, 2] লিখে primitive-এর মতো অপরিবর্তনীয় object আর array, যেগুলো মান দিয়ে তুলনা হতো (#[1,2] === #[1,2] হতো true). প্রস্তাবটি বহু বছর TC39-এর Stage 2-এ ছিল, কিন্তু engine-নির্মাতারা পারফরম্যান্স আর বাস্তবায়নের জটিলতা নিয়ে গুরুতর আপত্তি তোলেন — বিশেষ করে নতুন primitive type যোগ করা আর ===-এর অর্থ বদলানো নিয়ে। শেষে ২০২৫ সালে প্রস্তাবটি প্রত্যাহার করা হয়। একই সমস্যার হালকা সমাধান খুঁজতে নতুন একটি প্রস্তাব নিয়ে আলোচনা চলছে, কিন্তু তা একদম শুরুর ধাপে — তাই আজ কোনো প্রজেক্টে এর ওপর নির্ভর করবেন না।

মূল উপলব্ধি: JavaScript-এ immutability ভাষার ডিফল্ট নয়, একটি শৃঙ্খলা। const নাম স্থির রাখে, Object.freeze একটি স্তর জমায়, আর বাকিটা আসে “বদলানোর বদলে নতুন বানাও” অভ্যাস থেকে।

প্র ০৪ES module আসার আগে বড় JavaScript প্রজেক্ট global scope-এর দূষণ কীভাবে সামলাত? module pattern, IIFE আর namespace-এর ইতিহাস কী শেখায়?

২০০০-এর দশকে একটি ওয়েবপাতায় দশ-বিশটি <script> ট্যাগ থাকা সাধারণ ছিল — আপনার কোড, jQuery, একটি স্লাইডার plugin, বিজ্ঞাপনের কোড, analytics. সবগুলো একই global scope ভাগ করত। আপনার var data আর বিজ্ঞাপনের কোডের var data একই variable! কোন script শেষে লোড হলো, তার ওপর নির্ভর করত কোনটির মান টিকবে। এই সমস্যার নাম global namespace pollution.

মানুষ ধাপে ধাপে সমাধান বের করেছে:

  • Namespace object: সব কিছু একটি global object-এর ভেতরে — var MYAPP = {}; MYAPP.cart = .... Yahoo-র YUI library YAHOO.util... এভাবে সাজাত। Global নাম একটিতে নেমে এলো, কিন্তু ভেতরের সব কিছু তবুও সবার জন্য খোলা।
  • Module pattern: একটি function-এর ভেতরে private variable রেখে শুধু বাছাই করা জিনিস একটি object হিসেবে ফেরত দেওয়া। Douglas Crockford-এর লেখা আর Yahoo-র ডেভেলপারদের ব্লগের মাধ্যমে ২০০০-এর দশকের মাঝামাঝি এটি জনপ্রিয় হয়। এর ভিত্তি closure — function শেষ হলেও ফেরত দেওয়া method-গুলো private variable দেখতে পায়।
  • IIFE: function লিখে সাথে সাথে ডাকা — (function () { ... })();. ২০১০ সালে Ben Alman একটি ব্লগ পোস্টে এই নকশার নাম দেন “Immediately-Invoked Function Expression”. প্রায় প্রতিটি library-র পুরো কোড একটি IIFE-তে মোড়া থাকত, যাতে ভেতরের var বাইরে না যায়। jQuery-র উৎস খুললে এখনো এটি দেখবেন।
  • CommonJS (২০০৯) আর AMD/RequireJS: সার্ভারের জন্য Node.js গ্রহণ করে CommonJS (require/module.exports) — প্রতিটি ফাইলকে একটি function-এ মুড়ে চালানো, ঠিক যে কারণে Node.js-এ top-level var global হয় না। ব্রাউজারে asynchronous লোডের জন্য AMD. তারপর Browserify, webpack-এর মতো bundler.

শেষে ES2015 ভাষার ভেতরেই module আনে — import/export, প্রতিটি ফাইলের নিজস্ব module scope, ডিফল্টভাবে strict mode. ব্রাউজারগুলোতে <script type="module"> সমর্থন আসে ২০১৭–২০১৮ সালের দিকে। এখন global scope-এ কিছু রাখতে হলে স্পষ্টভাবে globalThis.x = ... লিখতে হয় — দুর্ঘটনায় নয়, ইচ্ছায়।

মূল উপলব্ধি: ভাষায় কোনো সুবিধা না থাকলে কমিউনিটি নকশা (pattern) দিয়ে তা বানিয়ে নেয়, আর সফল নকশাগুলো একসময় ভাষার অংশ হয়ে যায়। IIFE আর module pattern বোঝা মানে closure আর scope সত্যিই বোঝা।

প্র ০৫Lexical scope বনাম dynamic scope — প্রোগ্রামিং ভাষার ইতিহাসে এই দুইয়ের লড়াই কেমন ছিল, আর JavaScript কোথায় দাঁড়িয়ে?

একটি function-এর ভেতরে একটি “বাইরের” নাম ব্যবহার করা হলে প্রশ্ন ওঠে: বাইরে মানে কোথায়? দুটি উত্তর সম্ভব। Lexical (static) scope: function যেখানে লেখা হয়েছে, তার চারপাশ। Dynamic scope: function যেখান থেকে ডাকা হয়েছে, সেই মুহূর্তের call stack-এ সবচেয়ে কাছের ওই নাম।

১৯৫৮ সালে John McCarthy-র তৈরি প্রথম Lisp বাস্তবে dynamic scope দিয়ে চলত — বাস্তবায়ন সহজ ছিল, শুধু call stack-এ নাম খুঁজলেই হয়। কিন্তু এতে একটি বিখ্যাত সমস্যা দেখা দেয়, যার নাম “funarg problem”: একটি function-কে মান হিসেবে অন্য জায়গায় পাঠালে সে কোন variable দেখবে, তা নির্ভর করত কে তাকে ডাকল তার ওপর — ফলে একই function ভিন্ন জায়গায় ভিন্ন আচরণ করত। ALGOL 60 lexical (block) scope নিয়ে আসে, যা পরে C, Pascal-সহ বেশিরভাগ ভাষার ধারা হয়।

১৯৭৫ সালে MIT-তে Gerald Sussman আর Guy Steele তৈরি করেন Scheme — Lisp-এর একটি উপভাষা, যা lexical scope আর first-class function একসাথে নিয়ে আসে, আর closure-কে ভাষার কেন্দ্রে বসায়। পরে Common Lisp (১৯৮৪) ডিফল্ট হিসেবে lexical scope নেয়, তবে বিশেষভাবে চিহ্নিত variable-এর জন্য dynamic scope রেখে দেয়। Emacs Lisp দীর্ঘদিন dynamic scope-এ ছিল; Emacs 24 (২০১২) থেকে ফাইলভিত্তিক lexical-binding চালু করা যায়। Bash-এর local variable আজও dynamic scope মানে।

JavaScript Scheme থেকে অনুপ্রাণিত, তাই শুরু থেকেই lexical scope — এজন্যই closure এত স্বাভাবিকভাবে কাজ করে। তবে দুটি ব্যতিক্রম scope-কে ঘোলা করত: with statement (একটি object-এর property-কে চলার সময় scope-এ ঢুকিয়ে দেয়) আর sloppy mode-এর eval (চলার সময় নতুন variable ঘোষণা করতে পারে)। এ দুটির কারণে কোড দেখে নিশ্চিত বলা যেত না কোন নাম কোথা থেকে আসছে, আর engine-ও optimize করতে পারত না। Strict mode with নিষিদ্ধ করেছে আর eval-কে নিজের scope-এ আটকে দিয়েছে। আর this? সাধারণ function-এ এটি ঠিক হয় কীভাবে ডাকা হলো তা দেখে — অনেকটা dynamic আচরণের মতো; arrow function আবার lexical this নেয় (পর্ব ২৪)।

মূল উপলব্ধি: Lexical scope মানে কোড পড়েই বোঝা যায় কোন নাম কাকে বোঝায় — মানুষের জন্যও সহজ, engine-এর জন্যও। ইতিহাস দেখায়, যে ভাষা এই পূর্বানুমেয়তা ছেড়ে দেয়, তাকে পরে বিশেষ নিয়ম দিয়ে তা ফেরত আনতে হয়।

অনুশীলন

প্রথমে খাতায় উত্তর লিখুন, তারপর ব্রাউজারের console-এ চালিয়ে মেলান।

  1. আউটপুট অনুমান করুন: console.log(a); var a = 1; console.log(b); let b = 2; — কী ছাপা হবে, আর কোথায় থামবে?

    প্রথম লাইনে undefined — var a hoist হয়েছে, মান তখনো বসেনি। তৃতীয় statement-এ b TDZ-এ, তাই ReferenceError: Cannot access 'b' before initialization (Chrome/Edge/Node.js), আর প্রোগ্রাম সেখানেই থামে। খেয়াল করুন, দুটিই “ঘোষণার আগে ব্যবহার”, কিন্তু var চুপ থাকে আর let জোরে জানায়।

  2. const না let? নিচের প্রতিটির জন্য বলুন কোনটি ঠিক: (ক) একটি অর্ডারের আইটেমের array, যাতে push করা হবে (খ) লুপে বাড়তে থাকা মোট দাম (গ) প্রতি কেজি পেঁয়াজের নির্দিষ্ট দাম, যা প্রোগ্রামে বদলায় না (ঘ) একটি user object, যার name পরে বদলানো হবে।

    (ক) const — push mutation, নামটি একই array দেখায়। (খ) let — total += price নতুন মান বসায়। (গ) const, আর নামটি UPPER_CASE হতে পারে, যেমন ONION_PRICE_PER_KG. (ঘ) const — property বদলানো mutation; শুধু পুরো object অন্য object দিয়ে বদলাতে হলে let লাগত। চারটির মধ্যে তিনটিই const — বাস্তব কোডেও অনুপাত প্রায় এমনই।

  3. বাগ ধরুন: একটি পাতায় তিনটি বোতাম, কোড: var buttons = document.querySelectorAll("button"); for (var i = 0; i < buttons.length; i++) { buttons[i].onclick = function () { console.log("বোতাম " + i); }; } — যেকোনো বোতামে চাপলে কী আসবে? সবচেয়ে ছোট সংশোধন কী?

    তিনটি বোতামেই বোতাম 3. ক্লিক হয় লুপ শেষ হওয়ার অনেক পরে, আর তিনটি callback-ই একমাত্র var i-কে দেখে, যার মান তখন 3. সবচেয়ে ছোট সংশোধন: লুপে var i-এর জায়গায় let i — প্রতিটি পুনরাবৃত্তি নিজের i পায়, ফলে 0, 1, 2. (প্রথম লাইনের var buttons-কেও const করা উচিত।) আরও আধুনিক উপায়: buttons.forEach((btn, i) => ...) বা event delegation (পর্ব ৩৮)।

  4. Global object পরীক্ষা: যেকোনো ওয়েবপাতার console-এ পরপর লিখুন: var city = "ঢাকা"; তারপর window.city; এরপর let town = "খুলনা"; তারপর window.town. কী দেখলেন, কেন? (Console-এর কোড classic script-এর মতো চলে।)

    window.city দেয় 'ঢাকা' — top-level var global object-এর property হয়ে যায়। window.town দেয় undefined — let global scope-এ আছে (console-এ শুধু town লিখলে 'খুলনা' পাবেন), কিন্তু window-এর property নয়। ES2015-এর নকশাকারীরা ইচ্ছা করে এই পার্থক্য রেখেছেন, যাতে নতুন ঘোষণাগুলো window-এর শত শত পুরোনো property-র সাথে না মেশে।

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

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

ক্রমএকটি module ফাইলের function-এর ভেতরের for লুপে একটি নাম ব্যবহার হলো। Engine কোন ক্রমে খোঁজে, সাজান (▲▼ চেপে সরান)

  1. 🔲 for লুপের block scope
  2. 🧩 function scope (parameter আর local variable)
  3. 📦 module scope (ফাইলের top level)
  4. 🌐 global scope (globalThis-এর property সহ)
  5. ⛔ কোথাও না পেলে ReferenceError

const items = [1, 2]; items.push(3); চালালে কী হবে?

  • TypeError: Assignment to constant variable.
  • কোনো error নেই, কিন্তু array বদলায় না
  • items হয় [1, 2, 3]
  • SyntaxError

Chrome-এ console.log(x); let x = 5; চালালে কী আসে?

  • undefined
  • ReferenceError: Cannot access 'x' before initialization
  • ReferenceError: x is not defined
  • 5

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); — আউটপুট কী?

  • 0 1 2
  • 2 2 2
  • 0 0 0
  • 3 3 3

ব্রাউজারের একটি সাধারণ (module নয়) script-এর top level-এ var a = 1; let b = 2; লেখা হলো। কোনটি সত্য?

  • window.a হয় 1, কিন্তু window.b হয় undefined
  • window.a আর window.b দুটিই আছে
  • কোনোটিই window-এ নেই
  • শুধু window.b আছে

Hoisting-এর সময় কোন ঘোষণা ঘোষণার লাইনের আগেই পুরোপুরি ব্যবহারযোগ্য থাকে?

  • var count = 1
  • function greet() {}
  • const greet = () => {}
  • class Cart {}

একটি function-এর ভেতরে "use strict"; আছে, আর সেখানে কোনো ঘোষণা ছাড়াই total = 10; লেখা, যদিও total কোথাও ঘোষিত নয়। কী হবে?

  • চুপচাপ একটি global variable total তৈরি হবে
  • লাইনটি নীরবে উপেক্ষিত হবে
  • ReferenceError: total is not defined
  • function-এর একটি local variable total তৈরি হবে

শব্দকোষ

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

মনে রাখুন

  1. Variable হলো নাম আর মানের বাঁধন। const বাঁধন স্থির রাখে, মান নয় — object/array-এর ভেতর বদলানো যায়; পুরো স্থির করতে Object.freeze() (shallow).
  2. let/const block-scoped, var function-scoped. নাম খোঁজা হয় ভেতর থেকে বাইরে (scope chain); কোথাও না পেলে ReferenceError. Scope ঠিক হয় কোড কোথায় লেখা তা দেখে (lexical scope).
  3. Hoisting: function declaration পুরো তৈরি, var = undefined, let/const/class TDZ-এ — ঘোষণার আগে ছুঁলে Cannot access 'x' before initialization.
  4. Classic script-এর top-level var window-এর property হয়; ঘোষণা ছাড়া মান বসালে sloppy mode-এ accidental global, strict mode-এ ReferenceError.
  5. লুপে var + callback = 3 3 3; let = 0 1 2. অভ্যাস: ডিফল্ট const, দরকারে let, var কখনো নয়।

পরের পর্বে: নাম আর মান তৈরি হলো — এবার তাদের নিয়ে কাজ। অপারেটর, == বনাম ===, কোন মান truthy আর কোনটি falsy, আর আধুনিক JavaScript-এর দুটি দারুণ অপারেটর ?? ও ?..

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

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