Skip to content
All posts
JavaScript

JavaScript Closure: সহজ ভাষায়, উদাহরণসহ

· 5 min read · #closure #scope #react

Closure এমন একটা জিনিস যেটা আমরা প্রায় প্রতিদিন use করি, অথচ নামটা শুনলেই অনেকের মনে হয় কঠিন কিছু। Interview-তেও এটা খুব common প্রশ্ন।

আসলে ব্যাপারটা সহজ। এই post-এ ছোট ছোট example দিয়ে দেখব closure কী, কেন কাজ করে, আর real project-এ কোথায় কোথায় এটা চুপচাপ কাজ করে যাচ্ছে।

আগে scope বুঝে নিই

JavaScript-এ একটা function কোন কোন variable দেখতে পাবে, সেটা ঠিক হয় function টা কোথায় লেখা হয়েছে তা দিয়ে, কোথা থেকে call করা হচ্ছে তা দিয়ে না। একে বলে lexical scope।

ভেতরের function বাইরের variable দেখতে পায়, কিন্তু বাইরের function ভেতরেরটা দেখতে পায় না।

scope.js
const site = "Portfolio";
 
function outer() {
  const message = "Hello";
 
  function inner() {
    console.log(message, site); // বাইরের দুটো variable-ই দেখা যাচ্ছে
  }
 
  inner();
}
 
outer(); // Hello Portfolio

inner নিজের ভেতরে message খুঁজে পায় না, তাই এক ধাপ বাইরে outer-এ খোঁজে, তারপর আরও বাইরে global scope-এ। এই খোঁজার পথটাকে বলে scope chain।

Closure তাহলে কী?

এবার একটু twist। ভেতরের function টাকে call না করে যদি return করে দিই?

counter.js
function createCounter() {
  let count = 0;
 
  return function () {
    count++;
    return count;
  };
}
 
const counter = createCounter();
 
counter(); // 1
counter(); // 2
counter(); // 3

স্বাভাবিকভাবে createCounter() শেষ হওয়ার সাথে সাথে তার local variable count হারিয়ে যাওয়ার কথা। কিন্তু return করা function টা এখনো count use করছে, তাই JavaScript সেটাকে memory-তে রেখে দেয়।

Function আর তার মনে রাখা variable গুলো, এই দুটো মিলেই closure।

প্রতিবার createCounter() call করলে নতুন একটা count তৈরি হয়, তাই প্রতিটা counter-এর memory আলাদা:

const a = createCounter();
const b = createCounter();
 
a(); // 1
a(); // 2
b(); // 1, b-এর নিজের আলাদা count

আরেকটা জরুরি কথা: closure variable-এর value copy করে রাখে না, variable টাকেই reference করে। পরে variable বদলালে closure নতুন value-ই দেখবে। নিচের দুটো common ভুল বুঝতে এই কথাটা কাজে লাগবে।

Real project-এ কোথায় লাগে

Private data

Class ছাড়াও closure দিয়ে এমন data রাখা যায়, যেটা বাইরে থেকে সরাসরি change করা যায় না:

wallet.js
function createWallet(initial) {
  let balance = initial;
 
  return {
    deposit(amount) {
      balance += amount;
    },
    getBalance() {
      return balance;
    },
  };
}
 
const wallet = createWallet(500);
 
wallet.deposit(200);
wallet.getBalance(); // 700
wallet.balance; // undefined, বাইরে থেকে ধরা যায় না

balance বদলানোর একমাত্র উপায় deposit। ভুল করে কেউ বাইরে থেকে balance = -100 লিখে দিতে পারবে না।

Function factory

একই ধরনের কিন্তু একটু আলাদা function বারবার লাগলে, একটা function দিয়েই অনেকগুলো বানিয়ে নেওয়া যায়:

multiplier.js
function multiplier(factor) {
  return (n) => n * factor;
}
 
const double = multiplier(2);
const triple = multiplier(3);
 
double(5); // 10
triple(5); // 15

double মনে রাখে factor = 2, আর triple মনে রাখে factor = 3।

Debounce

Search box-এ প্রতিটা keystroke-এ API call করলে server-এর উপর অযথা চাপ পড়ে। Debounce করলে user টাইপ থামানোর পর একবারই call হয়। এখানে timer variable টা থাকে closure-এর ভেতরে:

debounce.js
function debounce(fn, delay) {
  let timer;
 
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
 
const search = debounce((query) => {
  console.log("API call:", query);
}, 300);
 
input.addEventListener("input", (e) => search(e.target.value));

search() যতবারই call হোক, সবাই একই timer দেখে। তাই নতুন keystroke এলে আগের timer cancel হয়ে নতুন করে গোনা শুরু হয়।

দুটো common ভুল

var দিয়ে loop

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 4, 4, 4

তিনবারই 4 print হয়। কারণ var function-scoped, তাই পুরো loop-এ i একটাই। তিনটা callback-ই সেই একই i-কে reference করে, আর timeout চলার সময় loop শেষ, তখন i হয়ে গেছে 4।

Fix সহজ: let use করা। let block-scoped, তাই প্রতিটা iteration নিজের আলাদা i পায়:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 1, 2, 3

React-এ stale closure

React-এ প্রতিটা render আসলে component function-এর একটা নতুন call। প্রতিটা render-এর নিজের state value থাকে, আর সেই render-এ তৈরি হওয়া function গুলো সেই value-ই মনে রাখে।

Timer.jsx
function Timer() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // এখানে count সবসময় 0
    }, 1000);
    return () => clearInterval(id);
  }, []);
 
  return <p>{count}</p>;
}

Effect-এর dependency array খালি ([]), তাই effect শুধু প্রথম render-এ চলে। Interval-এর callback ধরে রাখে প্রথম render-এর closure, যেখানে count হলো 0। ফলে প্রতি সেকেন্ডে চলে setCount(0 + 1), আর screen-এ সংখ্যাটা 1-এ আটকে থাকে।

Fix হলো functional update। এখানে React নিজেই latest value দিয়ে দেয়, পুরোনো closure-এর count-এর উপর নির্ভর করতে হয় না:

setCount((c) => c + 1);

Memory নিয়ে একটা কথা

Closure যতক্ষণ বেঁচে থাকে, তার ধরে রাখা variable গুলোও ততক্ষণ memory-তে থাকে। সাধারণত এটা কোনো সমস্যা না। কিন্তু কোনো event listener বা interval যদি বড় data ধরে রাখে আর কখনো remove না হয়, তাহলে memory leak হতে পারে।

তাই কাজ শেষে removeEventListener আর clearInterval call করুন। React-এ এই কাজটাই করে useEffect-এর cleanup function, যেমন উপরের Timer-এ return () => clearInterval(id)।

শেষ কথা

  • Function তার বাইরের scope-এর variable মনে রাখে, বাইরের function শেষ হয়ে গেলেও। এটাই closure।
  • Closure value copy করে না, variable-কে reference করে।
  • Private data, function factory, debounce: সবখানেই closure কাজ করছে।
  • React-এ পুরোনো state দেখলে আগে stale closure সন্দেহ করুন।

পরের বার কোনো callback-এ "ভুল" value দেখলে নিজেকে একটা প্রশ্ন করুন: এই function টা কখন তৈরি হয়েছিল, আর তখন কোন variable দেখছিল? উত্তর সাধারণত সেখানেই থাকে।