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

Symbol, Proxy আর Reflect — JavaScript-এ মেটাপ্রোগ্রামিং

JavaScript Metaprogramming — Unique Symbol Keys, Well-Known Symbols, Proxy Traps, the Reflect API, Invariants and Decorators

Taught in Bangla, this lesson explores metaprogramming in JavaScript: Symbols as collision-free property keys, well-known symbols that hook into language behaviour, Proxy traps for validation, typo protection, read-only views and logging, the Reflect API for default behaviour, plus invariants, performance cost, debugging difficulty and decorators.

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

  • মেটাপ্রোগ্রামিং আসলে কী, আর আপনি আগে থেকেই কোথায় কোথায় এটি ব্যবহার করেছেন
  • Symbol কেন অনন্য, Symbol-key কোথায় লুকায়, Symbol.for কী, আর Symbol.toPrimitive দিয়ে একটি Money object-কে `${price}`-এ “৳ ১২০.০০” বানানো
  • Proxy-র trap দিয়ে স্টকের যাচাই, ডিফল্ট মান, typo-রক্ষা, read-only view আর পরিবর্তনের লগ
  • Reflect কেন trap-এর ভেতরে লাগে, receiver কী, আর Proxy কোন নিয়ম ভাঙতে পারে না
  • Vue, Immer-এর মতো library-র ভেতরের “জাদু”, decorator-এর বর্তমান অবস্থা, আর AI-এর লেখা Proxy কোড যাচাই

১ · মেটাপ্রোগ্রামিং কী — যে কোড অন্য কোডের আচরণ দেখে ও বদলায়

এতদিন আপনি যে কোড লিখেছেন, তা কাজ করে ডেটা নিয়ে: দাম যোগ করা, তালিকা সাজানো, পাতায় লেখা দেখানো। এবার একটু ওপরের তলায় উঠি। ধরুন আপনি চান, একটি দোকানের স্টক object-এ কেউ ভুল করে -5 বসাতে গেলেই সাথে সাথে error হোক — কোডের যে জায়গা থেকেই লেখা হোক না কেন। অথবা চান, settings object-এর প্রতিটি পরিবর্তন নিজে থেকে লগ হোক। এখানে আপনি আর ডেটা নিয়ে কাজ করছেন না; কাজ করছেন “object পড়া বা লেখা হলে কী ঘটবে” সেই নিয়ম নিয়ে। কোড দিয়ে কোডের আচরণ ঠিক করার এই কৌশলের নাম মেটাপ্রোগ্রামিং (metaprogramming).

শুনতে কঠিন লাগলেও আপনি এর স্বাদ আগেই পেয়েছেন। পর্ব ১৪-এ getter আর setter দেখেছেন — obj.total পড়লে আসলে একটি function চলে। Object.defineProperty দিয়ে property-কে read-only বা লুকানো বানিয়েছেন। Object.freeze দিয়ে পুরো object-কে বদলানোর অযোগ্য করেছেন। আর পর্ব ২৭-এ দেখেছেন, [Symbol.iterator] method যোগ করলেই আপনার নিজের object for…of-এ চলে। এগুলো সবই ভাষার “স্বাভাবিক আচরণ” বদলানোর উপায়। এই পর্বে তার পূর্ণ হাতিয়ার-বাক্স খুলব।

একটি বড় অফিস ভবনের কথা ভাবুন। ভেতরের কর্মীরা (ডেটা) নিজের কাজ করেন। মেটাপ্রোগ্রামিং হলো ভবনের নিয়মকানুন নিয়ে কাজ। Symbol হলো প্রতিটি লকারের অনন্য চাবি — দুটি চাবি দেখতে যত একরকমই হোক, একটি দিয়ে আরেকটির লকার খোলে না। Proxy হলো রিসেপশনের দারোয়ান — কেউ কোনো কর্মীর সাথে দেখা করতে এলে সে আগে খাতায় নাম লেখায়, পরিচয় যাচাই করে, দরকার হলে ফিরিয়ে দেয়। আর Reflect হলো দারোয়ানের হাতের “সাধারণ নিয়মের বই” — যাচাই শেষে অতিথিকে ঠিক সেই পথেই পাঠাতে হবে, যে পথে দারোয়ান না থাকলেও তিনি যেতেন।

Symbol

অনন্য, সংঘর্ষহীন property-র নাম। বিশেষ “well-known” symbol দিয়ে আপনার object ভাষার ভেতরের কাজে যুক্ত হয় — iteration, primitive-এ রূপান্তর, instanceof.

Proxy

একটি object-এর মোড়ক, যা পড়া, লেখা, in, delete, key-তালিকা, function call আর new-এর মতো মৌলিক কাজ মাঝপথে ধরে নিজের নিয়ম চালায়।

Reflect

ঠিক ওই মৌলিক কাজগুলোর “স্বাভাবিক” রূপ, function আকারে। Proxy-র প্রতিটি trap-এর একটি জোড়া-function আছে, আর ফল দেয় সোজা true/false.

তিনটিই এসেছে একসাথে, ES2015-এ — কারণ এরা একে অপরের পরিপূরক। Proxy-র নকশা করতে গিয়েই বোঝা যায়, ভাষার মৌলিক কাজগুলোর জন্য একটি পরিষ্কার function-তালিকা দরকার (Reflect), আর ভাষার নতুন “হুক” যোগ করতে দরকার এমন নাম, যা পুরোনো কোডের কোনো property-র সাথে মিলে যাবে না (Symbol). একে বলা যায় JavaScript-এর reflection ব্যবস্থা — চলার সময় প্রোগ্রাম নিজের গঠন দেখতে ও বদলাতে পারে।

মূল ধারণা

সাধারণ কোড প্রশ্ন করে “দাম কত?” মেটাপ্রোগ্রামিং প্রশ্ন করে “কেউ দাম পড়তে চাইলে কী ঘটবে?” প্রথমটি ডেটা নিয়ে, দ্বিতীয়টি আচরণের নিয়ম নিয়ে। এই ক্ষমতা শক্তিশালী, কিন্তু নিয়ম লুকিয়ে থাকে বলে কোড পড়া কঠিন হয় — তাই এর জায়গা মূলত library, framework আর সীমানা-পাহারায় (যেমন ভুল ডেটা আটকানো), রোজকার সাধারণ কোডে নয়।

২ · Symbol — কখনো সংঘর্ষ হয় না এমন property-র নাম

পর্ব ০৬-এ দেখেছেন, JavaScript-এ সাতটি primitive টাইপের একটি হলো symbol. এবার দেখি সে আসলে কী কাজে লাগে। Symbol() ডাকলে প্রতিবার একটি সম্পূর্ণ নতুন, অনন্য মান তৈরি হয়। বন্ধনীর ভেতরে যে লেখা দেন, সেটি শুধু একটি বর্ণনা (description) — debugging-এ চেনার জন্য; পরিচয়ের অংশ নয়।

const a = Symbol("id");
const b = Symbol("id");
a === b;              // false — একই বর্ণনা, তবু আলাদা মান
a.description;        // "id"
a.toString();         // "Symbol(id)"
`${a}`;               // TypeError: Cannot convert a Symbol value to a string

শেষ লাইনটি ইচ্ছাকৃত। Symbol নিজে থেকে string-এ রূপান্তর হয় না — যাতে ভুল করে "user_" + sym লিখে কেউ অনন্য চাবিটিকে সাধারণ string বানিয়ে না ফেলে। দরকার হলে স্পষ্টভাবে String(sym) বা sym.description লিখতে হয়।

Symbol-কে property-র নাম হিসেবে

Symbol-এর আসল কাজ হলো object-এর property-র নাম (key) হওয়া। লিখতে হয় বর্গবন্ধনীতে — computed property name: { [AUDIT]: "চেক-৭" } বা item[AUDIT] = …. কেন string নয়? ধরুন আপনার দোকানের অ্যাপে একটি analytics library প্রতিটি পণ্য object-এ item.id বসিয়ে ট্র্যাক করে — অথচ আপনার নিজের id property আগে থেকেই আছে। দুটি কোড একই নামে লিখে একে অপরের ডেটা মুছে দিল। Library যদি const TRACK = Symbol("track") বানিয়ে item[TRACK] ব্যবহার করত, তাহলে কোনো সংঘর্ষই সম্ভব হতো না — ওই Symbol-টি যার হাতে নেই, সে ওই property-তে ভুল করেও পৌঁছাতে পারবে না।

Symbol-key আরেকটি বৈশিষ্ট্য পায়: সাধারণ key-তালিকা বানানোর বেশিরভাগ পথে সে দেখা যায় না. কোন পথে দেখা যায় আর কোন পথে যায় না, ধাপে ধাপে দেখুন:

1const AUDIT = Symbol("audit");2const item = { name: "চাল", price: 6500, [AUDIT]: "চেক-৭" };3for (const k in item) console.log(k);4console.log(Object.keys(item));5console.log(JSON.stringify(item));6console.log(Object.getOwnPropertySymbols(item));7console.log(Reflect.ownKeys(item));
এই পথে যা দেখা গেল
name, price
[AUDIT]
  1. তিনটি property, এক object. name আর price সাধারণ string-key. তৃতীয়টির key একটি Symbol, তাই লিখতে হয়েছে বর্গবন্ধনীতে। মেমরিতে তিনটিই সমান সত্যিকারের own property — পার্থক্য শুধু key-এর টাইপে।
  2. for…in — Symbol লুকানো। for…in শুধু string-key-এর enumerable property ঘোরে (prototype-এর enumerable property-সহ)। Symbol-key এখানে আসেই না। তাই পুরোনো কোনো লুপ — যা কখনো Symbol-এর কথা জানত না — নতুন Symbol-property দেখে বিভ্রান্ত হয় না।
  3. Object.keys — একই নিয়ম। Object.keys, Object.values, Object.entries শুধু নিজস্ব string-key দেয়। ফলে কোনো library আপনার object-এ Symbol-key বসালেও আপনার টেবিলে বা ফর্মে বাড়তি কলাম উদয় হয় না।
  4. JSON.stringify — Symbol বাদ। পর্ব ১৯-এ দেখেছেন, JSON শুধু string-key চেনে। Symbol-key আর Symbol-মান দুটোই নীরবে বাদ পড়ে। ভেতরের হিসাবের তথ্য (যেমন অডিট নোট) তাই সার্ভারে পাঠানো ডেটায় ঢোকে না — সুবিধা, আবার না জানলে চমকও।
  5. Object.getOwnPropertySymbols — শুধু Symbol. Symbol-key লুকানো, কিন্তু গোপন নয়. এই function যে কাউকে object-এর সব Symbol-key দিয়ে দেয়, আর সেটি দিয়ে মানও পড়া যায়। অর্থাৎ Symbol সংঘর্ষ ঠেকায়, নিরাপত্তা দেয় না — সত্যিকারের গোপন ডেটার জন্য class-এর #private field (পর্ব ২৬)।
  6. Reflect.ownKeys — সবকিছু। String আর Symbol মিলিয়ে সব নিজস্ব key — enumerable না হলেও। মেটাপ্রোগ্রামিং কোড (যেমন একটি সম্পূর্ণ deep copy বা Proxy-র ownKeys trap) এটিই ব্যবহার করে, কারণ সে কিছুই বাদ দিতে চায় না।

