এই পর্বে যা শিখবেন
- Lexical scope বনাম dynamic scope, environment record আর scope chain দিয়ে engine কীভাবে একটি নাম খোঁজে
- Closure ঠিক কী ধরে রাখে — মান নয়, binding — আর কেন দুটি counter একে অপরকে ছোঁয় না
- Closure দিয়ে private state, function factory, memoization,
onceআর rate limiter বানানো - লুপে
varবনামlet-এর রহস্য মানদণ্ডের (spec) ভাষায়, আর IIFE ও module pattern-এর ইতিহাস - Callback আর async কোডে stale মান, closure-জনিত memory leak, আর DevTools-এ closure দেখা
১ · Closure কী — যে function জন্মস্থান মনে রাখে
পর্ব ০৭-এ দেখেছেন, একটি নাম কোন variable-কে বোঝায় তা ঠিক হয় কোড কোথায় লেখা আছে তা দেখে — একে বলে lexical scope. আর পর্ব ১৩-এ দেখেছেন, JavaScript-এ function একটি মান: variable-এ রাখা যায়, পাঠানো যায়, অন্য function থেকে ফেরত দেওয়া যায় (first-class function). এই দুটি সত্য একসাথে রাখলে একটি প্রশ্ন ওঠে, আর সেই প্রশ্নের উত্তরই এই পর্বের বিষয়।
প্রশ্নটি এই: একটি function-এর ভেতরে আরেকটি function তৈরি হলো। ভেতরেরটি বাইরের function-এর একটি local variable ব্যবহার করে। এখন বাইরের function ভেতরেরটিকে return করে দিল, আর নিজে শেষ হয়ে গেল। পর্ব ১৩-এর call stack অনুযায়ী বাইরের function-এর frame সরে যাওয়ার কথা, তার local variable-ও হারিয়ে যাওয়ার কথা। তাহলে ভেতরের function পরে যখন চলবে, সেই variable কোথায় পাবে?
JavaScript-এর উত্তর: variable-টি হারায় না। প্রতিটি function তৈরি হওয়ার মুহূর্তে তার চারপাশের variable-এর পরিবেশের সাথে একটি স্থায়ী সংযোগ পেয়ে যায়। Function যেখানেই যাক, যত পরেই চলুক, সেই সংযোগ ধরে তার জন্মস্থানের variable পড়তে ও বদলাতে পারে। আর যতক্ষণ কেউ function-টিকে ধরে রেখেছে, ততক্ষণ সেই variable-গুলোও মেমরিতে বেঁচে থাকে। Function আর তার ধরে রাখা পরিবেশের এই জোড়ের নাম closure.
ব্যাংকের টোকেন মেশিনের কথা ভাবুন। সকালে শাখা খুললে ক্যাশ কাউন্টারের মেশিন ১ থেকে গোনা শুরু করে। মেশিনটি একটি ছোট খাতা নিজের ভেতরে রাখে — “শেষ টোকেন কত ছিল।” গ্রাহক বোতাম চাপেন, মেশিন খাতা দেখে পরের নম্বর দেয় আর খাতায় লিখে রাখে। ঋণ কাউন্টারের মেশিন একই নকশার, কিন্তু তার খাতা আলাদা। কোনো গ্রাহক খাতাটি সরাসরি দেখতে বা বদলাতে পারেন না — শুধু বোতাম চাপতে পারেন। এখানে বোতাম হলো function, ভেতরের খাতা হলো closure-এ ধরা variable, আর দুটি মেশিন মানে দুটি আলাদা closure.
সবচেয়ে ছোট উদাহরণটি দেখুন। এখানে কোনো চালাকি নেই, শুধু একটি function আরেকটি function ফেরত দিচ্ছে:
function makeGreeting(branch) {
const prefix = "স্বাগতম, " + branch + " শাখা"; // makeGreeting-এর local
return function (name) {
return prefix + " — " + name; // বাইরের prefix ব্যবহার
};
}
const greetRajshahi = makeGreeting("রাজশাহী"); // makeGreeting শেষ হয়ে গেল
greetRajshahi("রফিক"); // "স্বাগতম, রাজশাহী শাখা — রফিক"makeGreeting("রাজশাহী") চলল, prefix তৈরি হলো, একটি নতুন function তৈরি হয়ে ফেরত গেল, আর makeGreeting শেষ। সাধারণ নিয়মে prefix এখানেই মরে যেত। কিন্তু ফেরত যাওয়া function তার জন্মস্থানের দিকে একটি সংযোগ রেখেছে, আর সেই function এখন greetRajshahi নামে বেঁচে আছে — তাই prefix-ও বেঁচে আছে। শেষ লাইনে ডাকলে ভেতরের function সেই prefix খুঁজে পায়। এই function-এর কাছে prefix হলো একটি free variable — ব্যবহার করে, কিন্তু নিজে ঘোষণা করেনি।
Lexical scope বনাম dynamic scope — কেন “জন্মস্থান”, “ডাকার জায়গা” নয়
একটি function-এর ভেতরে বাইরের কোনো নাম থাকলে, বাইরে মানে কোথায়? দুটি সম্ভাব্য নিয়ম আছে। Lexical (static) scope: function যেখানে লেখা হয়েছে, তার চারপাশ। Dynamic scope: function যেখান থেকে ডাকা হয়েছে, সেই মুহূর্তের চারপাশ। JavaScript সবসময় প্রথম নিয়ম মানে:
const vat = 0.15;
function price(amount) { return amount + amount * vat; }
function eidSale() {
const vat = 0; // এখানে আরেকটি vat
return price(1000); // price কোন vat দেখবে?
}
eidSale(); // 1150 — price লেখা হয়েছে global-এ, তাই global-এর vatprice ডাকা হয়েছে eidSale-এর ভেতর থেকে, যেখানে vat = 0. Dynamic scope হলে ফল হতো ১০০০. কিন্তু price লেখা হয়েছে global-এ, তাই সে global-এর vat দেখে — ফল ১১৫০. এই নিয়মের সবচেয়ে বড় সুবিধা: কোড পড়েই বলা যায় কোন নাম কাকে বোঝায়, কে কোথা থেকে ডাকবে তা না জেনেই। Closure ঠিক এই নিয়মের ওপর দাঁড়িয়ে — function যেহেতু জন্মস্থানের variable দেখে, তাই জন্মস্থানের সাথে সংযোগটি তাকে সাথে রাখতেই হয়। (একটি ব্যতিক্রম মনে রাখুন: সাধারণ function-এর this ঠিক হয় ডাকার ধরন দেখে — সেটি পর্ব ২৪-এর বিষয়। Variable-এর ক্ষেত্রে নিয়ম সবসময় lexical.)
Closure = function + তার জন্মস্থানের variable-গুলোর জীবন্ত সংযোগ। JavaScript-এ প্রতিটি function-ই আসলে closure — global-এ লেখা function global পরিবেশ ধরে রাখে। তবে “closure” শব্দটি আমরা সাধারণত তখনই বলি, যখন একটি function তার বাইরের function শেষ হওয়ার পরেও সেখানকার variable ব্যবহার করে।
২ · Environment record আর scope chain — engine কীভাবে একটি নাম খোঁজে
Closure সত্যিই বুঝতে হলে engine-এর ভেতরের একটি ছবি দরকার। ECMAScript মানদণ্ড (spec) এই ছবিটি বর্ণনা করে কয়েকটি শব্দ দিয়ে। শব্দগুলো ভয় পাওয়ার মতো নয় — প্রতিটির পেছনে একটি সহজ ধারণা।
📒 Environment record
একটি scope-এর নাম আর মানের খাতা। প্রতিটি function কল, প্রতিটি { } block (যেখানে let/const আছে), প্রতিটি module আর global — প্রত্যেকের নিজের খাতা।
🔗 Outer reference
প্রতিটি খাতার পাতায় লেখা থাকে “আমাকে ঘিরে থাকা খাতা কোনটি।” Spec-এ এর নাম [[OuterEnv]]. Global-এর outer হলো null — শিকলের শেষ মাথা।
🧬 Function-এর [[Environment]]
Function তৈরির মুহূর্তে চলতি খাতাটি function object-এর ভেতরের একটি গোপন ঘরে ([[Environment]]) লেখা হয়ে যায়। এটাই “জন্মস্থানের ঠিকানা।”
📞 কলের সময়
Function ডাকলে নতুন একটি খাতা তৈরি হয় (parameter আর local-এর জন্য), আর তার outer বসানো হয় [[Environment]] থেকে — ডাকার জায়গা থেকে নয়।
এই খাতাটির নাম spec-এ environment record. পুরোনো সংস্করণে খাতা আর outer সংযোগ মিলিয়ে বলা হতো “Lexical Environment”; এখনকার spec-এ outer সংযোগটি সরাসরি record-এর ভেতরেই থাকে। দুই নামই বই আর ব্লগে দেখবেন — মানে একই। খাতাগুলো outer সংযোগ দিয়ে জুড়ে যে শিকল তৈরি হয়, সেটাই scope chain.
একটি নাম দেখলে engine-এর কাজ সোজা: চলতি খাতায় খোঁজো → না পেলে outer খাতায় → তার outer-এ → … → global → সেখানেও না পেলে ReferenceError. প্রথম যেখানে পাওয়া যায়, সেখানেই খোঁজা থামে — এ কারণেই ভেতরের একই নাম বাইরেরটিকে আড়াল করে (পর্ব ০৭-এর shadowing). তিন স্তর গভীর একটি function-এ চারটি নাম খোঁজা ধাপে ধাপে দেখুন। “পরে” চাপুন।
1const fee = 10;2function branch() {3 const city = "Rajshahi";4 function counter() {5 const no = 3;6 function teller() {7 return `${city}-${no}: ${fee} taka`;8 }9 return teller();10 }11 return counter();12}13branch();
| খোঁজা হচ্ছে | |
| teller-এর খাতা | |
| counter-এর খাতা | |
| branch-এর খাতা | |
| global খাতা | |
| ফল |
- শিকলটি তৈরি।
branch()→counter()→teller()— তিনটি কল, তিনটি নতুন খাতা। প্রতিটি খাতার outer হলো যে function-এর ভেতরে লেখা, তার খাতা: teller → counter → branch → global.tellerনিজে কিছু ঘোষণা করেনি, তাই তার খাতা খালি। noখোঁজা। লাইন ৭-এ engine প্রথমে teller-এর খাতায় দেখে — নেই। outer ধরে এক ধাপ বাইরে, counter-এর খাতায় — পাওয়া গেল, মান ৩. খোঁজা এখানেই থামে; বাইরের খাতাগুলো আর দেখা হয় না।cityখোঁজা। teller-এ নেই, counter-এ নেই, branch-এর খাতায় পাওয়া গেল — দুই ধাপ বাইরে। লক্ষ করুন,tellerঠিক কোথা থেকে ডাকা হয়েছে তা কোনো ভূমিকা রাখেনি; শিকল তৈরি হয়েছে কোড কোথায় লেখা তা দেখে।feeখোঁজা। তিনটি function-এর খাতা পেরিয়ে global-এ পাওয়া গেল, মান ১০. Template literal জোড়া লেগে ফল"Rajshahi-3: 10 taka". V8-এর মতো engine সাধারণত কোড parse আর compile করার সময়েই হিসাব করে রাখে কোন নাম কত ধাপ বাইরে, তাই বাস্তবে প্রতিবার খাতা ঘুরে খুঁজতে হয় না — কিন্তু ফল সবসময় এই নিয়মের সমান।- কোথাও না থাকলে। লাইন ৭-এ যদি
tokenলেখা থাকত, engine চারটি খাতাই ঘুরত, global-এর outernull-এ পৌঁছে থামত, আর ছুড়তReferenceError: token is not defined(V8-এর বার্তা)। পর্ব ০৭-এর TDZ মনে রাখুন: নামটি খাতায় আছে কিন্তু মান বসার আগে পড়লে বার্তা হয় অন্য —Cannot access 'token' before initialization.
এবার closure-এর রহস্যটি এক বাক্যে বলা যায়: একটি খাতা ততক্ষণ বেঁচে থাকে, যতক্ষণ কেউ তার দিকে সংযোগ রাখে। সাধারণ কলের পরে কেউ সংযোগ রাখে না, তাই খাতাটি garbage collection-এ মুছে যায়। কিন্তু ভেতরের কোনো function বাইরে ফেরত গেলে তার [[Environment]] সেই খাতার দিকে সংযোগ রাখে — খাতাটি থেকে যায়। এ কারণেই closure-এর variable stack-এ নয়, heap-এ থাকে বলে ভাবা ভালো।
৩ · Closure মান নয়, variable ধরে রাখে — counter factory
Closure নিয়ে সবচেয়ে বড় ভুল ধারণা হলো: “function তৈরির সময় বাইরের variable-গুলোর একটি কপি তুলে রাখে।” না — function তুলে রাখে খাতাটি, অর্থাৎ variable-গুলোর binding (নাম আর ঘরের সংযোগ)। তাই function তৈরির পরে বাইরে মান বদলালে function বদলানো মানটিই দেখে:
let rate = 5; // ডেলিভারি চার্জ, শতাংশে
const fee = (amount) => (amount * rate) / 100;
rate = 10; // function তৈরির পরে বদল
fee(1000); // 100 — 50 নয়; fee দেখে ঘরটি, তৈরির সময়ের মান নয়এই আচরণ সুবিধাও, বিপদও। সুবিধা: closure-এর ভেতরে একটি variable বদলালে পরের কলে সেটি মনে থাকে — counter, ব্যালান্স, cache সব এভাবেই কাজ করে। বিপদ: আপনি ভাবছেন একটি মুহূর্তের মান “ছবি তুলে” রাখা হয়েছে, আসলে রাখা হয়েছে ঘরটি — পরে কেউ বদলালে চমক (অংশ ৫ আর ৮)।
makeCounter — প্রতিটি কলে নতুন খাতা
এবার ব্যাংকের টোকেন মেশিনটি কোডে বানাই। makeCounter একটি function factory: প্রতিবার ডাকলে একটি নতুন counter function তৈরি করে দেয়। প্রশ্ন হলো, দুটি counter কি একই count ভাগ করে? ধাপে ধাপে দেখুন:
1function makeCounter(label) {2 let count = 0;3 return () => `${label}-${++count}`;4}5const cash = makeCounter("C");6const loan = makeCounter("L");7cash(); cash();8loan();
| E1 (cash-এর জন্মস্থান) | |
| E2 (loan-এর জন্মস্থান) | |
| কে কোন খাতা ধরে রেখেছে | |
| call stack |
- makeCounter("C") — প্রথম খাতা E1. কলের সাথে নতুন খাতা E1 তৈরি:
label = "C",count = 0. লাইন ৩-এ একটি arrow function তৈরি হলো; তার[[Environment]]-এ লেখা হলো E1. Function ফেরত গেলcash-এ, makeCounter-এর frame call stack থেকে সরে গেল — কিন্তু E1 মুছল না, কারণcashতাকে ধরে রেখেছে। - makeCounter("L") — সম্পূর্ণ নতুন খাতা E2. একই function, কিন্তু নতুন কল মানে নতুন খাতা:
label = "L",count = 0. নতুন একটি arrow function তৈরি হলো, যার জন্মস্থান E2. এখন heap-এ দুটি আলাদাcountআছে — একটি কোড, দুটি স্মৃতি। - cash() — E1-এর count বাড়ে। cash ডাকলে তার নিজের ছোট খাতা তৈরি হয় (খালি), outer = E1.
++countখুঁজে পায় E1-এরcount, সেটিকে ১ করে। ফল"C-1". Call শেষ, কিন্তু E1-এ এখন count = 1 স্থায়ীভাবে লেখা। - আবার cash() — মনে আছে! একই E1, তাই count ১ থেকে ২. ফল
"C-2". সাধারণ function-এর local variable প্রতিটি কলে নতুন করে শুরু হয়; closure-এর variable কলের পর কলে টিকে থাকে — এটাই “মনে রাখা।” - loan() — E1 অক্ষত। loan-এর outer E2, তাই E2-এর count ০ থেকে ১. ফল
"L-1". ক্যাশ কাউন্টারের ২টি টোকেন ঋণ কাউন্টারের গোনায় কোনো প্রভাব ফেলেনি। পরেcash-এর দিকে কেউ আর সংযোগ না রাখলে (যেমন সেটি একটিletvariable হলে আরnullবসালে) E1 আর পৌঁছানো যায় না — GC তখন একে মুছে দিতে পারে।
এবার তিনটি ধারণা — দুটি স্বাধীন counter, পরে বদলানো মান দেখা, আর গোপন ব্যালান্স — একসাথে চালিয়ে দেখুন। ওয়ালেটের অংশটি পরের অংশে বিস্তারিত।
function makeCounter(label) {
let count = 0; // এই variable closure-এ ধরা থাকবে
return () => `${label}-${++count}`;
}
const cash = makeCounter("C"), loan = makeCounter("L");
console.log(cash(), cash(), loan(), cash());
let rate = 5;
const fee = (amount) => (amount * rate) / 100;
rate = 10; // function তৈরির পরে বদল
console.log(fee(1000));
function makeWallet(balance) { // parameter-ও খাতায় থাকে, বাইরে থেকে ছোঁয়া যায় না
return {
deposit: (t) => (balance += t),
withdraw: (t) => (t > balance ? "ব্যালান্স কম" : (balance -= t)),
balance: () => balance,
};
}
const w = makeWallet(500);
console.log(w.deposit(200), w.withdraw(1000), w.withdraw(300));
console.log(w.balance(), Object.keys(w));প্রথম লাইনে C-1 C-2 L-1 C-3 — দুটি counter-এর খাতা আলাদা; loan ডাকার পরেও cash তার গোনা ধরে রেখেছে। দ্বিতীয় লাইনে 100 — fee তৈরির সময় rate ছিল ৫, কিন্তু ডাকার সময় ঘরটিতে ১০, আর closure ঘরটিই দেখে। তৃতীয় লাইনে 700 ব্যালান্স কম 400: ৫০০ + ২০০ = ৭০০; ১০০০ তোলা যায়নি; তারপর ৩০০ তোলার পরে ৪০০. শেষ লাইনে 400 [ 'deposit', 'withdraw', 'balance' ] — ফেরত পাওয়া object-এ শুধু তিনটি method; balance নামের variable-টি object-এর কোনো property নয়, তাই বাইরের কোড w.balance = 1000000 লিখে ব্যালান্স বদলাতে পারে না (লিখলে শুধু method-টি মুছে যাবে, গোপন variable অক্ষত থাকবে)।
৪ · Private state আর function factory — ওয়ালেট, ছাড় আর rate limiter
উপরের makeWallet একটি গুরুত্বপূর্ণ নকশা দেখায়। bKash বা Nagad-এর মতো একটি মোবাইল ওয়ালেট ভাবুন। ব্যালান্স কেউ সরাসরি লিখে বদলাতে পারে না; বদলানোর একমাত্র পথ নিয়ম মেনে জমা আর তোলা — যেখানে যাচাই হয় ব্যালান্স যথেষ্ট কি না। প্রোগ্রামিংয়ে এই ধারণার নাম encapsulation: ডেটা লুকিয়ে রেখে শুধু নিয়ন্ত্রিত দরজা খোলা রাখা।
Java বা C++-এ এর জন্য আছে private শব্দ। JavaScript-এ বহু বছর এমন কিছু ছিল না — object-এর সব property সবাই দেখতে ও বদলাতে পারত। তখন closure-ই ছিল সত্যিকারের গোপনীয়তার একমাত্র উপায়: variable-টি একটি function-এর খাতায় রাখুন, আর বাইরে দিন শুধু সেই function-গুলো যারা খাতাটি দেখতে পায়। বাইরের কোডের কাছে খাতায় পৌঁছানোর কোনো নাম নেই — তাই কোনো উপায়ও নেই।
| বিষয় | Closure-এর private variable | Class-এর #private field (ES2022) |
|---|---|---|
| গোপনীয়তা | সম্পূর্ণ — কোনো নাম দিয়েই বাইরে থেকে পৌঁছানো যায় না | সম্পূর্ণ — class-এর বাইরে obj.#balance লিখলে SyntaxError |
| মেমরি | প্রতিটি object-এর জন্য method-গুলোও নতুন করে তৈরি হয় | method থাকে prototype-এ, সব object ভাগ করে (পর্ব ২৫) |
| Debug-এ দেখা | DevTools-এর Scope পাতায় “Closure” অংশে | DevTools-এ object খুললেই দেখা যায় |
| উপযুক্ত কখন | ছোট, এককালীন অবস্থা — counter, cache, একটি callback-এর স্মৃতি | অনেক object, inheritance, instanceof দরকার হলে |
পর্ব ২৬-এ class আর #private field দেখবেন। তখন বুঝবেন, দুটি আসলে একই সমস্যার দুটি সমাধান — একটি function-এর পথে, একটি object-এর পথে। এর মজার দার্শনিক দিকটি ভাবনার প্রশ্ন ৪-এ।
Function factory — একটি কাঠামো, অনেক নিয়ম
একটি ই-শপের কথা ভাবুন। ঈদে ১০% ছাড়, নতুন গ্রাহকের জন্য ১৫%, পাইকারি ক্রেতার জন্য ২৫%. প্রতিটির জন্য আলাদা function লেখার বদলে একটি factory লিখুন, যা হার নিয়ে একটি নতুন ছাড়-function বানিয়ে দেয়:
function makeDiscount(percent) {
return (price) => Math.round(price - (price * percent) / 100);
}
const eidOffer = makeDiscount(10);
const newbie = makeDiscount(15);
const wholesale = makeDiscount(25);
eidOffer(1200); // 1080
wholesale(1200); // 900
[500, 800].map(newbie); // [ 425, 680 ]প্রতিটি makeDiscount কল নিজের খাতায় একটি percent রাখে, আর ফেরত দেওয়া function সেটি মনে রাখে। শেষ লাইনটি খেয়াল করুন: newbie একটি সাধারণ এক-argument function, তাই সরাসরি map-এ পাঠানো গেছে (পর্ব ১৬)। Factory আর higher-order function-এর এই জুটি পর্ব ২৩-এর currying আর composition-এর ভিত্তি।
Rate limiter — N বারের বেশি নয়
একটি OTP পাঠানোর বোতাম ভাবুন — একজন ব্যবহারকারী বারবার চাপলে সার্ভারে শত শত SMS যাবে, খরচও বাড়বে। একটি সরল সমাধান: যে function প্রথম N বার কাজটি করতে দেয়, তারপর থামিয়ে দেয়।
function makeLimiter(limit) {
let used = 0; // closure-এ থাকা গোনা
return (task) => {
if (used >= limit) return "সীমা শেষ — পরে চেষ্টা করুন";
used++;
return task();
};
}
const otpLimit = makeLimiter(3);
otpLimit(() => "OTP পাঠানো হলো"); // প্রথম ৩ বার: "OTP পাঠানো হলো"
// ৪র্থ বার থেকে: "সীমা শেষ — পরে চেষ্টা করুন"বাস্তব rate limiter সাধারণত একটি সময়ের জানালা ধরে গোনে (যেমন “প্রতি মিনিটে ৩ বার”) আর সময় পেরোলে গোনা নতুন করে শুরু করে; debounce আর throttle-এর মতো সময়ভিত্তিক closure পর্ব ৩৬-এ। আর মনে রাখুন: ব্রাউজারের এই সীমা শুধু ভালো ব্যবহারকারীর অভিজ্ঞতার জন্য — আসল সুরক্ষা সবসময় সার্ভারে থাকতে হবে, কারণ ব্রাউজারের কোড যে কেউ বদলাতে পারে।
C-তে closure নেই: function pointer-এর সাথে প্রসঙ্গ পাঠাতে আলাদা void * দিতে হয় (C-এর function pointer আর callback). C++11-এর lambda-তে আপনি বেছে নেন: [=] মানের কপি ধরে (পরের বদল দেখা যায় না — JavaScript-এর উল্টো), [&] reference ধরে (বাইরের variable মরে গেলে dangling) — JavaScript-এ GC থাকায় এই ভয় নেই (C++-এর lambda). Python-এর closure JavaScript-এর মতোই variable ধরে, তবে ভেতর থেকে বদলাতে nonlocal লিখতে হয় (Python-এর scope আর closure). Java-র lambda শুধু “effectively final” local ধরতে পারে, তাই সরাসরি makeCounter লেখা যায় না (Java-র lambda).
৫ · লুপের ক্লাসিক প্রশ্ন — প্রতি পুনরাবৃত্তিতে নতুন binding
পর্ব ০৭-এ প্রথম দেখেছিলেন: for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 10) ছাপে 3 3 3, আর var-এর জায়গায় let দিলে 0 1 2. তখন বলা হয়েছিল, “callback মান নয়, ঘর মনে রাখে।” এখন আপনার হাতে environment record-এর ছবি আছে — তাই এবার দেখা যাক ভাষার মানদণ্ড ঠিক কোন নিয়মে let-কে আলাদা করে।
প্রথমে মূল কথাটি আবার: callback তৈরি হয় লুপ চলার সময়, কিন্তু চলে লুপ শেষ হওয়ার পরে — কারণ setTimeout-এর callback চলে call stack খালি হলে (event loop, পর্ব ৩১)। তখন callback তার খাতায় variable-টির সেই মুহূর্তের মান পড়ে। তাই আসল প্রশ্ন: তিনটি callback-এর জন্মস্থান কি একটিই খাতা, না তিনটি আলাদা খাতা?
var:varblock মানে না;iথাকে ঘিরে থাকা function-এর (বা global-এর) খাতায় — একটিমাত্র binding. তিনটি callback একই খাতা ধরে রাখে।let: spec-এরforলুপের নিয়মে (ForBodyEvaluation) প্রতিটি পুনরাবৃত্তির শুরুতেCreatePerIterationEnvironmentনামের একটি ধাপ চলে: একটি নতুন খাতা তৈরি হয়, আগের খাতা থেকেj-এর বর্তমান মান কপি করা হয়, তারপর সেই নতুন খাতায়j++চলে। ফলে প্রতিটি পুনরাবৃত্তির নিজেরj— একে বলা যায় per-iteration binding.
1for (var i = 0; i < 3; i++)2 setTimeout(() => console.log(i), 10);3for (let j = 0; j < 3; j++)4 setTimeout(() => console.log(j), 20);
| var-এর খাতা | |
| let-এর খাতাগুলো | |
| কী ঘটছে |
- var লুপ, প্রথম পুনরাবৃত্তি।
iথাকে ঘিরে থাকা খাতা G-তে (এখানে global). Arrow function A তৈরি হলো; তার[[Environment]]পুরো লুপের একমাত্র খাতা G-এর দিকে।setTimeoutA-কে ১০ ms পরের জন্য জমা রাখে। - var লুপ শেষ। দ্বিতীয় আর তৃতীয় পুনরাবৃত্তিতে B আর C তৈরি হলো — দুজনেরই জন্মস্থান সেই একই G.
i++প্রতিবার G-এর একই ঘর বদলায়। শেষেi = 3, শর্ত মিথ্যা, লুপ থামে। তিনটি callback, একটি ঘর। - let লুপ, প্রথম পুনরাবৃত্তি। লুপ শুরুর আগে
j = 0একটি খাতায় তৈরি হয়, তারপর spec-এরCreatePerIterationEnvironmentতার একটি কপি L1 বানায়:{ j: 0 }. এই পুনরাবৃত্তির body-তে তৈরি callback D-এর জন্মস্থান L1. - পরের পুনরাবৃত্তি — আগে কপি, তারপর j++. Body শেষে আবার নতুন খাতা L2 তৈরি হয়, L1-এর
j(০) কপি হয়, তারপর L2-তেj++চলে → ১. L1 অক্ষত থাকে ০-তেই। Callback E-এর জন্মস্থান L2. ক্রমটি গুরুত্বপূর্ণ: বাড়ানো নতুন খাতায় হয় বলেই পুরোনো callback-এর মান বদলায় না। - let লুপ শেষ। L3-তে
j = 2, callback F. তারপর L4 তৈরি হয়েj = 3, শর্ত3 < 3মিথ্যা — body চলে না, কোনো callback L4 ধরে না, তাই সেটি শিগগিরই GC-র ভাগে যায়। - Timer চলল। ১০ ms-এ A, B, C চলে — সবাই G দেখে, যেখানে
i = 3:3 3 3. ২০ ms-এ D, E, F চলে — নিজের নিজের L1, L2, L3:0 1 2. কোড প্রায় একই, পার্থক্য শুধু কয়টি খাতা তৈরি হলো।
লুপের closure-বাগ আসলে “কয়টি binding” প্রশ্ন। var: পুরো লুপে একটি। for (let …): প্রতি পুনরাবৃত্তিতে একটি। for (const x of list)-এও প্রতি পুনরাবৃত্তিতে নতুন binding, তাই সেখানেও callback নিরাপদ। আর let i লুপের বাইরে ঘোষণা করে ভেতরে শুধু i++ করলে আবার একটি binding — বাগ ফেরত আসে।
ES2015-এর আগে মানুষ প্রতিটি পুনরাবৃত্তির জন্য নতুন খাতা বানাত একটি function ডেকে — হয় IIFE দিয়ে (পরের অংশ), নয়তো একটি factory দিয়ে: setTimeout(makeLogger(i), 10), যেখানে makeLogger(n) একটি function ফেরত দেয়। দুটোই একই কৌশল: function কল = নতুন খাতা = নতুন binding. let শুধু এই কৌশলটিকে ভাষার ভেতরে বসিয়ে দিয়েছে।
৬ · IIFE আর module pattern — ES module আসার আগের ইতিহাস
২০১৫-র আগে JavaScript-এ let/const-এর block scope ছিল না, ব্রাউজারে module-ও ছিল না। একটি পাতায় দশটি <script> ফাইল থাকলে সবার top-level var একই global খাতায় পড়ত। আপনার var count আর একটি slider লাইব্রেরির var count — একটি অন্যটিকে চুপচাপ মুছে দিত। নতুন scope বানানোর একমাত্র উপায় ছিল একটি function. তাই মানুষ পুরো ফাইলটিকে একটি function-এ মুড়ে সাথে সাথে ডেকে ফেলত — IIFE:
var Cart = (function () {
var items = []; // গোপন — বাইরে থেকে পৌঁছানো যায় না
function total() {
return items.reduce(function (s, x) { return s + x.price; }, 0);
}
return { // বাইরে শুধু এই "public API"
add: function (item) { items.push(item); },
total: total
};
})();
Cart.add({ name: "চা পাতা", price: 180 });
Cart.total(); // 180
Cart.items; // undefined — items লুকানোএটাই বিখ্যাত module pattern. IIFE একবার চলে, তার খাতায় items আর total থাকে। ফেরত দেওয়া object-এর method-গুলো সেই খাতার closure — তাই গোপন ডেটা ব্যবহার করতে পারে। global-এ যায় শুধু একটি নাম, Cart. শুরুর বন্ধনী (function …) দরকার, কারণ লাইনের শুরুতে function দেখলে engine সেটিকে declaration ভাবত, আর declaration সাথে সাথে ডাকা যায় না।
jQuery-র ফাইল খুললে দেখবেন পুরো লাইব্রেরি একটি বিশাল IIFE-এর ভেতরে। Douglas Crockford-এর লেখা আর Yahoo-র YUI লাইব্রেরির ব্লগ এই নকশাকে ব্যাপক জনপ্রিয় করে। পরে এর একটি রূপ “revealing module pattern” নামে পরিচিত হয় — সব function ভেতরে সাধারণভাবে লিখে, শেষে কোনগুলো বাইরে দেখানো হবে তার একটি তালিকা ফেরত দেওয়া। Node.js-এর CommonJS-ও আসলে প্রতিটি ফাইলকে গোপনে একটি function-এ মুড়ে চালায় — সেজন্যই সেখানে top-level var global-এ যায় না (পর্ব ০৭-এর টেবিল মনে করুন)।
আজ আপনার কাছে আছে ES module (import/export, পর্ব ৪৫): প্রতিটি ফাইল নিজেই একটি scope, যা export করা হয়নি তা গোপন। তাই নতুন কোডে IIFE-এর দরকার খুব কম। তবু দুটি জায়গায় এখনো দেখবেন: পুরোনো কোডবেস আর লাইব্রেরি, আর একটি async কাজ সাথে সাথে চালানোর জন্য (async () => { … })(); — যেখানে top-level await ব্যবহার করা যায় না।
৭ · Memoization আর once — closure-এ রাখা cache
একটি ই-কমার্স সাইটে ডেলিভারি চার্জ হিসাব করতে হয় জেলা অনুযায়ী — হয়তো দূরত্ব, কুরিয়ারের হার আর ওজনের একটি ভারী হিসাব। একই সেশনে একজন গ্রাহক ঢাকা বেছে নিলেন, তারপর খুলনা, তারপর আবার ঢাকা। তৃতীয়বার পুরো হিসাব আবার করার দরকার কী? প্রথম ফলটি মনে রাখলেই হয়। একই input-এ একই ফল দেয় এমন function-এর ফল জমিয়ে রাখার এই কৌশলের নাম memoization, আর জমানোর জায়গাটি একটি cache. Cache কোথায় রাখবেন? Closure-এ — যাতে সেটি কলের পর কলে টিকে থাকে, অথচ বাইরের কেউ গোলমাল করতে না পারে।
1function memoize(fn) {2 const cache = new Map();3 return (key) => {4 if (cache.has(key)) return cache.get(key);5 const v = fn(key); // ভারী হিসাব6 cache.set(key, v);7 return v;8 };9}10const charge = memoize(slowCharge);11charge("Dhaka"); charge("Khulna"); charge("Dhaka");
| কল | |
| cache | |
| slowCharge চলল? | |
| slowCharge মোট কল |
- Cache-এর জন্ম।
memoize(slowCharge)একবার চলে। তার খাতায় তৈরি হয় একটি খালিMap(পর্ব ১৮) আরfn = slowCharge. ফেরত দেওয়া arrow function —charge— এই খাতা ধরে রাখে। Cache এখন closure-এর ভেতরে: বাইরের কেউ একে দেখতে পায় না। - charge("Dhaka") — miss. Cache-এ “Dhaka” নেই, তাই লাইন ৫-এ আসল ভারী হিসাব চলে, ফল ধরা যাক ৬০ টাকা। ফলটি cache-এ লেখা হয় আর ফেরত যায়।
- charge("Khulna") — আবার miss. নতুন key, তাই আবার হিসাব — ১২০ টাকা। Cache-এ এখন দুটি entry. লক্ষ করুন, দুটি কলই একই closure-এর একই
cacheদেখছে — makeCounter-এরcount-এর মতোই। - আবার charge("Dhaka") — hit! লাইন ৪-এ
cache.has("Dhaka")সত্য, তাই সাথে সাথে ৬০ ফেরত —slowChargeআর চলেনি। ভারী হিসাব যত দামি, লাভ তত বেশি। শর্ত একটিই: function-টি pure হতে হবে — একই input-এ সবসময় একই ফল (পর্ব ১৩)। কুরিয়ারের হার দিনের মাঝে বদলালে পুরোনো cache ভুল দাম দেখাবে। - বিপদ — cache যদি কখনো না থামে। এই cache-এর কোনো সীমা নেই। key যদি হয় প্রতিটি ঠিকানা বা প্রতিটি search লেখা, তবে লাখ লাখ entry জমবে, আর
chargeযতদিন বেঁচে আছে cache-ও ততদিন — এটি একটি memory leak. সমাধান: সর্বোচ্চ আকার বেঁধে দিন আর সবচেয়ে পুরোনোটি মুছুন (LRU নীতি), বা সময় পেরোলে entry বাতিল করুন। Object-কে key বানালেWeakMap-ও একটি পথ।
এবার memoization-এর আসল জাদু দেখুন fibonacci-তে — সাথে লুপের প্রশ্নটি আর once. প্রথম দুই লাইনের timer-এর আউটপুট সবার শেষে আসবে, কারণ timer চলে sync কোড শেষ হওয়ার পরে:
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);
function memoize(fn) {
const cache = new Map();
return (n) => {
if (cache.has(n)) return cache.get(n);
const result = fn(n);
cache.set(n, result);
return result;
};
}
let calls = 0;
const fib = memoize((n) => { calls++; return n < 2 ? n : fib(n - 1) + fib(n - 2); });
console.log(fib(40), "calls:", calls);
function once(fn) {
let done = false, result;
return (...args) => {
if (!done) { done = true; result = fn(...args); }
return result;
};
}
const placeOrder = once((id) => `অর্ডার ${id} জমা হলো`);
console.log(placeOrder(7), "|", placeOrder(8));প্রথমে sync কোডের দুই লাইন। 102334155 calls: 41 — memoize ছাড়া সরল recursive fibonacci(40) প্রায় ৩৩ কোটি বার নিজেকে ডাকত; এখানে প্রতিটি n (০ থেকে ৪০) মাত্র একবার হিসাব হয়েছে, তাই ৪১টি কল। কৌশলটি হলো recursive কলগুলো fib নামে, অর্থাৎ memoize করা সংস্করণকে ডাকছে — তাই ভেতরের কলও cache দেখে। তারপর অর্ডার 7 জমা হলো | অর্ডার 7 জমা হলো — once-এর closure-এ থাকা done প্রথম কলের পরে সত্য হয়ে যায়; দ্বিতীয় কলে আসল function আর চলে না, আগের ফলই ফেরত আসে। “অর্ডার দিন” বোতামে দুবার ক্লিকে দুটি অর্ডার হওয়া ঠেকাতে এটি কাজের (যদিও আসল নিশ্চয়তা আবারও সার্ভারে)। শেষে timer: var 3 তিনবার, তারপর let 0, let 1, let 2 — অংশ ৫-এর ছবির মতোই।
৮ · Callback, async আর মেমরি — closure-এর অন্য পিঠ
Event handler আর callback — “মনে রাখা” স্বাভাবিকভাবে
আপনি সম্ভবত না জেনেই closure লিখে আসছেন। একটি বোতামে event listener লাগানোর সময়:
function setupLikeButton(button, postId) {
let likes = 0;
button.addEventListener("click", () => {
likes++; // closure-এ থাকা গোনা
button.textContent = `👍 ${likes} (post ${postId})`;
});
}setupLikeButton এক মুহূর্তে শেষ হয়ে যায়, কিন্তু ক্লিক আসে মিনিট বা ঘণ্টা পরে। তখনও handler likes, button আর postId জানে — কারণ সেগুলো তার জন্মস্থানের খাতায় আছে, আর ব্রাউজার handler-টিকে ধরে রেখেছে বলে খাতাটিও বেঁচে আছে। পাতায় বিশটি পোস্ট থাকলে বিশবার setupLikeButton ডাকা হবে — বিশটি আলাদা খাতা, বিশটি আলাদা গোনা, ঠিক makeCounter-এর মতো। Event-এর বিস্তারিত পর্ব ৩৮-এ।
Async কোডে stale মান
Closure ঘর ধরে রাখে — কিন্তু কোন ঘর? যে ঘর তৈরির মুহূর্তে scope-এ ছিল। Async কোডে এখান থেকে একটি সূক্ষ্ম বাগ আসে, যার নাম stale closure: callback একটি পুরোনো খাতার পুরোনো মান দেখে, অথচ আপনি চেয়েছিলেন “এখনকার” মান।
async function showPrice(productId) {
const currency = getCurrency(); // এই মুহূর্তের মুদ্রা, ধরা যাক "BDT"
const price = await fetchPrice(productId); // ২ সেকেন্ড অপেক্ষা
render(price, currency); // ইতিমধ্যে ব্যবহারকারী USD বেছে নিলেও এখানে "BDT"
}await-এর পরের অংশ আসলে একটি পরে-চলা callback (পর্ব ৩৪), আর সেটি currency-এর পুরোনো binding দেখে — const বলে সেটি আর কখনো বদলাবে না। সমাধান নির্ভর করে কী চান তার ওপর: অপেক্ষার পরে getCurrency() আবার ডাকুন, বা পুরোনো অনুরোধটি বাতিল করুন (পর্ব ৩৬-এর AbortController). মূল শিক্ষা: পরে চলা কোড যে variable পড়ছে, সেটি তৈরির মুহূর্তের নাকি এখনকার — প্রতিবার ভাবুন।
Memory — closure যা ধরে রাখে, তা মুছতে পারে না
GC শুধু সেই জিনিস মোছে, যেখানে আর পৌঁছানো যায় না (পর্ব ০৫)। একটি দীর্ঘজীবী closure — event handler, setInterval-এর callback, global-এ রাখা cache — তার খাতাকে পৌঁছানো-যায় অবস্থায় রাখে। খাতায় একটি বিশাল array থাকলে সেটিও থেকে যায়:
function setupReport(button) {
const rows = loadAllRows(); // ধরা যাক ৫ লাখ সারির বিশাল array
const summary = rows.length + " সারি";
const debug = () => rows[0]; // rows ব্যবহার করে — কখনো ডাকা হয় না
button.addEventListener("click", () => console.log(summary));
}Click handler শুধু summary ব্যবহার করে, তাই মনে হয় rows মুছে যাওয়ার কথা। কিন্তু V8-এর মতো engine সাধারণত একটি scope-এর যে variable-গুলো কোনো ভেতরের function ব্যবহার করে, সেগুলোকে একটি ভাগ করা context object-এ রাখে — আর সেই scope-এর সব closure একই context ধরে রাখে। এখানে debug rows ব্যবহার করেছে বলে rows সেই ভাগ করা context-এ গেছে; handler বেঁচে থাকলে context বেঁচে থাকে, তাই rows-ও। (কোনো ভেতরের function ব্যবহার না করা variable সাধারণত context-এ যায়ই না।) এটি engine-এর বাস্তবায়নের বিষয়, spec-এর নয় — তবে বাস্তবে এভাবেই leak হয়। সমাধান: অপ্রয়োজনীয় closure সরান, কাজ শেষে বড় ডেটার variable-এ null বসান, আর handler দরকার না থাকলে removeEventListener করুন।
DevTools-এ closure দেখা
Closure অদৃশ্য নয় — DevTools এগুলো সরাসরি দেখায়। Chrome বা Edge-এ Sources প্যানেলে closure-এর ভেতরের একটি লাইনে breakpoint বসান (বা কোডে debugger; লিখুন)। কোড থামলে ডান পাশের Scope অংশে পরপর দেখবেন: Local (এই কলের নিজের খাতা), Closure (makeCounter) (জন্মস্থানের খাতা — এখানে count আর label), Script (top-level let/const), আর Global. এটাই আক্ষরিক অর্থে scope chain-এর ছবি। Console-এ console.dir(cash) লিখে function-টি খুললে ভেতরের [[Scopes]] ঘরেও একই শিকল দেখা যায়। Memory প্যানেলের heap snapshot-এ “Retainers” দেখে বোঝা যায় কোন closure একটি বড় object-কে বাঁচিয়ে রেখেছে — বিস্তারিত পর্ব ৫৩-এ।
- Closure কী? — একটি function আর তার জন্মস্থানের (lexical) variable-গুলোর জোড়; বাইরের function শেষ হওয়ার পরেও function সেই variable পড়তে ও বদলাতে পারে।
for (var i…) setTimeout(() => console.log(i))কী ছাপে, কেন, আর সমাধান? —3 3 3; সব callback একটিvarbinding দেখে। সমাধানlet(প্রতি পুনরাবৃত্তিতে নতুন binding), বা IIFE/factory দিয়ে নতুন scope.- Closure মানের কপি রাখে, না variable? — variable (binding); তৈরির পরে বাইরের মান বদলালে closure নতুন মানটিই দেখে।
- Closure দিয়ে private variable কীভাবে বানাবেন? — variable একটি function-এর ভেতরে রেখে শুধু তাকে ব্যবহারকারী method-গুলো ফেরত দিন (module pattern); আধুনিক বিকল্প class-এর
#privatefield. - Closure কি memory leak ঘটাতে পারে? — হ্যাঁ, দীর্ঘজীবী closure (handler, timer, cache) তার scope-এর বড় ডেটা বাঁচিয়ে রাখলে; handler সরানো আর cache-এর সীমা দেওয়া সমাধান।
৯ · AI-এর যুগে — AI-এর লেখা closure যাচাই করা
AI টুলগুলো closure খুব ভালো লেখে — কারণ ইন্টারনেটে এর কোটি উদাহরণ। সমস্যা হলো, সেই উদাহরণগুলোর বড় অংশ পুরোনো বা খেলনা। তিনটি ফাঁদ বিশেষভাবে চোখে রাখুন।
১. লুপে var. পুরোনো Stack Overflow উত্তর থেকে শেখা মডেল এখনো মাঝে মাঝে for (var i …) লিখে ভেতরে event handler বা timer বসায় — সব বোতাম শেষ index দেখায়।
২. React hook-এ stale closure. React-এর function component প্রতিটি render-এ নতুন করে চলে, তাই প্রতিটি render-এর নিজের খাতা, নিজের count. AI প্রায়ই এমন কোড দেয় (চালানো যায় না, React লাগে):
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000); // count সবসময় 0
return () => clearInterval(id);
}, []); // খালি dependency: effect শুধু প্রথম render-এ
return <p>{count}</p>;
}
// প্রত্যাশিত আচরণ: 0 → 1 → 1 → 1 … (আটকে যায়)
// ঠিক রূপ: setCount((c) => c + 1)Effect চলে শুধু প্রথম render-এ, তাই interval-এর callback চিরকাল প্রথম render-এর খাতা ধরে রাখে, যেখানে count = 0. প্রতি সেকেন্ডে সে setCount(0 + 1) করে — মান ১-এ আটকে যায়। সমাধান: functional update setCount((c) => c + 1), যা পুরোনো খাতার মান না পড়ে React-এর কাছ থেকে সবশেষ মান নেয়; অথবা dependency তালিকায় count দেওয়া। React-এর পরিচয় পর্ব ৫৪-এ।
৩. চিরকাল বাড়তে থাকা cache. “এই function-টি দ্রুত করো” বললে AI প্রায়ই একটি সীমাহীন Map দিয়ে memoize করে — key হিসেবে user input বা object. ডেমোতে চমৎকার, সার্ভারে কয়েক দিন চললে মেমরি শেষ।
AI-কে closure-ভিত্তিক কোড লেখাতে চাইলে ঝুঁকিগুলো আগে থেকেই বলে দিন:
AI-এর লেখা closure হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Closure” শব্দটি কোথা থেকে এল? ১৯৬০-এর দশকের একটি গবেষণাপত্র আর ১৯৭০-এর দশকের একটি ছোট ভাষা কীভাবে আজকের JavaScript-এর ভিত্তি তৈরি করল?
১৯৬৪ সালে ব্রিটিশ কম্পিউটার বিজ্ঞানী Peter Landin একটি গবেষণাপত্র প্রকাশ করেন — “The Mechanical Evaluation of Expressions”. সেখানে তিনি SECD machine নামে একটি তাত্ত্বিক যন্ত্র বর্ণনা করেন, যা lambda calculus-এর ধাঁচে লেখা expression হিসাব করে। একটি সমস্যা তাঁকে সমাধান করতে হয়েছিল: একটি function-কে যদি মান হিসেবে অন্য জায়গায় পাঠানো হয়, সে তার free variable-গুলোর মান কোথায় পাবে? Landin-এর সমাধান: function-এর কোডের সাথে তার তৈরির মুহূর্তের environment জুড়ে একটি জোড় বানাও। এই জোড়কে তিনি নাম দেন closure — কারণ environment দিয়ে function-এর “খোলা” free variable-গুলো “বন্ধ” (closed) করা হলো। আজ আমরা যে শব্দ ব্যবহার করি, তা এই গাণিতিক অর্থ থেকেই।
কিন্তু তখনকার জনপ্রিয় Lisp-গুলো dynamic scope-এ চলত, আর function-কে মান হিসেবে পাঠানোর সমস্যাটি (“funarg problem”) পুরোপুরি মেটেনি। মোড় ঘোরে ১৯৭৫ সালে। MIT-তে Gerald Jay Sussman আর Guy L. Steele Jr. Carl Hewitt-এর actor model বোঝার চেষ্টা করতে গিয়ে একটি ছোট Lisp লেখেন — Scheme. প্রকাশিত হয় “Scheme: An Interpreter for Extended Lambda Calculus” নামের একটি AI Memo হিসেবে। Scheme lexical scope আর সত্যিকারের closure নিয়ে আসে। তাঁরা একটি চমকপ্রদ জিনিস আবিষ্কার করেন: একটি actor আর একটি closure কার্যত একই জিনিস — দুটোই কিছু অবস্থা ধরে রেখে বার্তার উত্তর দেয়।
Scheme ছোট ছিল, কিন্তু প্রভাব বিশাল। Sussman আর Harold Abelson-এর বই “Structure and Interpretation of Computer Programs” (১৯৮৫) Scheme দিয়ে এক প্রজন্মকে প্রোগ্রামিং শেখায়, আর closure দিয়ে counter, ব্যাংক অ্যাকাউন্ট, এমনকি object system বানানো দেখায় — ঠিক এই পর্বের makeWallet-এর মতো। Common Lisp lexical scope গ্রহণ করে, পরে ML, Haskell আর শেষে প্রায় সব আধুনিক ভাষা।
JavaScript-এর গল্প এখানে ঢোকে ১৯৯৫ সালে, যা দ্বিতীয় প্রশ্নের বিষয়। শুধু এটুকু মনে রাখুন: যখন আপনি makeCounter লেখেন, আপনি এমন একটি ধারণা ব্যবহার করছেন যার নাম প্রায় ষাট বছর পুরোনো আর যার ব্যবহারিক রূপ পঞ্চাশ বছর।
মূল উপলব্ধি: Closure কোনো JavaScript-এর বিশেষ কৌশল নয়, বরং প্রোগ্রামিং ভাষার তত্ত্বের একটি মৌলিক ধারণা: “function + environment”. ধারণাটি গণিতে জন্মে, Scheme-এ ব্যবহারিক হয়, আর JavaScript-এর হাত ধরে কোটি মানুষের দৈনন্দিন যন্ত্র হয়েছে।
প্র ০২Brendan Eich ব্রাউজারে Scheme বসাতে চেয়েছিলেন — সেই ইচ্ছা পুরো পূরণ না হলেও JavaScript-এ কী থেকে গেল, আর তার ফল আজও কোথায় দেখা যায়?
১৯৯৫ সালে Netscape Brendan Eich-কে নিয়োগ দেয়, আর তাঁকে আকর্ষণ করা হয়েছিল “ব্রাউজারে Scheme বসানো”র প্রস্তাব দিয়ে। কিন্তু তিনি যোগ দেওয়ার সময়ের মধ্যে পরিস্থিতি বদলে যায়: Netscape তখন Sun Microsystems-এর সাথে Java নিয়ে জোট বাঁধছে, আর ব্যবস্থাপনা চাইল নতুন script ভাষাটি দেখতে Java-র “ছোট ভাই”-এর মতো হোক। ফলে Eich-কে বাইরে C/Java-র ব্যাকরণ দিতে হলো — বাঁকা বন্ধনী, for, if. কিন্তু ভেতরে তিনি রাখলেন Scheme-এর দুটি মূল জিনিস: first-class function আর lexical scope — এর মানে, closure প্রথম দিন থেকেই ভাষার অংশ।
এই সিদ্ধান্তের ফল বিশাল। ব্রাউজার মূলত ঘটনা-নির্ভর পরিবেশ: ক্লিক, টাইমার, নেটওয়ার্কের উত্তর। প্রতিটি ঘটনার জন্য একটি function দিতে হয়, আর সেই function-এর জানতে হয় কোন বোতাম, কোন ফর্ম, কোন ডেটার জন্য সে কাজ করছে। Closure থাকায় এটি স্বাভাবিকভাবেই হয় — handler তার জন্মস্থানের variable মনে রাখে। Java-তে একই কাজের জন্য তখন anonymous inner class লিখতে হতো, আর সেটিও শুধু final variable ধরতে পারত। Java-তে lambda আসে ২০১৪ সালে (Java 8), আর তখনো “effectively final” শর্ত থেকে যায়।
অনেক বছর পর্যন্ত মানুষ বুঝত না যে JavaScript-এর আসল শক্তি এখানে। প্রথম দশকে বেশিরভাগ লোক একে “খেলনা Java” ভেবে লিখত। পরিবর্তন আসে ২০০০-এর দশকের মাঝামাঝি — Ajax-এর যুগে বড় ওয়েব অ্যাপ বানাতে গিয়ে মানুষ closure-এর ওপর ভর করে module pattern, callback-নির্ভর লাইব্রেরি আর jQuery-র নকশা তৈরি করে। ২০০৯ সালে Node.js আসে, যার পুরো non-blocking নকশা দাঁড়িয়ে আছে callback-এর ওপর — প্রতিটি callback একটি closure, যা অনুরোধের প্রসঙ্গ মনে রাখে।
আজকের React-এর hook-ও closure-এর ওপর দাঁড়ানো: প্রতিটি render একটি function কল, আর প্রতিটি event handler সেই render-এর খাতা ধরে রাখে। এ কারণেই stale closure আজকের ফ্রন্টএন্ড ডেভেলপারদের অন্যতম সাধারণ বাগ — ১৯৯৫-এর একটি নকশা-সিদ্ধান্তের দীর্ঘ ছায়া।
মূল উপলব্ধি: চাপের মধ্যে নেওয়া একটি আপস — বাইরে Java, ভেতরে Scheme — JavaScript-কে ঘটনা-নির্ভর জগতের জন্য আশ্চর্যরকম উপযুক্ত করেছে। একটি ভাষাকে বুঝতে হলে তার ব্যাকরণ নয়, তার মূল মডেল খুঁজুন।
প্র ০৩ES module আসার আগে বড় JavaScript প্রজেক্টগুলো কীভাবে টিকে ছিল? Module pattern-এর উত্থান আর পতন থেকে কী শেখা যায়?
২০০০-এর দশকের শুরুর দিকে একটি সাধারণ ওয়েবসাইটে JavaScript ছিল কয়েকশো লাইন। কিন্তু Gmail (২০০৪) আর Google Maps (২০০৫)-এর মতো অ্যাপ দেখাল যে ব্রাউজারে হাজার হাজার লাইনের অ্যাপ চলতে পারে। সমস্যা হলো, ভাষায় তখন module ব্যবস্থা ছিল না, block scope-ও ছিল না। সব ফাইল একই global scope ভাগ করত। দুটি লাইব্রেরি একই নামের global variable রাখলে একটি অন্যটিকে চুপচাপ ভেঙে দিত।
সমাধান এল ভাষার ভেতর থেকেই — closure দিয়ে। Douglas Crockford তাঁর লেখা আর বক্তৃতায় দেখান কীভাবে একটি function-এর ভেতরে private variable রেখে শুধু বাছাই করা method একটি object হিসেবে ফেরত দেওয়া যায়। Yahoo-র YUI লাইব্রেরির দল এই নকশা ব্যাপকভাবে ব্যবহার করে আর ২০০৭ সালের দিকে YUI ব্লগে “module pattern” নামে এর একটি জনপ্রিয় ব্যাখ্যা প্রকাশিত হয়। পরে Christian Heilmann “revealing module pattern” নামের রূপটি জনপ্রিয় করেন। jQuery, Underscore, Backbone — সে যুগের প্রায় সব বড় লাইব্রেরি একটি বিশাল IIFE-এর ভেতরে লেখা।
কিন্তু module pattern একটি সমস্যা মেটায়নি — নির্ভরতা (dependency). কোন ফাইল কোনটির ওপর নির্ভর করে, আর কোন ক্রমে <script> বসাতে হবে, তা হাতে ঠিক রাখতে হতো। তাই এল CommonJS (২০০৯, Node.js গ্রহণ করে), যা প্রতিটি ফাইলকে গোপনে একটি function-এ মুড়ে require আর module.exports দেয় — মূলত module pattern-কে যন্ত্রের হাতে তুলে দেওয়া। ব্রাউজারের জন্য এল AMD আর RequireJS. শেষে ES2015 ভাষার ভেতরেই import/export নিয়ে আসে, যা static — কোড না চালিয়েই নির্ভরতা বোঝা যায়, তাই bundler অব্যবহৃত কোড বাদ দিতে পারে (tree shaking).
আজ module pattern প্রায় অবসরে, কিন্তু এর ধারণাগুলো বেঁচে আছে: ES module-এর যা export হয়নি তা গোপন, ঠিক IIFE-এর খাতার মতো। বিস্তারিত পর্ব ৪৫-এ।
মূল উপলব্ধি: ভাষায় একটি সুবিধা না থাকলে কমিউনিটি বিদ্যমান যন্ত্র দিয়ে — এখানে closure — সেটি বানিয়ে নেয়। সফল নকশা পরে ভাষার অংশ হয়ে যায়। পুরোনো নকশা জানলে নতুন বৈশিষ্ট্যটি কেন এমন, তা বোঝা সহজ হয়।
প্র ০৪“Closure হলো গরিবের object” আর “object হলো গরিবের closure” — একটি বিখ্যাত koan দুটি কথাই বলে। দুটি কীভাবে একসাথে সত্য হতে পারে?
প্রোগ্রামিং জগতে একটি ছোট গল্প প্রচলিত আছে, যা Anton van Straaten ২০০০-এর দশকের শুরুর দিকে একটি আলোচনায় লিখেছিলেন — জেন বৌদ্ধ koan-এর ধাঁচে। গল্পে একজন ছাত্র গুরু Qc Na-কে বলে, “শুনেছি object খুব ভালো জিনিস।” গুরু বলেন, “বোকা — object তো শুধু গরিবের closure.” ছাত্র কয়েক সপ্তাহ Scheme-এ closure দিয়ে object system বানিয়ে ফিরে এসে বলে, “গুরু, এবার বুঝেছি object আসলে গরিবের closure.” গুরু তাকে লাঠি দিয়ে মেরে বলেন, “কবে শিখবে? closure হলো গরিবের object.” আর তখন ছাত্রটি জ্ঞানলাভ করে।
রসিকতার পেছনে একটি গভীর সত্য আছে: দুটি একই জিনিসের দুটি দৃষ্টিভঙ্গি — কিছু অবস্থা (state), আর সেই অবস্থার ওপর কাজ করে এমন আচরণ। এই পর্বের makeWallet দেখুন: balance হলো অবস্থা, deposit/withdraw হলো আচরণ। পর্ব ২৬-এর class দিয়ে লিখলে #balance field আর দুটি method — একই জিনিস, অন্য পোশাকে।
Closure-এর দিক থেকে দেখলে, একটি object হলো কয়েকটি function যারা একই environment ভাগ করে — object-এর “ভেতরের field” আসলে closure-এর খাতা। Object-এর দিক থেকে দেখলে, একটি closure হলো এমন একটি object যার মাত্র একটি method (“call”) আর কিছু গোপন field. Java ঠিক এভাবেই lambda বাস্তবায়ন করে: একটি lambda আসলে একটি functional interface-এর object, আর ধরা variable-গুলো তার field. C++-এর lambda-ও compiler-এর তৈরি একটি নামহীন class, যার operator() আছে।
তাহলে কোনটি “ভালো”? ব্যবহারিক উত্তর: কাজ অনুযায়ী। একটি আচরণ আর সামান্য অবস্থা হলে closure ছোট আর স্পষ্ট — once, makeDiscount, একটি handler. অনেক আচরণ, অনেক instance, inheritance, instanceof দিয়ে পরিচয় যাচাই আর মেমরিতে method ভাগাভাগি দরকার হলে class ভালো। JavaScript দুটোই সমানভাবে দেয় — এটা ভাষাটির বিরল শক্তি।
মূল উপলব্ধি: Object আর closure প্রতিদ্বন্দ্বী নয়, একই ধারণার — “অবস্থা + আচরণ, ভেতরটা লুকানো” — দুটি রূপ। একটি দিয়ে অন্যটি বানানো যায়। যখন দুটোকেই এভাবে দেখতে পারবেন, তখন কোনটি কোথায় খাটে তা স্বাভাবিকভাবে বুঝবেন।
প্র ০৫Engine আসলে closure কীভাবে বাস্তবায়ন করে? প্রতিটি function কলের খাতা কি সত্যিই heap-এ থাকে, আর তা হলে JavaScript এত দ্রুত কীভাবে?
Spec বলে প্রতিটি কলে একটি environment record তৈরি হয় — কিন্তু spec শুধু আচরণ বর্ণনা করে, বাস্তবায়ন নয়। প্রতিটি কলের সব variable heap-এ রাখলে খুব ধীর হতো: heap-এ জায়গা নেওয়া আর পরে GC-র মোছা — দুটোই খরচের। তাই engine-গুলো চালাকি করে। এখানে V8-এর সাধারণ পদ্ধতির একটি সরল ছবি দেওয়া হলো; বিস্তারিত সংস্করণভেদে বদলায়।
প্রথম ধাপ — parse করার সময় বিশ্লেষণ। কোড চালানোর আগেই engine দেখে নেয় প্রতিটি function-এর কোন variable কোনো ভেতরের function ব্যবহার করে। যে variable কেউ ধরে না, সেটি সাধারণ local — রাখা হয় stack frame বা register-এ, কল শেষে সাথে সাথে হারিয়ে যায়, GC-র কোনো খরচ নেই। যে variable কোনো ভেতরের function ব্যবহার করে, সেটি রাখা হয় heap-এর একটি context object-এ। ভেতরের function object সেই context-এর দিকে একটি pointer রাখে, আর context-গুলো নিজেরাও outer context-এর দিকে pointer রাখে — spec-এর scope chain-এর বাস্তব রূপ।
দ্বিতীয় ধাপ — একটি scope, একটি context. একই scope-এর সব closure সাধারণত একটি context ভাগ করে। এটি মেমরি বাঁচায় আর দুটি closure-কে একই variable দেখতে দেয় (makeWallet-এর তিনটি method একটি balance দেখে)। কিন্তু এর একটি পার্শ্বফল আছে, যা অংশ ৮-এ দেখেছেন: একটি closure যে variable ব্যবহারই করে না, সেটিও বেঁচে থাকতে পারে, শুধু অন্য একটি closure ব্যবহার করেছে বলে। ২০১৩ সালে Meteor দলের একজন প্রকৌশলী (David Glasser) এমন একটি অপ্রত্যাশিত leak নিয়ে একটি বহুল আলোচিত লেখা প্রকাশ করেন।
তৃতীয় ধাপ — optimizing compiler. V8-এর TurboFan-এর মতো optimizing compiler “escape analysis” নামের একটি কৌশল ব্যবহার করে: কোনো object বা context যদি function-এর বাইরে “পালায়” না, তাহলে কখনো কখনো heap-এ আদৌ তৈরি না করে সরাসরি register-এ রাখা যায়। Inlining-এর পরে অনেক ছোট closure কার্যত অদৃশ্য হয়ে যায়। তাই arr.map((x) => x * rate)-এর মতো কোড প্রায়ই সাধারণ লুপের কাছাকাছি দ্রুত চলে। আবার eval বা with থাকলে engine আগে থেকে বুঝতে পারে না কোন নাম কে ব্যবহার করবে, তাই সতর্ক হয়ে সব রেখে দেয় — এটি এদের এড়ানোর একটি কারণ।
মূল উপলব্ধি: Spec-এর মডেল (প্রতিটি কলে খাতা, শিকল ধরে খোঁজা) দিয়ে আচরণ বুঝুন, আর মনে রাখুন বাস্তব engine সেই আচরণ বজায় রেখে যতটা সম্ভব খরচ কমায়। বেশিরভাগ সময় closure-এর গতি নিয়ে চিন্তার দরকার নেই — চিন্তা করুন closure কী ধরে রাখছে আর কতক্ষণ।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।
-
আউটপুট অনুমান করুন:
let x = 1; const f = () => x; x = 2; const g = () => x; x = 3;— এখনf()আরg()কী দেবে? তারপর একই কোডেlet-এর জায়গায় প্রতিটি function-এর জন্য আলাদা factory (const make = (v) => () => v;) ব্যবহার করেf = make(x)লিখলে কী বদলাবে?দুটোই
3দেবে।fআরgদুজনেই একইxbinding ধরে রেখেছে, আর ডাকার মুহূর্তে সেই ঘরে ৩. Closure “ছবি” তোলে না।make(x)লিখলে প্রতিটি কলের নিজের parametervতৈরি হয়, আর argument-এর মান সেই মুহূর্তে কপি হয়ে যায় — তাইf = make(x)যখনx = 1, তখনf()সবসময় ১ দেবে। মান “জমাতে” চাইলে নতুন binding লাগে — নতুন function কল বা প্রতি-পুনরাবৃত্তিরlet. -
Counter-এর গোনা:
const a = makeCounter("A"); const b = a; const c = makeCounter("A");তারপরa(); b(); c(); a();— চারটি কল কী কী ফেরত দেবে? (makeCounterএই পর্বের মতো:`${label}-${++count}`)"A-1","A-2","A-1","A-3".b = aনতুন counter বানায় না — একই function-এর দিকে দ্বিতীয় একটি নাম (পর্ব ১৪-এর reference), তাই একই খাতা।cনতুনmakeCounterকল থেকে এসেছে — নতুন খাতা, নতুন গোনা, যদিও label একই। নাম বা label নয়, কতবার factory ডাকা হয়েছে তাই ঠিক করে কয়টি স্বাধীন অবস্থা। -
বাগ খুঁজুন: একটি পাতায় পাঁচটি ট্যাব বোতাম। কোড:
var tabs = document.querySelectorAll(".tab"); for (var k = 0; k < tabs.length; k++) { tabs[k].onclick = function () { showTab(k); }; }— যেকোনো ট্যাবে ক্লিক করলেshowTab(5)চলে, আর পঞ্চম ট্যাব বলে কিছু নেই। কেন? তিনটি ভিন্ন সমাধান লিখুন।পাঁচটি handler একটিই
var kbinding ধরে রেখেছে; ক্লিক আসে লুপ শেষ হওয়ার পরে, যখনk = 5. সমাধান: (১)for (let k = 0; …)— প্রতি পুনরাবৃত্তিতে নতুন binding. (২)tabs.forEach((tab, k) => { tab.onclick = () => showTab(k); });— প্রতিটি callback কল নিজেরkparameter পায়। (৩) ES2015-এর আগের রীতি:tabs[k].onclick = (function (n) { return function () { showTab(n); }; })(k);— IIFE নতুন খাতা বানায়। বাস্তবে আরও ভালো: index-এর ওপর নির্ভর না করে বোতামেdata-tabattribute রেখে event delegation (পর্ব ৩৮)। -
কোড লিখুন — বুথের টোকেন:
makeQueue(prefix)লিখুন, যা তিনটি method-সহ একটি object দেয়:next()পরের টোকেন দেয় ("A-001","A-002"…),served()এখন পর্যন্ত কয়টি দেওয়া হয়েছে বলে,reset()দিনশেষে আবার ১ থেকে শুরু করে। গোনাটি বাইরে থেকে বদলানো যাবে না।একটি সম্ভাব্য সমাধান:
function makeQueue(prefix) { let n = 0; return { next: () => `${prefix}-${String(++n).padStart(3, "0")}`, served: () => n, reset() { n = 0; }, }; } const q = makeQueue("A"); q.next(); q.next(); // "A-001", "A-002" q.served(); // 2তিনটি method একই খাতা ভাগ করে, তাই
reset-এর বদলnext-ও দেখে।padStart(পর্ব ০৯) সংখ্যাকে তিন অঙ্কে ভরে। বাইরের কোডq.n = 100লিখলে object-এ একটি নতুন অপ্রাসঙ্গিক property তৈরি হবে মাত্র — closure-এরnঅক্ষত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJavaScript-এ const c = makeCounter(); c(); চলার সময় ঘটনাগুলো কোন ক্রমে ঘটে, সাজান (▲▼ চেপে বা টেনে)।
- 📞 makeCounter কলের জন্য নতুন environment record তৈরি, তাতে count = 0
- 🧬 ভেতরের function তৈরি, তার [[Environment]]-এ সেই খাতা লেখা
- 📤 function ফেরত গেল, makeCounter-এর frame call stack থেকে সরল — খাতা বেঁচে রইল
- ▶️ c() ডাকা হলো, নতুন ছোট খাতা তৈরি, তার outer = makeCounter-এর খাতা
- 🔍 scope chain ধরে count খুঁজে পাওয়া গেল আর ১ বাড়ানো হলো
let n = 1; const show = () => n; n = 5; console.log(show()); — কী ছাপা হয়?
- 1
- 5
- undefined
- ReferenceError
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } — কী ছাপা হয়?
- 0 1 2
- 2 2 2
- 3 3 3
- undefined undefined undefined
JavaScript-এ lexical scope মানে কী?
- Function যেখানে লেখা হয়েছে, তার চারপাশের variable দেখে
- Function যেখান থেকে ডাকা হয়েছে, তার চারপাশের variable দেখে
- Function শুধু নিজের local variable দেখতে পারে
- শুধু var-এর জন্য scope ঠিক হয় চলার সময়
const a = makeCounter(); const b = makeCounter(); (প্রতিটি কল ভেতরে let count = 0 রাখে আর ++count ফেরত দেয় এমন function দেয়) — a(); a(); b(); চালালে শেষ কলটি কী দেয়?
- 3
- 2
- 0
- 1
একটি সীমাহীন Map-ভিত্তিক memoize cache একটি দীর্ঘজীবী সার্ভারে কী ঝুঁকি তৈরি করে?
- প্রতিটি কল আগের চেয়ে ধীর হয়ে যায়
- নতুন key জমতেই থাকে আর GC মুছতে পারে না — memory leak
- বাইরের কোড cache বদলে দিতে পারে
- নির্দিষ্ট সংখ্যার পরে Map একটি RangeError ছোড়ে
ES module আসার আগে JavaScript-এ module pattern কীভাবে variable গোপন রাখত?
- private keyword দিয়ে
- নামের আগে _ বসিয়ে
- একটি IIFE-এর ভেতরে variable রেখে শুধু বাছাই করা method closure হিসেবে ফেরত দিয়ে
- Object.freeze দিয়ে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।
মনে রাখুন
- Closure = function + জন্মস্থানের খাতা। Function তৈরির সময় চলতি environment তার
[[Environment]]-এ লেখা হয়; যতক্ষণ function বেঁচে, খাতাও বেঁচে। - নাম খোঁজা চলে scope chain ধরে: ভেতর → outer → … → global →
ReferenceError. শিকল ঠিক হয় কোড কোথায় লেখা তা দেখে। - Closure binding ধরে, মানের কপি নয়। প্রতিটি factory কল = নতুন খাতা = স্বাধীন অবস্থা (দুটি counter একে অপরকে ছোঁয় না)।
- লুপে
var= একটি binding (3 3 3);for (let …)= প্রতি পুনরাবৃত্তিতে নতুন binding (0 1 2). IIFE আর module pattern ছিল let আর ES module-এর আগের সমাধান। - Private state, factory, memoization,
once, handler — সব closure. কিন্তু closure যা ধরে রাখে তা মেমরিতেও থাকে: cache-এ সীমা দিন, async-এ stale মান নিয়ে ভাবুন, DevTools-এর Scope-এ যাচাই করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৩ · functional programming — pure function, currying আর compositionপরবর্তী পর্বClosure-এর ওপর দাঁড়ানো একটি পুরো চিন্তাপদ্ধতি।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ০৭ · let, const, var — scope, hoisting আর TDZফিরে দেখুনScope, shadowing আর লুপের var-বাগের প্রথম পরিচয়।
- Python-এর scope আর closureতুলনা করুনLEGB নিয়ম, nonlocal আর late binding-এর ফাঁদ।
- C++-এর lambda — capture by value আর referenceতুলনা করুনGC ছাড়া জগতে closure: কপি ধরবেন, না reference?