Symbol.for — সবার জন্য একটি ভাগাভাগির খাতা

কখনো কখনো উল্টোটাই দরকার: একই Symbol চাই আলাদা ফাইল, আলাদা library, এমনকি পাতার iframe থেকেও। এর জন্য আছে global symbol registry — একটি বৈশ্বিক খাতা। Symbol.for("app.theme") খাতায় ওই নাম খোঁজে; থাকলে সেই Symbol-ই ফেরত দেয়, না থাকলে নতুন বানিয়ে খাতায় তোলে। তাই Symbol.for("app.theme") === Symbol.for("app.theme") হলো true, আর Symbol.keyFor(sym) খাতার নামটি ফেরত দেয় (সাধারণ Symbol()-এর বেলায় undefined). মনে রাখুন, খাতাটি বৈশ্বিক — নাম মিলে গেলে সংঘর্ষ আবার ফিরে আসে, তাই "আপনার-library.কাজ" ধরনের লম্বা, স্পষ্ট নাম ব্যবহার করুন।

৩ · Well-known symbol — ভাষার ভেতরের হুকে নিজের object জোড়া

JavaScript নিজেও কিছু Symbol বানিয়ে Symbol-এর গায়ে রেখে দিয়েছে: Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag, Symbol.hasInstance, Symbol.asyncIterator আর আরও কয়েকটি (মোট ১৩টি)। এদের বলে well-known symbol. প্রতিটি ভাষার একটি নির্দিষ্ট কাজের হুক: ভাষা ওই কাজ করার আগে দেখে, object-এ ওই Symbol-key-তে কোনো method আছে কি না। থাকলে সেটি ডাকে। এভাবে আপনার নিজের object ভাষার built-in আচরণে অংশ নিতে পারে — কোনো নতুন keyword ছাড়াই।

Symbolভাষা কখন খোঁজেউদাহরণ
Symbol.iteratorfor…of, spread [...x], destructuring, Array.fromনিজের তালিকা-object-কে iterable বানানো — পর্ব ২৭
Symbol.asyncIteratorfor await…ofপাতার পর পাতা আসা API-র ফল একে একে পড়া (ES2018; async নিয়ে পর্ব ৩৪)
Symbol.toPrimitive+x, x * 2, `${x}`, x + "", x == yMoney object-কে সংখ্যা বা “৳ ১২০.০০” লেখায় রূপান্তর
Symbol.toStringTagObject.prototype.toString.call(x)"[object Object]"-এর বদলে "[object Money]"
Symbol.hasInstancex instanceof Cনিজের নিয়মে “এটি কি এই ধরনের?” ঠিক করা

Symbol.toPrimitive — object কখন সংখ্যা, কখন লেখা

পর্ব ০৬-এ দেখেছেন, object-কে primitive-এ বদলাতে ভাষা ভেতরে ToPrimitive নিয়ম চালায় — সাধারণত valueOf() আর toString() দিয়ে। কিন্তু object-এ [Symbol.toPrimitive](hint) method থাকলে ভাষা সরাসরি সেটিই ডাকে, আর সাথে একটি ইঙ্গিত (hint) দেয় — কোন প্রসঙ্গে রূপান্তর হচ্ছে: "number", "string" নাকি "default" (যখন দুটোই হতে পারে)। একটি টাকার object দিয়ে প্রতিটি প্রসঙ্গ দেখুন:

1const price = new Money(12000);   // ১২০ টাকা, পয়সায়2`দাম: ${price}`3+price4price * 35"মোট " + price6price == "৳ ১২০.০০"
রূপান্তরের ভেতরে
hint
method ফেরত দিল
পুরো expression
  1. Template literal → "string". ${…}-এর ভেতরে ফল নিশ্চিতভাবে লেখা হবে, তাই hint "string". আমাদের method এখানে বাংলা অঙ্কে টাকার চিহ্নসহ লেখা দেয়। String(price)-ও একই hint পাঠায়।
  2. Unary + → "number". +price মানে “একে সংখ্যা বানাও”, তাই hint "number", আর method টাকার পরিমাণ সংখ্যায় দেয় — ১২০. Number(price), Math.round(price)-ও এই পথে আসে।
  3. গুণ, ভাগ, তুলনা → "number". *, -, /, <, > শুধু সংখ্যা নিয়ে কাজ করে, তাই এখানেও "number": ১২০ × ৩ = ৩৬০. price > 100 লিখলে পাবেন true.
  4. Binary + → "default". + সংখ্যা যোগও করে, লেখা জোড়াও দেয় — ভাষা জানে না আপনি কোনটি চান। তাই hint "default". আমাদের method default-এ লেখা দেয়, ফলে "মোট ৳ ১২০.০০". (Date-এর নিজের method default-কে লেখা ধরে, বাকি built-in object সাধারণত সংখ্যা।)
  5. == → "default". Loose equality-তে object-কে primitive বানাতে হয়, আর এখানেও প্রসঙ্গ অস্পষ্ট — "default". ফেরত আসা লেখা ডানদিকের লেখার সাথে মিলে গেল, তাই true. শিক্ষা: এমন object-এ == এড়িয়ে চলুন, স্পষ্টভাবে তুলনা করুন।

Symbol.toStringTag আর Symbol.hasInstance

Object.prototype.toString.call(x) হলো টাইপ চেনার একটি পুরোনো, নির্ভরযোগ্য কৌশল — "[object Array]", "[object Map]" ইত্যাদি দেয়। নিজের class-এ get [Symbol.toStringTag]() { return "Money"; } লিখলে আপনার object-ও "[object Money]" দেখাবে; console আর কিছু debugging টুলও এই নাম দেখায়। Symbol.hasInstance আরও সাহসী: class-এ static [Symbol.hasInstance](x) লিখলে x instanceof ThatClass-এর উত্তর আপনি ঠিক করেন। যেমন static [Symbol.hasInstance](n) { return n % 2 === 0; } থাকলে 4 instanceof Even হয় true. মজার, কিন্তু সাবধান — যে পাঠক instanceof-এর স্বাভাবিক অর্থ (prototype chain-এ খোঁজা, পর্ব ২৫) জানেন, তিনি বিভ্রান্ত হবেন।

এবার সব একসাথে চালিয়ে দেখুন — একটি Money class, যার টাকা পয়সায় রাখা (পর্ব ১০-এর নিয়মে), আর Intl.NumberFormat (পর্ব ১০, ২০) দিয়ে বাংলা অঙ্কে লেখা:

const INTERNAL = Symbol("internal");
const bn = new Intl.NumberFormat("bn-BD", { minimumFractionDigits: 2 });
class Money {
  constructor(paisa) { this.paisa = paisa; this[INTERNAL] = "audit-7"; }
  get [Symbol.toStringTag]() { return "Money"; }
  [Symbol.toPrimitive](hint) {
    const taka = this.paisa / 100;
    return hint === "number" ? taka : `৳ ${bn.format(taka)}`;
  }
}
const price = new Money(12000);
console.log(`দাম: ${price}`);                 // hint "string"
console.log(price * 3, price > 100);          // hint "number"
console.log("মোট " + price);                  // hint "default"
console.log(Object.prototype.toString.call(price));

console.log(Object.keys(price), JSON.stringify(price));
console.log(Object.getOwnPropertySymbols(price), price[INTERNAL]);
console.log(Symbol("id") === Symbol("id"), Symbol.for("app.id") === Symbol.for("app.id"));

প্রথম তিন লাইন: দাম: ৳ ১২০.০০, 360 true, মোট ৳ ১২০.০০ — একই object প্রসঙ্গ বুঝে কখনো সংখ্যা, কখনো সুন্দর লেখা। তারপর [object Money] — toStringTag-এর কাজ। পরের লাইনে [ 'paisa' ] {"paisa":12000}: Symbol-key-টি Object.keys আর JSON দুই জায়গাতেই অদৃশ্য। কিন্তু [ Symbol(internal) ] audit-7 দেখায়, যার কাছে Symbol আছে সে মান পড়তে পারে, আর getOwnPropertySymbols দিয়ে যে কেউ Symbol-টি খুঁজেও নিতে পারে। শেষ লাইন false true: সাধারণ Symbol() প্রতিবার নতুন, Symbol.for খাতা থেকে একই মান ফেরত দেয়। খেয়াল করুন, Intl.NumberFormat একবার বানিয়ে বারবার ব্যবহার করা হয়েছে — প্রতিবার নতুন বানানো ধীর।

৪ · Proxy — object-এর সামনে একজন দারোয়ান

Symbol দিয়ে object ভাষার কিছু নির্দিষ্ট কাজে অংশ নেয়। কিন্তু যদি চান যেকোনো property পড়া বা লেখা ধরতে — এমনকি যে property এখনো নেই তারও? Getter/setter প্রতিটি নামের জন্য আলাদা লিখতে হয়, আগে থেকে নাম জানতে হয়। Proxy এই সীমা ভাঙে। তৈরি করার নিয়ম একটাই:

const proxy = new Proxy(target, handler);
// target  — আসল object (বা function), যাকে মোড়ানো হচ্ছে
// handler — একটি সাধারণ object, যার method-গুলো (trap) কাজ মাঝপথে ধরে

Target হলো আসল object — দোকানের গুদাম। Handler হলো দারোয়ানের নির্দেশনার খাতা। Handler-এর প্রতিটি method-কে বলে trap (ফাঁদ) — কারণ সে একটি নির্দিষ্ট মৌলিক কাজকে ফাঁদে ধরে। Handler খালি ({}) হলে Proxy সব কাজ সোজা target-এ পাঠিয়ে দেয়, কোনো পার্থক্য বোঝা যায় না। যে trap লিখবেন, শুধু সেই কাজটির নিয়ম বদলাবে। মোট ১৩টি trap আছে; প্রধানগুলো:

Trapকোন কাজ ধরেArgument
getp.x, p[key]target, key, receiver
setp.x = v (ফেরত দিতে হয় true/false)target, key, value, receiver
has"x" in ptarget, key
deletePropertydelete p.xtarget, key
ownKeysObject.keys(p), for…in, Reflect.ownKeystarget
applyp(…) — target যখন functiontarget, thisArg, args
constructnew p(…)target, args, newTarget
বাকি ৬টিdefineProperty, getOwnPropertyDescriptor, getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions—

এই ১৩টি কাজ এলোমেলোভাবে বাছা নয়। ECMAScript মানদণ্ডে প্রতিটি object-এর ১১টি “ভেতরের method” আছে — [[Get]], [[Set]], [[HasProperty]] ইত্যাদি — আর function-এর বাড়তি দুটি, [[Call]] ও [[Construct]]; মোট ১৩টি। obj.x থেকে শুরু করে Object.keys পর্যন্ত ভাষার প্রতিটি object-সংক্রান্ত কাজ শেষ পর্যন্ত এগুলোর কোনো একটিতে গিয়ে ঠেকে। Proxy আপনাকে সেই একদম নিচের স্তরটি নিজের হাতে লেখার সুযোগ দেয়। একটি দোকানের স্টক object-এ inventory.chal পড়লে ভেতরে কী ঘটে, ধাপে ধাপে দেখুন:

  1. আপনার কোডinventory.chalসাধারণ property পড়া
  2. Proxy[[Get]]handler-এ get আছে?
  3. get trapkey in target?আপনার নিয়ম
  4. Reflect.get(target, key, receiver)স্বাভাবিক নিয়ম
  5. target{ chal: 50, dal: 20 }আসল গুদাম
আপনার কোডদেখতে একদম সাধারণ
const inventory = new Proxy({ chal: 50, dal: 20 }, handler);
console.log(inventory.chal);   // কোডে কোথাও লেখা নেই যে মাঝে কেউ আছে
Engine-এর চোখেএটি সাধারণ object নয়
inventory  ──►  Proxy { [[Target]]: {…}, [[Handler]]: handler }
কাজ: [[Get]]("chal")
handler.get আছে?  →  হ্যাঁ  →  target-এ সরাসরি না গিয়ে trap ডাকো
get traptarget, key, receiver নিয়ে ডাক
get(target, key, receiver) {
  if (!(key in target)) return 0;       // "chal" আছে, তাই এই লাইন পার
  return Reflect.get(target, key, receiver);
}
target = আসল object · key = "chal" · receiver = inventory (Proxy নিজে)
Reflect.get → targetদারোয়ান না থাকলে যা হতো
Reflect.get(target, "chal", receiver)
  ├─ নিজস্ব property "chal"?  হ্যাঁ → 50
  ├─ না থাকলে → prototype chain-এ খোঁজা
  └─ getter হলে → this = receiver দিয়ে getter চালানো
ফেরার পথমান উল্টো পথে আপনার কোডে
target ──50──► Reflect.get ──50──► get trap ──50──► console.log → 50
trap চাইলে এখানে মান বদলাতে পারত — যেমন দাম টাকায় রূপান্তর, বা লগ লেখা
অজানা key: inventory.teltarget পর্যন্ত যাওয়াই হলো না
get trap:  "tel" in target?  →  না  →  return 0
target ছোঁয়া হয়নি · "tel" in inventory  →  false  (has trap নেই, তাই target-এর উত্তর)
  1. সাধারণ চেহারা। কোডে শুধু inventory.chal — যে লিখছে সে হয়তো জানেই না inventory একটি Proxy. এটিই Proxy-র শক্তি (ব্যবহারকারীকে কিছু বদলাতে হয় না) আর বিপদ (নিয়মটি চোখের আড়ালে)।
  2. Engine দেখে: এটি Proxy. Proxy-র ভেতরে দুটি লুকানো ঘর — target আর handler. Property পড়া মানে ভেতরের [[Get]] কাজ। Engine handler-এ get নামের method খোঁজে; পেলে target-এ সরাসরি না গিয়ে সেটিকে ডাকে। না পেলে কাজটি সোজা target-এ যেত।
  3. Trap চলে — আপনার নিয়ম। Trap তিনটি জিনিস পায়: আসল target, key "chal" (সবসময় string বা Symbol — arr[0] লিখলেও key আসে "0"), আর receiver — যে object-এর ওপর পড়া হচ্ছে, সাধারণত Proxy নিজে। আমাদের নিয়ম: key target-এ থাকলে স্বাভাবিক পথে যাও।
  4. Reflect.get — স্বাভাবিক নিয়ম। Trap নিজে target[key] না লিখে Reflect.get ডাকে, যা ঠিক সেই কাজটি করে যা Proxy না থাকলে হতো: নিজস্ব property, না পেলে prototype chain, আর getter থাকলে this হিসেবে receiver দিয়ে চালানো। শেষ অংশটি কেন জরুরি, অংশ ৬-এ দেখবেন।
  5. মান ফেরত আসে। Target থেকে ৫০ উল্টো পথে Reflect, trap হয়ে আপনার কোডে পৌঁছায়। Trap চাইলে মাঝপথে মানটি বদলে দিতে পারত। সব মিলিয়ে একটি property পড়ায় এখন বাড়তি দুটি function call — এই খরচের কথা অংশ ৭-এ।
  6. অজানা key — target ছোঁয়াই হয় না। inventory.tel-এ trap দেখে key নেই, তাই সোজা ডিফল্ট 0 ফেরত দেয়। কিন্তু "tel" in inventory এখনো false, কারণ in আলাদা কাজ (has trap), আর সেটি আমরা লিখিনি। শিক্ষা: একটি trap শুধু একটি কাজ বদলায় — বাকিগুলোর সাথে সামঞ্জস্য রাখার দায়িত্ব আপনার।

প্রথম ব্যবহার — না-থাকা key-তে ডিফল্ট মান

উপরের get trap-টিই একটি পূর্ণ কৌশল: স্টকে নেই এমন পণ্যের পরিমাণ undefined-এর বদলে 0. ফলে inventory.tel + 5 লিখলে NaN আসে না। Python-এর defaultdict যাঁরা চেনেন, তাঁরা মিল পাবেন। তবে খেয়াল রাখুন, এটি কোডের “ভুল ধরা” ক্ষমতা কমায় — বানান ভুল করে inventory.chaal লিখলেও চুপচাপ 0 আসবে। কোনটি চান — নীরব ডিফল্ট নাকি জোরে error — সেটি নকশার সিদ্ধান্ত; পরের অংশে উল্টোটিও দেখব।

৫ · Proxy দিয়ে বাস্তব কাজ — যাচাই, typo-রক্ষা, read-only আর লগ

একটি অনলাইন মুদি দোকানের গুদাম ভাবুন। স্টকের সংখ্যা বদলায় অনেক জায়গা থেকে — অর্ডার, ফেরত, নতুন চালান, অ্যাডমিন প্যানেল। প্রতিটি জায়গায় “সংখ্যা ঋণাত্মক হলো কি না” যাচাই লিখলে একদিন কেউ ভুলবেই। তার চেয়ে গুদামের দরজায় একজন দারোয়ান বসান — set trap — যে প্রতিটি লেখা যাচাই করবে। ঠিক মান হলে স্বাভাবিকভাবে লেখা হয়, ভুল হলে TypeError:

1set(target, key, value, receiver) {2  if (!Number.isInteger(value) || value < 0)3    throw new TypeError(`${String(key)}: স্টক ঋণাত্মক হতে পারে না`);4  console.log(`লগ: ${String(key)} ${target[key] ?? 0} → ${value}`);5  return Reflect.set(target, key, value, receiver);6}
trap-এর এই ডাক
লেখার কোড
key, value
যাচাই
target
  1. লেখা মানেই trap. inventory.dal = 15 লিখলে মান target-এ যায় না — engine handler-এর set ডাকে: target, key "dal", value 15, আর receiver (Proxy). inventory.dal -= 5 বা inventory.dal++ লিখলেও শেষে এই একই set ডাক আসে (আগে একটি get).
  2. যাচাই পাস। ১৫ পূর্ণসংখ্যা আর ঋণাত্মক নয়, তাই throw-এর লাইন বাদ। Number.isInteger একসাথে অনেক ভুল ধরে — "15" string, NaN, 2.5 কেজি-র মতো ভগ্নাংশ (যদি গুনতি একক চান)।
  3. লগ লেখা। লেখার আগে পুরোনো আর নতুন মান লগে যায় — একই trap-এ যাচাই আর অডিট দুটোই হলো। বাস্তবে এখানে সার্ভারে ঘটনা পাঠানো বা “স্টক ৫-এর নিচে” সতর্কবার্তাও দেওয়া যায়।
  4. Reflect.set লেখে, true ফেরত দেয়। স্বাভাবিক লেখাটি Reflect.set করে — setter থাকলে receiver দিয়ে চালায়, read-only হলে false দেয়। set trap-কে অবশ্যই truthy মান ফেরত দিতে হয়; false দিলে strict mode-এ (module আর class-এর ভেতরে সবসময়) ভাষা নিজেই TypeError দেয়: 'set' on proxy: trap returned falsish for property 'dal'.
  5. ভুল মান: যাচাই ফেল। এবার inventory.chal = -3. একই trap, একই প্রথম লাইন, কিন্তু শর্ত সত্য — মান ঋণাত্মক। কোড যে জায়গা থেকেই এসে থাকুক, এই দরজা পার হওয়ার উপায় নেই।
  6. TypeError — target অক্ষত। Trap error ছোড়ে, তাই Reflect.set পর্যন্ত যাওয়াই হয় না: চালের স্টক ৫০-ই থাকে। Error ডাকার জায়গায় পৌঁছায়, সেখানে try/catch দিয়ে ধরে ব্যবহারকারীকে বোঝানো যায় (error নিয়ে বিস্তারিত পরের পর্ব ২৯-এ)। ভুল ডেটা নীরবে ঢোকার চেয়ে জোরে ব্যর্থ হওয়া অনেক ভালো।

এবার পুরোটা চালিয়ে দেখুন — যাচাই করা গুদাম, ডিফল্ট মান, লগ, আর সাথে একটি config object, যা বানান ভুলে error দেয়:

const stock = { chal: 50, dal: 20 };
const inventory = new Proxy(stock, {
  get(target, key, receiver) {
    return key in target ? Reflect.get(target, key, receiver) : 0;   // অজানা পণ্য → 0
  },
  set(target, key, value, receiver) {
    if (!Number.isInteger(value) || value < 0)
      throw new TypeError(`${String(key)}: স্টক ঋণাত্মক হতে পারে না (${value})`);
    console.log(`লগ: ${String(key)} ${target[key] ?? 0} → ${value}`);
    return Reflect.set(target, key, value, receiver);
  },
});
console.log(inventory.chal, inventory.tel, "tel" in inventory);
inventory.dal -= 5;
inventory.tel = 12;
try { inventory.chal -= 60; } catch (e) { console.log(e.name + ": " + e.message); }
console.log(JSON.stringify(stock), inventory === stock);
const config = new Proxy({ apiUrl: "/api", timeout: 3000 }, {
  get(t, key, r) { if (key in t) return Reflect.get(t, key, r); throw new Error(`অজানা সেটিং "${String(key)}"`); },
});
console.log(config.timeout);
try { console.log(config.timout); } catch (e) { console.log(e.message); }

প্রথম লাইন 50 0 false — চাল আছে, তেল নেই বলে ডিফল্ট ০, কিন্তু in সত্যি কথাই বলে। তারপর লগ: dal 20 → 15 (-= আগে get, পরে set ডেকেছে) আর লগ: tel 0 → 12 — নতুন পণ্যও একই দরজা দিয়ে ঢুকেছে। chal -= 60 দিতে চাইল −১০, তাই TypeError: chal: স্টক ঋণাত্মক হতে পারে না (-10). পরের লাইন {"chal":50,"dal":15,"tel":12} false: সব লেখা আসলে আসল stock-এ গেছে, অথচ Proxy আর target আলাদা object — === দেয় false. শেষে config: 3000, তারপর বানান ভুল timout-এ অজানা সেটিং "timout" — চুপচাপ undefined নিয়ে এগোনোর বদলে প্রথম দিনেই ধরা পড়ল। একটি সাবধানতা: এই config-কে JSON.stringify করলে সে toJSON key খুঁজবে আর error খাবে; বাস্তবে Symbol-key আর এমন পরিচিত key-কে ছাড় দিতে হয়।

আরও কয়েকটি চেনা নকশা

Read-only view: কোনো module-কে settings পড়তে দিতে চান, বদলাতে নয়। set, deleteProperty আর defineProperty trap-এ error ছুড়ুন, get-এ ভেতরের object পেলে সেটিকেও একই Proxy-তে মুড়ে দিন। Object.freeze-এর সাথে পার্থক্য হলো — freeze আসল object-কেই সবার জন্য জমিয়ে দেয়, আর Proxy শুধু এই পথে আসা লোকদের থামায়; মালিক নিজে target বদলাতে পারে, আর read-only view-তে সাথে সাথে নতুন মান দেখা যায়।

পরিবর্তনের লগ আর পর্যবেক্ষণ: settings object-এর প্রতিটি set-এ একটি callback ডাকুন — “থিম dark থেকে light হলো” — আর UI নিজে থেকে নতুন করে আঁকুন। এই ধারণাই আধুনিক framework-এর reactivity-র হৃদয়। Vue 3-এর reactive() ধারণাগতভাবে এরকম (সরলীকৃত, আসল কোড নয়; framework নিয়ে পর্ব ৫৪-এ):

// ধারণা বোঝানোর জন্য সরলীকৃত — Vue-র আসল বাস্তবায়ন অনেক বড়
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);                 // "এই মুহূর্তে যে UI আঁকা হচ্ছে, সে key-টি পড়ল" — মনে রাখো
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const ok = Reflect.set(target, key, value, receiver);
      trigger(target, key);               // "key বদলেছে" — যারা পড়েছিল, তাদের আবার চালাও
      return ok;
    },
  });
}
const cart = reactive({ count: 0 });
// প্রত্যাশিত আচরণ: cart.count++ লিখলেই পাতার "কার্টে ১টি পণ্য" লেখাটি নিজে থেকে বদলায়

ঋণাত্মক index (মজার উদাহরণ): Python-এ a[-1] শেষ উপাদান দেয়। JavaScript-এ arr[-1] হলো "-1" নামের একটি property, সাধারণত undefined. একটি get trap key "-1" দেখে তাকে length - 1-এ বদলে দিলেই arr[-1] দেয় শেষ উপাদান। তবে আধুনিক JavaScript-এ এর জন্য Proxy লাগে না — ES2022 থেকে arr.at(-1) আছে (পর্ব ১৫)। Proxy-র আগে সবসময় ভাবুন: ভাষায় সহজ উপায় আছে কি?

Function-এর Proxy — apply আর construct: target function হলে apply trap প্রতিটি call ধরে — কতবার ডাকা হলো গোনা, সময় মাপা, ফল cache করা। Test library-গুলোর “spy” বা “mock” function এই ধারণায় চলে। construct trap new ধরে — যেমন একটি class থেকে একটিমাত্র object (singleton) বানাতে দেওয়া।

Revocable proxy — প্রত্যাহারযোগ্য অনুমতি: Proxy.revocable(target, handler) একটি { proxy, revoke } জোড়া দেয়। revoke() ডাকার পর ওই Proxy দিয়ে যেকোনো কাজে TypeError: Cannot perform 'get' on a proxy that has been revoked (V8-এর বার্তা)। একটি third-party plugin-কে কিছুক্ষণের জন্য আপনার ডেটায় প্রবেশাধিকার দিলেন, কাজ শেষে দরজা বন্ধ — মেয়াদ শেষে বাতিল হওয়া bKash-এর OTP-র মতো। এটিই revocable proxy, আর নিরাপত্তা-গবেষণার “membrane” নকশার ভিত্তি (ভাবনার প্রশ্ন ২)।

৬ · Reflect — ডিফল্ট কাজের সরকারি হাতিয়ার

Reflect কোনো class নয়, new Reflect() হয় না — এটি Math-এর মতো একটি সাধারণ object, যাতে ১৩টি static function আছে, Proxy-র ১৩টি trap-এর হুবহু একই নাম আর একই argument নিয়ে: Reflect.get, Reflect.set, Reflect.has, Reflect.deleteProperty, Reflect.ownKeys, Reflect.apply, Reflect.construct, Reflect.defineProperty ইত্যাদি। প্রতিটি সেই মৌলিক কাজটি “স্বাভাবিক নিয়মে” করে। নিয়মটি সহজ: trap-এর ভেতরে স্বাভাবিক আচরণ চাইলে একই নামের Reflect function ডাকুন, সব argument হুবহু দিয়ে.

কেন target[key] যথেষ্ট নয় — receiver-এর বাগ

অনেকে (আর অনেক AI) trap-এ লেখেন return target[key]. বেশিরভাগ সময় কাজ করে। বিপদ লুকিয়ে থাকে getter আর inheritance-এ। নিচের কোডে একটি user object-এর getter this._name পড়ে। তারপর Proxy-কে prototype বানিয়ে একটি admin object তৈরি হলো, যার নিজের _name আছে:

const user = { _name: "রিনা", get name() { return this._name; } };

const bad  = new Proxy(user, { get(t, k)    { return t[k]; } });               // ভুল
const good = new Proxy(user, { get(t, k, r) { return Reflect.get(t, k, r); } }); // ঠিক

const admin1 = Object.create(bad);  admin1._name = "অ্যাডমিন";
const admin2 = Object.create(good); admin2._name = "অ্যাডমিন";

console.log(admin1.name, admin2.name);
// প্রত্যাশিত আউটপুট: রিনা অ্যাডমিন

admin1.name পড়লে admin1-এ নিজের name নেই, তাই খোঁজ যায় prototype-এ — Proxy-তে। Trap পায় receiver = admin1. কিন্তু ভুল trap সেটি ফেলে দিয়ে t[k] লেখে, ফলে getter চলে this = user নিয়ে — উত্তর আসে “রিনা”, অ্যাডমিনের নাম নয়। ঠিক trap receiver-টি Reflect.get-কে দেয়, getter চলে this = admin2 নিয়ে — “অ্যাডমিন”. এই বাগ সহজে ধরা পড়ে না, কারণ সাধারণ ব্যবহারে (Proxy-কে সরাসরি পড়া) দুটোই একই উত্তর দেয়। Class-এর inheritance-এ (পর্ব ২৬) Proxy থাকলে এটি সত্যিকারের ভুল হিসাবে পরিণত হয়। set-এর বেলাতেও একই কথা — Reflect.set(t, k, v, r) receiver দিয়ে setter চালায় আর নতুন property ঠিক object-এ বসায়।

Reflect বনাম Object-এর পুরোনো method

কিছু Reflect function দেখতে Object-এর পুরোনো method-এর মতো, কিন্তু আচরণে পার্থক্য আছে — সবচেয়ে বড়টি হলো ব্যর্থ হলে error না ছুড়ে false ফেরত দেওয়া:

কাজপুরোনো উপায়Reflect
জমানো object-এ property যোগObject.defineProperty(frozen, "a", {…}) → TypeError: Cannot define property a, object is not extensibleReflect.defineProperty(frozen, "a", {…}) → false
Property আছে কি?"a" in obj — operatorReflect.has(obj, "a") — function, অন্য function-এ পাঠানো যায়
Property মোছাdelete obj.a — operatorReflect.deleteProperty(obj, "a") → true/false
সব keyObject.keys + Object.getOwnPropertySymbolsReflect.ownKeys(obj) — এক ডাকে সব, non-enumerable-সহ
নির্দিষ্ট this দিয়ে function চালানোfn.apply(thisArg, args) — fn-এর নিজের apply কেউ বদলে দিলে ভাঙেReflect.apply(fn, thisArg, args) — সবসময় আসলটি
Array-র মতো argument দিয়ে newnew C(...args)Reflect.construct(C, args, newTarget) — অন্য new.target দেওয়ার সুবিধাসহ

true/false ফেরত দেওয়ার সুবিধা Proxy-তে স্পষ্ট: set, deleteProperty, defineProperty trap-কে ঠিক এই boolean-ই ফেরত দিতে হয়। return Reflect.set(…) লিখলে স্বাভাবিক ফলটি হুবহু পৌঁছে যায় — read-only property-তে লিখতে গেলে false, আর strict mode-এ ভাষা সেটিকে ঠিক সময়ে TypeError বানায়। আর operator-গুলোর (in, delete, new) function-রূপ থাকায় এগুলো higher-order function-এ সহজে পাঠানো যায়।

৭ · নিয়ম, দাম আর সাবধানতা — invariant, গতি, debugging আর decorator

Invariant — Proxy যে মিথ্যা বলতে পারে না

Proxy যা খুশি তাই ফেরত দিতে পারে — প্রায়। ভাষার কিছু প্রতিশ্রুতি আছে যা কোনো অবস্থায় ভাঙা যায় না, এদের বলে invariant. যেমন: কোনো property যদি target-এ non-configurable আর non-writable হয় (property descriptor, পর্ব ১৪), তাহলে get trap-কে তার আসল মানই ফেরত দিতে হবে। জমানো object-এর property সব সময় একই থাকবে — এই প্রতিশ্রুতির ওপর নিরাপত্তা-কোড ভরসা করে, আর Proxy সেটি ভাঙলে পুরো ভাষার নির্ভরযোগ্যতা যেত। তাই engine প্রতিটি trap-এর ফল target-এর সাথে মিলিয়ে দেখে:

const target = {};
Object.defineProperty(target, "x", { value: 1, writable: false, configurable: false });
const liar = new Proxy(target, { get: () => 2 });
liar.x;   // TypeError (নিচে V8-এর বার্তা)
TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '1' but got '2')

এরকম আরও নিয়ম: has trap non-configurable property-কে “নেই” বলতে পারে না; ownKeys-এর তালিকায় সব non-configurable key থাকতেই হবে; Object.preventExtensions করা target-এর বেলায় ownKeys হুবহু target-এর key দিতে হবে। সাধারণ, বদলানো যায় এমন property-তে এসব নিয়ম প্রযোজ্য নয় — সেখানে trap স্বাধীন।

যা Proxy দিয়ে যায় না

  • Private field: #balance-এর মতো private field (পর্ব ২৬) target-এর গায়ে লেগে থাকে, Proxy-র গায়ে নয়। Proxy-র মাধ্যমে এমন getter ডাকলে this হয় Proxy, ফলে TypeError: Cannot read private member #bal from an object whose class did not declare it.
  • Map, Set, Date-এর মতো built-in: এদের ডেটা থাকে ভেতরের লুকানো ঘরে। new Proxy(new Map(), {})-এ .get("a") ডাকলে TypeError: Method Map.prototype.get called on incompatible receiver #<Map> — method-কে আসল Map-এ bind করে দিতে হয়। Vue-র মতো library এজন্য Map/Set-এর জন্য আলাদা handler লেখে।
  • পরিচয় (identity): proxy !== target. একই object-এর দুই রূপ মিশে গেলে Set, WeakMap বা === তুলনায় অদ্ভুত ভুল হয়।
  • কপি ও পাঠানো: structuredClone(proxy) DataCloneError দেয় — Proxy-কে worker-এ পাঠানো যায় না।

গতির দাম আর debugging-এর দাম

পর্ব ০৫-এ দেখেছেন, V8 সাধারণ object-এর property পড়াকে hidden class আর inline cache দিয়ে প্রায় একটি মেমরি-পড়ার মতো দ্রুত বানায়। Proxy-তে প্রতিটি পড়া একটি আসল function call (আর Reflect মিলিয়ে দুটি), সাথে invariant-যাচাই — JIT অনেক কৌশল খাটাতে পারে না। ফলে tight loop-এ লাখো বার পড়া হলে সাধারণ object-এর চেয়ে কয়েক গুণ থেকে অনেক গুণ ধীর হতে পারে (engine আর কাজভেদে সংখ্যা বদলায়, তাই নিজের কোডে মেপে দেখুন — পর্ব ৫৩)। UI-এর state বা config-এর মতো মাঝে মাঝে ছোঁয়া ডেটায় এটি টেরই পাওয়া যায় না; ছবির pixel বা বড় হিসাবের array-তে Proxy নয়।

Debugging-এর দামটি আরও সূক্ষ্ম। Proxy-র নিয়ম চোখে দেখা যায় না: config.timeout লেখা লাইনটি আসলে একটি function চালায়, যা অন্য ফাইলে লেখা। Stack trace-এ trap-এর ফ্রেম ঢুকে পড়ে, console-এ object দেখাতে গিয়ে DevTools নিজেই trap ডেকে বসে, আর নতুন সহকর্মী বোঝেন না “undefined কেন 0 হলো”. তাই নিয়ম: Proxy ব্যবহার করলে নাম আর মন্তব্যে স্পষ্ট লিখুন (createValidatedInventory), সীমানায় (ডেটা ঢোকার দরজায়) রাখুন, আর সাধারণ function বা class দিয়ে কাজ হলে সেটাই বেছে নিন।

বাস্তব library-র ভেতরের “জাদু”

তবু Proxy বড় বড় library-র মেরুদণ্ড। Vue 3-এর reactivity (উপরে দেখেছেন) Proxy-নির্ভর; MobX-এর নতুন সংস্করণগুলোও তাই। Immer (React/Redux জগতে জনপ্রিয়) আপনাকে একটি draft দেয় — একটি Proxy — যাতে আপনি স্বাভাবিকভাবে draft.items.push(x) লেখেন; Proxy প্রতিটি পরিবর্তন নোট করে, আর শেষে আসল ডেটা না ছুঁয়ে একটি নতুন অপরিবর্তনীয় কপি বানায় (পর্ব ২১-এর structural sharing মনে করুন)। কিছু ORM আর API client-ও Proxy দিয়ে না-থাকা method-কে ডাটাবেস কোয়েরি বা HTTP call-এ রূপ দেয়। এগুলো ব্যবহার করার সময় মনে রাখুন: আপনার হাতের object হয়তো আসল object নয় — console.log-এ অদ্ভুত দেখালে, বা structuredClone-এ error এলে, এটাই কারণ।

Decorator — আসছে, কিন্তু সাবধানে বলুন

মেটাপ্রোগ্রামিংয়ের আরেকটি হাতিয়ার হলো decorator — class বা তার method-এর আগে @ দিয়ে লেখা একটি function, যা সংজ্ঞাটিকে মুড়ে বা বদলে দেয়। এর বর্তমান মানদণ্ড-রূপটি ২০২২ সালে TC39-এ Stage 3-এ পৌঁছায়, আর TypeScript 5.0 (২০২৩) থেকে এটি সমর্থন করে। এই লেখার সময় পর্যন্ত বেশিরভাগ ব্রাউজার আর Node.js-এ এটি নিজস্বভাবে চলে না; TypeScript বা Babel দিয়ে রূপান্তর করে ব্যবহার করা হয় — ব্যবহারের আগে নিজের পরিবেশের সমর্থন দেখে নিন। তাই নিচের কোড এখানে চালানো যাবে না:

function logged(method, context) {          // TC39 Stage 3 (২০২২) রূপ
  return function (...args) {
    console.log(`→ ${String(context.name)}(${args.join(", ")})`);
    return method.apply(this, args);
  };
}
class Cart {
  @logged add(item) { /* … */ }
}
new Cart().add("চাল");
// প্রত্যাশিত আউটপুট: → add(চাল)

সাবধান: Angular আর NestJS-এর মতো framework বহু বছর ধরে TypeScript-এর পুরোনো “experimental” decorator ব্যবহার করে, যার argument আর আচরণ নতুন মানদণ্ড থেকে আলাদা। অনলাইনে (আর AI-এর উত্তরে) দুই ধরনের কোড মিশে থাকে — কোনটি দেখছেন, tsconfig.json-এর experimentalDecorators দেখে বুঝুন। দীর্ঘ ইতিহাস ভাবনার প্রশ্ন ৫-এ।

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

Python-এ একই কাজ করে __getattr__/__setattr__, descriptor আর metaclass — Proxy-র trap অনেকটা Python-এর dunder method-এর মতো, আর Symbol.toPrimitive মনে করায় __str__/__float__-কে (Python-এর মেটাপ্রোগ্রামিং). Java-তে java.lang.reflect.Proxy (JDK 1.3 থেকে) শুধু interface-এর জন্য dynamic proxy বানায়, আর reflection দিয়ে class-এর গঠন পড়া যায় — Spring-এর মতো framework এর ওপর দাঁড়িয়ে (Java-র annotation আর reflection). C আর C++-এ চলার সময়ের (runtime) reflection নেই; C++26 মানদণ্ডে compile-time reflection যুক্ত হচ্ছে, যা Proxy-র মতো চলার সময় কাজ ধরে না।

পরীক্ষা ও ইন্টারভিউতে
  • Symbol("a") === Symbol("a") কী দেয়? — false; প্রতিটি Symbol অনন্য, একই মান পেতে Symbol.for("a").
  • Symbol-key কি Object.keys বা JSON.stringify-এ আসে? — না; দেখতে Object.getOwnPropertySymbols বা Reflect.ownKeys. লুকানো, কিন্তু গোপন নয়।
  • Proxy কী, আর trap কী? — একটি object-এর মোড়ক, যা মৌলিক কাজ (get, set, has, delete…) ধরে; handler-এর যে method কাজটি ধরে, সেটি trap.
  • Trap-এর ভেতরে Reflect কেন? — স্বাভাবিক আচরণ হুবহু পেতে, বিশেষত receiver ঠিক রেখে getter/setter চালাতে; আর boolean ফল সরাসরি ফেরত দিতে।
  • Vue 3 reactivity-তে Proxy কেন? — Proxy নতুন যোগ হওয়া বা মোছা property আর array-র index বদলও ধরে, যা Vue 2-এর Object.defineProperty পারত না।

৮ · AI-এর যুগে — “জাদু” লেখার আগে সহজ পথ খুঁজুন

AI-কে “object-এ validation যোগ করো” বললে সে প্রায়ই সোজা Proxy লিখে দেয় — দেখতে আধুনিক আর চতুর। কিন্তু অনেক সময় একটি সাধারণ setStock(name, qty) function বা class-এর setter যথেষ্ট, যা পড়তে সহজ, দ্রুত, আর TypeScript-এর টাইপ-যাচাইয়ের সাথেও ভালো মেলে। AI যে কোড থেকে শিখেছে তাতে Proxy-র অনেক “দেখাও কী পারি” উদাহরণ আছে, কিন্তু রক্ষণাবেক্ষণের দাম নিয়ে কথা কম।

AI যা প্রায়ই লেখেসমস্যাযা চাইবেন
সাধারণ যাচাইয়ের জন্য Proxyলুকানো নিয়ম, ধীর, debugging কঠিনআগে জিজ্ঞেস করুন: function বা setter দিয়ে হয় কি না
trap-এ return target[key] / target[key] = valuereceiver হারায় — getter/setter আর inheritance-এ ভুল thisReflect.get(target, key, receiver), Reflect.set(…, receiver)
set trap কিছু ফেরত দেয় নাundefined মানে falsish — strict mode-এ TypeErrorreturn Reflect.set(…) বা return true
লুকানো ডেটার জন্য obj.__meta বা obj._internal string-keyঅন্য library-র সাথে নামের সংঘর্ষ, JSON আর Object.keys-এ বেরিয়ে পড়েSymbol-key, বা গোপন হলে #private/WeakMap
get trap-এ সব key ধরে error/ডিফল্টSymbol.toPrimitive, toJSON, then-এর মতো ভাষার প্রশ্নেও ভুল উত্তরtypeof key === "symbol" আর পরিচিত key-কে ছাড়
পুরোনো experimentalDecorators আর নতুন decorator মিশিয়েcompile হয় না, বা ভুল argumentকোন সংস্করণ, স্পষ্ট বলে দিন

AI-কে মেটাপ্রোগ্রামিং কোড লিখতে বললে বিকল্প আর ঝুঁকিগুলো আগেই চেয়ে নিন:

আমি JavaScript-এ একটি দোকানের স্টক object বানাচ্ছি, যেখানে কোনো পণ্যের পরিমাণ ঋণাত্মক বা ভগ্নাংশ হতে পারবে না। কোড লেখার আগে দুটি সমাধান তুলনা করো: (ক) একটি সাধারণ setStock(name, qty) function বা class, (খ) একটি Proxy. কোনটি কখন ভালো, পারফরম্যান্স আর debugging-এর দিক থেকে বলো। যদি Proxy বেছে নাও, তাহলে এই শর্ত মানবে: ১. প্রতিটি trap-এ স্বাভাবিক কাজের জন্য Reflect ব্যবহার করবে, receiver-সহ সব argument দিয়ে। ২. set trap সবসময় Reflect.set-এর ফল ফেরত দেবে; ভুল মানে TypeError ছুড়বে, পরিষ্কার বাংলা বার্তায়। ৩. Symbol key (যেমন Symbol.toPrimitive) আর "toJSON", "then"-এর মতো key-তে বাধা দেবে না। ৪. deleteProperty আর defineProperty দিয়েও যেন যাচাই এড়ানো না যায়। শেষে ৫টি পরীক্ষার কেস দাও: ঠিক মান, ঋণাত্মক মান, "5" string, JSON.stringify, আর Object.defineProperty দিয়ে ঋণাত্মক মান বসানোর চেষ্টা — প্রতিটির প্রত্যাশিত ফলসহ।

AI-এর লেখা মেটাপ্রোগ্রামিং কোড — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Proxy আর Reflect কি একেবারে নতুন ধারণা? ১৯৯১ সালের “metaobject protocol” ধারণার সাথে এদের সম্পর্ক কী?

না, ধারণাটি পুরোনো — JavaScript-এর জন্মেরও আগের। ১৯৮০-র দশকের শুরুতে Brian Cantwell Smith তাঁর গবেষণায় “reflection”-এর তত্ত্ব দেন: একটি প্রোগ্রাম নিজের চলার প্রক্রিয়াকে দেখতে আর বদলাতে পারবে। Smalltalk আর Lisp-এর জগতে এই চিন্তা বাস্তব রূপ পায়। তার সবচেয়ে প্রভাবশালী দলিল হলো ১৯৯১ সালে MIT Press থেকে প্রকাশিত বই The Art of the Metaobject Protocol — লেখক Gregor Kiczales, Jim des Rivières আর Daniel G. Bobrow. বইটি Common Lisp Object System (CLOS)-কে ঘিরে লেখা।

মূল ভাবনাটি এরকম: একটি object-oriented ভাষায় “property পড়া”, “method খোঁজা”, “নতুন object বানানো” — এগুলোও আসলে কিছু নিয়ম, যা ভাষার ভেতরে কোনো কোড চালায়। সেই নিয়মগুলোকে যদি নিজেই object (metaobject) বানিয়ে প্রোগ্রামারের হাতে খুলে দেওয়া যায়, তাহলে ভাষার আচরণ ভাষার ভেতর থেকেই বদলানো যায় — নতুন compiler না লিখে। এই খোলা দরজার নিয়মকানুনকে বলা হলো metaobject protocol (MOP). লেখকেরা যুক্তি দেন, একটি ভাষাকে সবার জন্য নিখুঁত করা অসম্ভব; তার চেয়ে ভাষাকে “খোলা” রাখা ভালো, যাতে প্রত্যেকে নিজের প্রয়োজনে সামান্য বদলে নিতে পারে — তবে নিয়ন্ত্রিত, নির্দিষ্ট হুকের মাধ্যমে।

JavaScript-এর Proxy ঠিক এই ধারার উত্তরসূরি। ECMAScript মানদণ্ডে প্রতিটি object-এর ভেতরের method ([[Get]], [[Set]], [[OwnPropertyKeys]]…) আছে; Proxy-র ১৩টি trap সেগুলোর হুবহু প্রতিচ্ছবি। অর্থাৎ JavaScript একটি ছোট, পরিষ্কার MOP দিয়েছে: “object-এর মৌলিক আচরণ এই ১৩টি, এর যেকোনোটি আপনি নিজে লিখতে পারেন।” Reflect হলো সেই প্রোটোকলের “ডিফল্ট বাস্তবায়ন” — CLOS-এ যেমন call-next-method দিয়ে স্বাভাবিক আচরণে ফেরা যেত।

Kiczales পরে আরেকটি বড় ধারণার জন্ম দেন — aspect-oriented programming (AOP), যার বাস্তব রূপ Java-র AspectJ, আর Spring framework-এর অনেক “জাদু” (যেমন লগ বা transaction স্বয়ংক্রিয়ভাবে যোগ করা) সেখান থেকে এসেছে। আজ যখন Vue একটি Proxy দিয়ে UI-কে ডেটার সাথে জুড়ে দেয়, তখন সে আসলে ত্রিশ বছরের পুরোনো এই চিন্তাকেই কাজে লাগাচ্ছে। মজার কথা, MOP-এর সমালোচনাও একই রকম ছিল: ক্ষমতা বেশি হলে কোড বোঝা কঠিন হয়, আর optimizer-এর কাজ কঠিন হয় — Proxy-র গতি আর debugging নিয়ে আমাদের সাবধানতা সেই পুরোনো বিতর্কেরই প্রতিধ্বনি।

মূল উপলব্ধি: ভালো ভাষা-নকশা ভাষাকে সম্পূর্ণ বন্ধও রাখে না, সম্পূর্ণ খোলাও না — কয়েকটি স্পষ্ট, সীমিত হুক খোলে। Proxy-র ১৩টি trap আর invariant ঠিক সেই নিয়ন্ত্রিত খোলা দরজা।

প্র ০২Proxy-র প্রথম নকশা Proxy.create Firefox-এ চালু হয়েও বাতিল হলো কেন? “direct proxy” কী সমস্যার সমাধান করল?

ES2015-এর বেশ আগে, ২০০৯–২০১০ সালের দিকে TC39-এ Tom Van Cutsem (বেলজিয়ামের গবেষক) আর Mark S. Miller (Google, যিনি নিরাপদ JavaScript নিয়ে কাজ করতেন) Proxy-র প্রথম প্রস্তাব দেন। সেই নকশায় Proxy বানানো হতো Proxy.create(handler, proto) দিয়ে — কোনো target object ছিল না। Proxy-টি ছিল পুরোপুরি “ফাঁপা”: তার সব আচরণ handler-এর trap ঠিক করত। Firefox 4 (২০১১) এই API চালু করে, আর V8-এ এটি পরীক্ষামূলক flag-এর পেছনে ছিল। অনেকে উৎসাহ নিয়ে পরীক্ষা শুরু করেন।

কিন্তু সমস্যা ধরা পড়ে ভাষার প্রতিশ্রুতিতে। JavaScript কিছু জিনিস নিশ্চিত করে: Object.freeze করা object-এর property আর কখনো বদলাবে না; non-configurable property মুছে যাবে না; Object.isFrozen(x) একবার true হলে চিরকাল true. নিরাপত্তা-কোড — যেমন Miller-এর Caja প্রকল্প, যা অচেনা third-party কোডকে পাতায় নিরাপদে চালাত — এই প্রতিশ্রুতির ওপর দাঁড়িয়ে ছিল। ফাঁপা Proxy যা খুশি বলতে পারত: আজ “আমি frozen”, কাল অন্য মান। কারও কাছে মিলিয়ে দেখার মতো আসল object ছিল না, তাই ভাষা মিথ্যা ধরতে পারত না।

সমাধান হলো “direct proxy” নকশা (২০১১–১২): প্রতিটি Proxy-র সাথে একটি আসল target থাকবে। Trap স্বাধীনভাবে কাজ করবে, কিন্তু যখনই সে এমন কিছু দাবি করবে যা ভাষার প্রতিশ্রুতির সাথে জড়িত (non-configurable property, non-extensible object), engine target-এর সাথে মিলিয়ে দেখবে — না মিললে TypeError. এই পর্বের অংশ ৭-এর invariant error সেই নকশারই ফল। Van Cutsem আর Miller এই কাজ নিয়ে গবেষণাপত্রও লেখেন (“Trustworthy Proxies”, ECOOP 2013). সাথে আসে Reflect — শুরুতে একটি “reflect module” হিসেবে, যাতে trap-এর প্রতিটি কাজের ডিফল্ট রূপ একটি function হিসেবে পাওয়া যায়। ES2015-এ শেষ পর্যন্ত এই direct proxy নকশাই মানদণ্ড হয়, আর পুরোনো Proxy.create Firefox থেকে সরিয়ে দেওয়া হয়।

এই নকশার একটি বিশেষ ব্যবহার হলো “membrane” — একটি object-জগতের চারপাশে Proxy-র দেয়াল, যা ভেতর থেকে বেরোনো প্রতিটি object-কেও স্বয়ংক্রিয়ভাবে Proxy-তে মুড়ে দেয়। Revocable proxy দিয়ে একটি ডাকেই পুরো জগতের সাথে যোগাযোগ কেটে দেওয়া যায়। নিরাপদ JavaScript-এর আধুনিক চেষ্টাগুলো (যেমন SES/Hardened JavaScript) এই ধারণাগুলো থেকেই বড় হয়েছে।

মূল উপলব্ধি: ক্ষমতা দেওয়ার সময় ভাষাকে নিজের প্রতিশ্রুতি রক্ষার উপায়ও রাখতে হয়। “Target” যোগ করে Proxy-কে মিথ্যা ধরার একটি মাপকাঠি দেওয়া হয়েছে — তাই Proxy শক্তিশালী, কিন্তু ভাষার নিরাপত্তা ভাঙতে পারে না।

প্র ০৩Iteration-এর জন্য ভাষা obj.iterator নামের সাধারণ method না নিয়ে Symbol.iterator কেন নিল? “web compatibility” সমস্যা কী?

ওয়েবের একটি নির্মম নিয়ম আছে: পুরোনো ওয়েবসাইট ভাঙা যাবে না. ব্রাউজার একটি নতুন সংস্করণ দিলে কোটি কোটি পাতা — যেগুলোর অনেকের মালিক হয়তো আর নেই — আগের মতোই চলতে হবে। একে বলে web compatibility. ফলে JavaScript-এ নতুন কিছু যোগ করা মানে এমন নাম আর আচরণ বেছে নেওয়া, যা পুরোনো কোনো কোডের সাথে ধাক্কা খাবে না।

ES2015-এর জন্য দরকার ছিল একটি iteration protocol (পর্ব ২৭): ভাষা for…of চালানোর আগে object-কে জিজ্ঞেস করবে, “আপনার iterator কোথায়?” প্রশ্নটি করতে হয় একটি property-র নাম দিয়ে। নামটি যদি সাধারণ string হতো — ধরুন "iterator" — তাহলে ওয়েবে ইতিমধ্যে থাকা লাখো object, যাদের iterator নামে নিজস্ব property আছে (কোনো library-র cursor, কোনো অ্যাপের নিজের ডেটা), হঠাৎ ভাষার চোখে iterable হয়ে যেত, আর ভুল আচরণ করত। Symbol এই সমস্যার পরিষ্কার সমাধান: Symbol.iterator একটি নতুন, অনন্য key, যা আগে কারও কোডে থাকা অসম্ভব। ভাষা নিশ্চিন্তে নতুন হুক যোগ করতে পারে।

সমস্যাটি কাল্পনিক নয়। ২০১৮ সালে একটি নতুন array method-এর প্রস্তাবিত নাম ছিল flatten. দেখা গেল, পুরোনো MooTools library নিজের Array.prototype.flatten বসাত এমনভাবে যে নতুন built-in এলে সেই library ব্যবহার করা সাইট ভেঙে যেত। মজা করে এর নাম হয় “SmooshGate” (কেউ ঠাট্টায় smoosh নাম প্রস্তাব করেছিলেন), আর শেষে method-টির নাম হয় flat. একই ধরনের ঝুঁকির জন্য ES2015-এ Symbol.unscopables নামে একটি well-known symbol যোগ হয়: পুরোনো with statement ব্যবহার করা কোডে নতুন array method-এর নাম (যেমন values) স্থানীয় variable-কে আড়াল করে ফেলছিল, তাই array নিজেই জানিয়ে দেয় কোন নাম with-এর ভেতরে দেখা যাবে না।

এভাবে well-known symbol-গুলো একটি নীতির প্রকাশ: ভাষার নতুন প্রোটোকল যোগ হবে এমন key দিয়ে, যা পুরোনো কোড কখনো ব্যবহার করতে পারেনি। Python-এ এই কাজ করে __iter__ ধরনের dunder নাম — একটি নামকরণের রীতি, যা সাধারণত মানা হয়, কিন্তু প্রযুক্তিগতভাবে সংঘর্ষ অসম্ভব নয়। JavaScript-এর Symbol সংঘর্ষকে প্রযুক্তিগতভাবেই অসম্ভব করেছে।

মূল উপলব্ধি: যে প্ল্যাটফর্ম কখনো পুরোনো কোড ভাঙতে পারে না, সেখানে নতুন বৈশিষ্ট্য যোগ করার জন্য “নতুন নাম রাখার জায়গা” লাগে। Symbol হলো JavaScript-এর সেই সংঘর্ষহীন নামের জায়গা — ভাষার ভবিষ্যৎ বিবর্তনের নিরাপত্তা-ভালভ।

প্র ০৪Vue 2 থেকে Vue 3-এ (২০২০) reactivity Object.defineProperty থেকে Proxy-তে গেল কেন? এর জন্য Internet Explorer 11 বাদ দিতে হলো কেন?

Vue-র মূল প্রতিশ্রুতি হলো “ডেটা বদলালে UI নিজে বদলায়”. এর জন্য framework-কে জানতে হয় কখন কোন property পড়া হলো আর কখন বদলানো হলো। Vue 2 (২০১৬) এই কাজ করত পর্ব ১৪-এর Object.defineProperty দিয়ে: component-এর ডেটা object-এর প্রতিটি property-কে একটি getter আর setter-এ বদলে দিত। Getter-এ “কে পড়ল” নোট হতো, setter-এ “যারা পড়েছিল তাদের আবার আঁকো”. ES5-এর এই ক্ষমতা Internet Explorer 9 থেকেই ছিল, তাই Vue 2 পুরোনো ব্রাউজারেও চলত।

কিন্তু এই পদ্ধতির মৌলিক সীমা ছিল: getter/setter বসানো যায় শুধু যে property আগে থেকে আছে তার ওপর। ফলে Vue 2-এ নতুন property যোগ করলে (this.user.age = 20) বা মুছে ফেললে UI টের পেত না — এর জন্য বিশেষ Vue.set আর Vue.delete ডাকতে হতো। Array-র index ধরে লেখা (arr[3] = x) বা length বদলও ধরা পড়ত না; Vue array-র push, splice-এর মতো method-কে নিজের সংস্করণে বদলে কাজ চালাত। তার ওপর, শুরুতেই পুরো ডেটার প্রতিটি স্তর ঘুরে প্রতিটি property বদলাতে হতো — বড় ডেটায় শুরুটা ধীর। নতুন Vue ব্যবহারকারীদের সবচেয়ে সাধারণ প্রশ্ন ছিল “ডেটা বদলালাম, স্ক্রিন বদলাল না কেন?”

Proxy এই সব সীমা একসাথে দূর করে: get, set, deleteProperty, has, ownKeys trap যেকোনো key-তে কাজ করে — আগে থেকে থাকুক বা না থাকুক, array-র index হোক বা নতুন property. আর ভেতরের object-কে Proxy-তে মোড়ানো যায় তখনই, যখন সেটি প্রথম পড়া হয় (lazy), তাই শুরুও দ্রুত। Evan You আর তাঁর দল Vue 3 নতুন করে লেখেন, আর ২০২০ সালের সেপ্টেম্বরে Vue 3.0 প্রকাশিত হয়।

মূল্যটি ছিল IE11. Proxy এমন একটি বৈশিষ্ট্য যা পুরোনো JavaScript দিয়ে polyfill বা অনুকরণ করা যায় না — কারণ “যেকোনো key-তে যেকোনো কাজ ধরা” ES5-এ মৌলিকভাবেই অসম্ভব, Babel-এর মতো রূপান্তরকারীও পারে না। তাই Vue 3 IE11 সমর্থন বাদ দেয় (একটি আলাদা IE-সমর্থিত build-এর পরিকল্পনাও শেষে বাদ পড়ে), আর যাদের IE11 দরকার ছিল তাদের জন্য Vue 2-কে আরও কয়েক বছর রক্ষণাবেক্ষণ করা হয় — Vue 2-এর আনুষ্ঠানিক সমর্থন শেষ হয় ২০২৩ সালের শেষে। Microsoft নিজেও ২০২২ সালে বেশিরভাগ Windows-এ IE11-এর সমর্থন বন্ধ করে।

মূল উপলব্ধি: কিছু ভাষা-বৈশিষ্ট্য “সুবিধা” নয়, “সক্ষমতা” — এগুলো ছাড়া কিছু নকশা অসম্ভব। এমন বৈশিষ্ট্য গ্রহণ করতে গেলে পুরোনো প্ল্যাটফর্ম ছাড়ার সিদ্ধান্ত নিতে হয়, আর সেই রূপান্তরের সময় দুই সংস্করণ পাশাপাশি টিকিয়ে রাখার দামও দিতে হয়।

প্র ০৫JavaScript-এ decorator আসতে প্রায় এক দশক লাগল কেন? ২০১৫ সালের “legacy” decorator আর ২০২২ সালের Stage 3 নকশার পার্থক্য কী?

Decorator-এর ধারণা নতুন নয় — Python-এ @decorator আছে ২০০৪ সাল (Python 2.4) থেকে, আর Java-র annotation-ও দেখতে একই রকম। ২০১৪–১৫ সালে Yehuda Katz (Ember.js-এর অন্যতম নির্মাতা) TC39-এ JavaScript-এর জন্য decorator-এর প্রস্তাব দেন। প্রস্তাবটি তখন একেবারে প্রাথমিক স্তরে, কিন্তু চাহিদা এত বেশি ছিল যে TypeScript 1.5 (২০১৫) একে experimentalDecorators flag-এর পেছনে বাস্তবায়ন করে ফেলে, আর Babel-ও একটি plugin দেয়। Google-এর Angular 2 পুরো framework এর ওপর দাঁড় করায় — @Component, @Injectable. পরে NestJS, MobX, TypeORM-এর মতো library-ও এটি ব্যবহার করে।

সমস্যা হলো, এই প্রথম নকশায় decorator সরাসরি property descriptor (পর্ব ১৪) হাতে পেত, আর class-কে যা খুশি বদলাতে পারত — এমনকি class-এর আকারই। Engine নির্মাতারা আপত্তি তোলেন: class-এর গঠন যদি চলার সময় এলোমেলোভাবে বদলায়, তাহলে hidden class-এর মতো optimization কঠিন হয়, আর class field ও private field (যা তখন একই সাথে মানদণ্ডে আসছিল) এর সাথে নকশা মেলে না। ২০১৬ থেকে ২০২১ পর্যন্ত প্রস্তাবটি কয়েকবার আমূল বদলায় — একটি সংস্করণ “static decorators” নামে সম্পূর্ণ নতুন ভাষা-কাঠামো চেয়েছিল, সেটিও বাতিল হয়।

শেষে Kristen Hewell Garrett-এর নেতৃত্বে একটি সরল নকশা আসে, যা ২০২২ সালের মার্চে Stage 3-এ পৌঁছায়। এখানে decorator একটি সাধারণ function: সে যে জিনিসকে সাজাচ্ছে (method, field, class, accessor) সেটি আর একটি context object পায়, আর একটি প্রতিস্থাপন ফেরত দিতে পারে — কিন্তু class-এর আকার বদলাতে পারে না। ফলে engine আগে থেকে গঠন জানে। সাথে আসে নতুন accessor keyword (স্বয়ংক্রিয় getter/setter-ওয়ালা field). ২০২৩ সালের মার্চে TypeScript 5.0 এই নতুন রূপ সমর্থন করে, আর পুরোনোটিও flag দিয়ে রেখে দেয়। Decorator-এর সাথে metadata জোড়ার একটি আলাদা প্রস্তাবও পরে এগিয়েছে।

ফল হলো একটি অস্বস্তিকর বিভাজন: বিশাল পরিমাণ চালু কোড পুরোনো “legacy” রূপে লেখা, যার argument আর আচরণ নতুন মানদণ্ডের সাথে মেলে না, অথচ দেখতে প্রায় হুবহু এক (@something). একটি বৈশিষ্ট্য মানদণ্ড হওয়ার আগেই জনপ্রিয় হলে ঠিক এই দাম দিতে হয়।

মূল উপলব্ধি: ভাষার বৈশিষ্ট্য শুধু ব্যবহারকারীর সুবিধা দেখে চূড়ান্ত হয় না — engine-এর গতি, অন্য বৈশিষ্ট্যের সাথে সামঞ্জস্য আর দীর্ঘমেয়াদি স্থিরতাও মাপতে হয়। “আগে বানাও, পরে মানদণ্ড” পথ দ্রুত, কিন্তু রেখে যায় দুই ভিন্ন জগৎ।

অনুশীলন

বেশিরভাগ কাজ browser console-এ (F12) কয়েকটি লাইন লিখেই করা যায় — ফল আগে অনুমান করুন, তারপর মিলিয়ে নিন।

  1. ফল অনুমান করুন: const k = Symbol("k"); const o = { a: 1, [k]: 2 }; এরপর (ক) Object.keys(o).length (খ) JSON.stringify(o) (গ) Reflect.ownKeys(o).length (ঘ) o[Symbol("k")] (ঙ) Symbol.keyFor(k) (চ) Symbol.for("k") === Symbol.for("k")

    (ক) 1 — Symbol-key গোনা হয় না। (খ) '{"a":1}' — JSON Symbol-key বাদ দেয়। (গ) 2 — Reflect.ownKeys সব key দেয়। (ঘ) undefined — নতুন Symbol("k") একটি সম্পূর্ণ আলাদা চাবি, একই বর্ণনা হলেও। (ঙ) undefined — k সাধারণ Symbol, global registry-তে নেই। (চ) true — registry একই নামে একই Symbol দেয়। শিক্ষা: Symbol-এর পরিচয় তার মান, বর্ণনা নয়।

  2. Proxy-র ফাঁক খুঁজুন: অংশ ৫-এর গুদামে set trap ঋণাত্মক মান আটকায়। কিন্তু একজন সহকর্মী লিখলেন Object.defineProperty(inventory, "chal", { value: -100 }). কী হবে? আর delete inventory.chal? কীভাবে সব পথ বন্ধ করবেন?

    Object.defineProperty set trap ডাকে না — এটি আলাদা মৌলিক কাজ ([[DefineOwnProperty]]), যা যায় defineProperty trap-এ। সেই trap না থাকায় কাজটি সোজা target-এ যায়, আর চালের স্টক হয়ে যায় −১০০ — যাচাই এড়ানো গেল। delete inventory.chal-ও বাধাহীন — পণ্যটি উধাও, তারপর get trap ডিফল্ট ০ দেখাবে। সমাধান: defineProperty trap-এ descriptor.value একই নিয়মে যাচাই করুন (বা getter/setter descriptor পুরো নিষেধ করুন), আর deleteProperty trap-এ মোছা আটকান বা লগ করুন। শিক্ষা: একটি trap একটি কাজ ধরে; “সব লেখা” আটকাতে set, defineProperty, deleteProperty তিনটিই ভাবতে হয় — আর target-এর সরাসরি reference বাইরে দেবেন না।

  3. Read-only settings (কোড লিখুন): একটি readOnly(obj) function লিখুন, যা এমন Proxy দেয় যাতে কোনো property লেখা বা মোছা যায় না (TypeError), আর ভেতরের object পড়লে সেটিও read-only Proxy হয়ে আসে। পরীক্ষা: const s = readOnly({ theme: "dark", font: { size: 18 } }); s.font.size = 20;

    একটি সমাধান: function readOnly(obj) { return new Proxy(obj, { get(t, k, r) { const v = Reflect.get(t, k, r); return typeof v === "object" && v !== null ? readOnly(v) : v; }, set(t, k) { throw new TypeError(`"${String(k)}" বদলানো যাবে না`); }, deleteProperty(t, k) { throw new TypeError(`"${String(k)}" মোছা যাবে না`); }, defineProperty(t, k) { throw new TypeError(`"${String(k)}" যোগ করা যাবে না`); } }); }. এখন s.font.size = 20 দেয় TypeError: "size" বদলানো যাবে না, কারণ s.font নিজেও read-only Proxy. লক্ষ করুন: প্রতিবার পড়লে নতুন Proxy তৈরি হয়, তাই s.font === s.font হয় false — একটি WeakMap cache (পর্ব ১৮) দিয়ে একই target-এর জন্য একই Proxy ফেরত দেওয়া ভালো। Object.freeze-এর সাথে তুলনা করুন: freeze আসল object বদলে দেয় আর গভীরে যায় না; Proxy আসলটি ছোঁয় না।

  4. AI অডিট: যেকোনো AI-কে বলুন “JavaScript-এ একটি Proxy লেখো যা object-এর প্রতিটি পরিবর্তন console-এ লগ করে।” তারপর পরীক্ষা করুন: (ক) target-এ একটি getter রাখুন যা this ব্যবহার করে, আর Proxy-কে prototype বানিয়ে পড়ুন; (খ) set trap কী ফেরত দেয় দেখুন, আর "use strict"-সহ চালান; (গ) `${proxy}` আর JSON.stringify(proxy) চালান। কী কী ভাঙল?

    সাধারণ ফলাফল: (ক) অনেক উত্তরে get(target, key) { return target[key]; } — receiver হারায়, তাই inheritance-এ getter ভুল this পায় (অংশ ৬-এর “রিনা” বনাম “অ্যাডমিন”)। (খ) কোনো কোনো উত্তরে set trap target[key] = value; লিখে কিছু ফেরত দেয় না — undefined falsish, তাই strict mode-এ TypeError: 'set' on proxy: trap returned falsish for property …. (গ) যদি get trap-এ প্রতিটি পড়া লগ হয়, তাহলে `${proxy}`-এ Symbol(Symbol.toPrimitive) আর toString পড়ার লগ দেখবেন, আর JSON.stringify-এ toJSON — ভাষার নিজের প্রশ্নগুলোও trap দিয়ে যায়। কোনো লগ-কোড String(key) না লিখে সরাসরি "key: " + key লিখলে Symbol key-তে TypeError. ঠিক রূপ: Reflect.get/set receiver-সহ, return Reflect.set(…), আর Symbol key-তে String(key).

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

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

ক্রমএকটি Proxy-র p.stock পড়লে যা যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কোড p.stock পড়তে চায়
  2. Engine দেখে p একটি Proxy, আর handler-এ get trap খোঁজে
  3. get trap target, key "stock" আর receiver নিয়ে চলে, নিজের যাচাই করে
  4. Trap Reflect.get(target, key, receiver) দিয়ে স্বাভাবিক পড়া চালায়
  5. Target থেকে মান trap হয়ে কোডে ফেরত আসে

const s = Symbol("x"); const o = { a: 1, [s]: 2 }; হলে Object.keys(o) কী দেয়?

  • [ 'a', Symbol(x) ]
  • [ 'a' ]
  • [ Symbol(x) ]
  • TypeError

একটি object-এ [Symbol.toPrimitive](hint) method আছে। `${obj}` লিখলে method কোন hint পায়?

  • "number"
  • "default"
  • undefined
  • "string"

Proxy-র কোন trap "key" in proxy কাজটি ধরে?

  • has
  • get
  • ownKeys
  • getOwnPropertyDescriptor

Strict mode-এ একটি Proxy-র set trap false ফেরত দিলে p.x = 5 লেখার কী হয়?

  • মান তবুও target-এ লেখা হয়
  • নীরবে কিছুই হয় না
  • TypeError ছোড়া হয়
  • defineProperty trap ডাকা হয়

Proxy-র get trap-এর ভেতরে target[key]-এর বদলে Reflect.get(target, key, receiver) লেখার প্রধান কারণ কী?

  • Reflect.get অনেক দ্রুত
  • Getter-এ সঠিক this (receiver) পাওয়া যায়
  • target[key] prototype chain খোঁজে না
  • target[key] লিখলে SyntaxError হয়

Target-এ একটি property non-writable আর non-configurable, মান 1. Proxy-র get trap সেটির জন্য 2 ফেরত দিলে কী হয়?

  • 2 পাওয়া যায়
  • 1 পাওয়া যায়
  • undefined পাওয়া যায়
  • TypeError ছোড়া হয়

শব্দকোষ

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

মনে রাখুন

  1. মেটাপ্রোগ্রামিং = কোডের আচরণ নিয়ে কোড। JavaScript-এর তিন হাতিয়ার Symbol, Proxy, Reflect — সবই ES2015 থেকে।
  2. Symbol অনন্য; Symbol-key Object.keys, for…in, JSON-এ লুকানো, কিন্তু গোপন নয়। Symbol.for = বৈশ্বিক খাতা।
  3. Well-known symbol দিয়ে object ভাষার কাজে যোগ দেয়: Symbol.iterator, Symbol.toPrimitive (hint: string/number/default), Symbol.toStringTag, Symbol.hasInstance.
  4. Proxy = target + handler; প্রতিটি trap একটি মৌলিক কাজ ধরে। যাচাই, ডিফল্ট, typo-রক্ষা, read-only, লগ, reactivity — কিন্তু invariant ভাঙা যায় না, আর গতি ও debugging-এর দাম আছে।
  5. Trap-এর ভেতরে Reflect.* receiver-সহ, আর set থেকে boolean ফেরত। সাধারণ function দিয়ে কাজ হলে Proxy নয়।

পরের পর্বে: এই পর্বে বারবার TypeError ছুড়েছি আর try/catch-এ ধরেছি — কিন্তু error আসলে কী, কখন ছুড়বেন আর কখন ধরবেন? throw, try/catch/finally, নিজের error class আর cause দিয়ে error-এর শিকল বানানো।

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

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