Learn JavaScript

lesson ১ / ৯ · this, prototype আর class

Module ৯ · this, prototype আর class

this আর prototype-এর ছবি: call-এর সময় ঠিক হয়, chain বেয়ে পাওয়া যায়

Freeপড়া

এই lesson-এ যা শিখবে

  • Function কীভাবে call হচ্ছে দেখে বলে দাও this কী হবে, কোথায় লেখা হয়েছে সেটা দেখে না।
  • বুঝিয়ে বলো একটা property খুঁজতে গিয়ে কীভাবে object থেকে তার prototype, তারপর Object.prototype পর্যন্ত ওঠা হয়।
  • Object.create দিয়ে একটা ছোট chain বানাও, আর তার মাধ্যমে একটা method অনেক object-এর মধ্যে ভাগ করে দাও।

Mitu একটা quiz app লিখছে। প্রতিটা player একটা object, যার একটা নাম আছে আর একটা method আছে, যেটা player-কে hello বলে। player.hello() লিখলে ঠিকঠাক চলে। তারপর ও method-টা পরে call করবে বলে একটা variable-এ রেখে দিল, আর greeting এল "Hi, undefined"। Method একটুও বদলায়নি। বদলেছে শুধু call করার ধরনটা।

এই module-এর অর্ধেকটাই এই একটা চমক। বাকি অর্ধেক হলো Module 8-এর একটা প্রশ্ন: {}-এর toString এল কোথা থেকে, কেউ তো লেখেনি? দুটো উত্তরই ছোট। this ঠিক হয় function call হওয়ার মুহূর্তে। আর যে property object-এ নেই, সেটা খোঁজা হয় prototype নামের কয়েকটা object-এর একটা chain-এ।

যে method নিজের object ভুলে যায়

Method হলো এমন একটা function, যেটা কোনো object-এর property হিসেবে রাখা। ভেতরে this শব্দটা দিয়ে method নিজের object-এর কাছে পৌঁছায়, যাতে this.name পড়তে পারে। এই যে Mitu-র player, আর ওর দুটো call।

const player = {
  name: "Rafi",
  hello() {
    return "Hi, " + this.name;
  },
};
console.log(player.hello());
const hello = player.hello;
console.log(hello());
console.log(hello === player.hello);
Hi, Rafi
Hi, undefined
true
Run in Compiler

শেষ লাইন দেখাচ্ছে function একটাই। তবু দ্বিতীয় call-এ নামটা হারিয়ে গেল। hello() হিসেবে call করার সময় dot-এর সামনে কিছু ছিল না, তাই this আর player থাকেনি। এই file-এ this হয়ে গেছে global object, যার কোনো name নেই। Lesson 02 দেখাবে strict code-এ এর বদলে আসে undefined, আর তখন call-টা crash করে।

this ঠিক হয় call-এর সময়

এক লাইনে নিয়মটা হলো: call-এর মুহূর্তে dot-এর সামনে যে object, সেটাই this। Function কোথায় লেখা হয়েছিল, তাতে কিছু যায় আসে না। কার হাতে আছে, তাতেও না।

function describe() {
  return this.name + " has " + this.score + " points";
}
const rafi = { name: "Rafi", score: 7, describe };
const nadia = { name: "Nadia", score: 9, describe };
console.log(rafi.describe());
console.log(nadia.describe());
console.log(rafi.describe === nadia.describe);
Rafi has 7 points
Nadia has 9 points
true
Run in Compiler

Function একটা, object দুটো। rafi.describe() চালায় rafi-কে this বানিয়ে, আর nadia.describe() ঠিক একই function চালায় nadia-কে this বানিয়ে। তাই this অনেকটা একটা parameter-এর মতো, যেটা তুমি পাঠাও dot কোথায় বসালে তা দিয়ে। কোন কোন ভাবে call this ঠিক করে, তার পুরো list আছে Lesson 02-এ।

প্রতিটা object-এর একটা prototype আছে

এবার দ্বিতীয় প্রশ্ন। Module 8-এ দেখেছ "toString" in {} হলো true, অথচ Object.hasOwn({}, "toString") হলো false। Object-টা toString-এর মালিক না। ও সেখানে পৌঁছায় নিজের prototype-এর মাধ্যমে: আরেকটা object-এর দিকে একটা লুকানো link।

const empty = {};
const proto = Object.getPrototypeOf(empty);
console.log(proto === Object.prototype);
console.log(Object.hasOwn(empty, "toString"));
console.log(Object.hasOwn(proto, "toString"));
console.log(Object.getPrototypeOf(proto));
true
false
true
null
Run in Compiler

Object.getPrototypeOf(o) সেই object-টা দেয়, যার দিকে o-এর link। সাধারণ object হলে সেটা Object.prototype, যার নিজের আছে toString আর আরও কয়েকটা method। Object.prototype-এর link কোথাও যায় না: তার prototype হলো null। প্রতিটা chain শেষ হয় এখানেই।

খোঁজা chain বেয়ে ওঠে

o.key পড়লে JavaScript আগে দেখে key-টা o-এর নিজের কি না। না হলে দেখে o-এর prototype-এ, তারপর সেই object-এর prototype-এ, এভাবে চলতে থাকে। যে object প্রথম key-টার মালিক, সেখানেই থামে। null-এ পৌঁছে গেলে উত্তর undefined। Link-এর এই পথটাই prototype chain।

Chain তুমি নিজেও বানাতে পারো Object.create(p) দিয়ে, যেটা একটা নতুন খালি object বানায়, যার prototype হলো p।

const game = { theme: "light", sound: "on" };
const team = Object.create(game);
team.theme = "dark";
const player = Object.create(team);
player.name = "Rafi";
console.log(player.name, player.theme, player.sound);
console.log(Object.hasOwn(player, "theme"));
console.log(player.speed);
Rafi dark on
false
undefined
Run in Compiler

player.name পাওয়া যায় player-এর নিজের ভেতরেই। player.theme player-এ নেই, তাই খোঁজা ওঠে team-এ, আর সেখানে পায় "dark"। Game-এর "light" পর্যন্ত কখনো পৌঁছায় না, কারণ team-এর value সেটাকে shadow করে, মানে ঢেকে দেয়। player.sound ওঠে দুই ধাপ। player.speed উঠে যায় একদম শেষে, আর দেয় undefined।

লেখা কখনো ওপরে ওঠে না

পড়ার সময় chain বেয়ে ওঠা হয়। লেখার সময় হয় না। o.key = v assign করলে key-টা বসে o-এর নিজের ভেতরে, prototype-এ আগে থেকে ওই key থাকলেও।

const defaults = { volume: 5 };
const settings = Object.create(defaults);
console.log(settings.volume);
settings.volume = 9;
console.log(settings.volume, defaults.volume);
console.log(Object.keys(settings));
5
9 5
[ 'volume' ]
Run in Compiler

লেখার পরে settings-এর নিজের একটা volume হলো, যেটা defaults-এরটাকে ঢেকে দেয়। defaults-এ কেউ হাত দেয়নি, তাই যে object-গুলো ওর দিকে link করা, ওরা এখনো 5-ই পড়ে। এই কারণেই একটা prototype ভাগ করে নেওয়া নিরাপদ। (Chain-এ কোনো setter থাকলে সেটা একমাত্র ব্যতিক্রম, Lesson 02-এ দেখবে।)

Method থাকে prototype-এ

এবার দুটো অর্ধেক জোড়া লাগাও। অনেকগুলো object একটা prototype-এর দিকে link করতে পারে, যেখানে method-গুলো রাখা। প্রতিটা object শুধু নিজের data রাখে। Method চলার সময় dot-এর সামনের object-টাই this, তাই একটা ভাগ করা method সব object-এর জন্যই কাজ করে।

const playerMethods = {
  hello() {
    return "Hi, " + this.name;
  },
  addPoint() {
    this.score += 1;
    return this.score;
  },
};
const rafi = Object.create(playerMethods);
rafi.name = "Rafi";
rafi.score = 0;
const lima = Object.create(playerMethods);
lima.name = "Lima";
lima.score = 4;
console.log(rafi.hello(), lima.hello());
console.log(rafi.addPoint(), lima.addPoint());
console.log(rafi.hello === lima.hello);
Hi, Rafi Hi, Lima
1 5
true
Run in Compiler

rafi.hello rafi-এর ভেতরে নেই। খোঁজা উঠে যায় playerMethods-এ, আর সেখানে পায়। তারপর rafi.hello() call-টা ওটাকে চালায় rafi-কে this বানিয়ে। Class-এর পেছনের পুরো কৌশল এটাই, Lesson 02 এর ওপরই দাঁড়াবে: class হলো ঠিক এই ভাগ করা prototype সুন্দর করে লেখার একটা উপায়।

নিচে দেখো একটা খোঁজা কীভাবে ওঠে, একবারে একটা object। Chain আঁকা আছে বাঁয়ের object থেকে ডানের Object.prototype পর্যন্ত। Object.prototype-এর box দেখায় তার অনেকগুলো নামের মাত্র তিনটা।

একই ধাপগুলো table-এ, প্রতি ধাপে একটা row।

ধাপLookupAtFoundকী হলো
1name0truename আছে rafi-এর নিজের, তাই rafi.name এখানেই থামে আর দেয় একটা string।
2hello0falsehello rafi-এর নিজের না, তাই খোঁজা উঠে যায় playerMethods-এ।
3hello1truehello আছে playerMethods-এর নিজের, তাই rafi.hello এখানেই থামে আর দেয় একটা function।
4toString0falsetoString rafi-এর নিজের না, তাই খোঁজা উঠে যায় playerMethods-এ।
5toString1falsetoString playerMethods-এর নিজের না, তাই খোঁজা উঠে যায় Object.prototype-এ।
6toString2truetoString আছে Object.prototype-এর নিজের, তাই rafi.toString এখানেই থামে আর দেয় একটা function।
7speed0falsespeed rafi-এর নিজের না, তাই খোঁজা উঠে যায় playerMethods-এ।
8speed1falsespeed playerMethods-এর নিজের না, তাই খোঁজা উঠে যায় Object.prototype-এ।
9speed2falseObject.prototype-এরও speed নেই, আর তার prototype হলো null, তাই rafi.speed হলো undefined।

প্রতিটা খোঁজা আবার শুরু হয় বাঁয়ের rafi থেকে। name সাথে সাথে থামে। hello ওঠে এক link। toString ওঠে দুই link। speed উঠে যায় শেষ পর্যন্ত আর কিছুই পায় না, এই কারণেই না-থাকা property হলো undefined, কোনো error না।

এক নজরে this আর chain

obj.method()                   // inside method, this is obj: the object before the dot
const f = obj.method; f()      // same function, nothing before the dot: this is lost
Object.getPrototypeOf(o)       // the object o links to, or null at the end
Object.create(p)               // a new empty object whose prototype is p
o.key                          // read: o, then its prototype, then on up, else undefined
o.key = v                      // write: always on o itself, never up the chain
Object.hasOwn(o, "key")        // true only if o itself owns key
Example 1: একটা method, অনেক ticket

Nadia-র cinema hall ticket print করে। Input হলো ক্রেতাদের নাম আর seat নম্বরের একটা list। প্রতিটা ticket object একটাই prototype-এর দিকে link করা, যেখানে label method রাখা, তাই ticket যতই হোক, method আছে একবারই।

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
const ticketMethods = {
  label() {
    return this.buyer + " in seat " + this.seat;
  },
};
const tickets = [];
for (let i = 0; i < tokens.length; i += 2) {
  const t = Object.create(ticketMethods);
  t.buyer = tokens[i];
  t.seat = Number(tokens[i + 1]);
  tickets.push(t);
}
for (const t of tickets) console.log(t.label());
console.log(tickets.every((t) => Object.getPrototypeOf(t) === ticketMethods));
console.log(Object.keys(tickets[0]));
Nadia in seat 12
Kenji in seat 7
Sumon in seat 3
true
[ 'buyer', 'seat' ]

Object.keys শুধু ticket-এর নিজের key দেখায়, buyer আর seat। label কোনো ticket-এ copy হয়নি। প্রতিটা t.label() call prototype পর্যন্ত ওঠে, আর this হলো dot-এর সামনের ticket-টা।

Run in Compiler
Example 2: একটা default ঢেকে দেওয়া, তারপর ঢাকনা সরানো

একটা settings object defaults-এর দিকে link করা। একটা key লিখলে default-টা ঢাকা পড়ে। নিজের key-টা delete করলে default আবার ফিরে আসে, কারণ খোঁজা আবার ওপরে ওঠে।

const defaults = { lang: "en", fontSize: 14 };
const mine = Object.create(defaults);
mine.lang = "bn";
console.log(mine.lang, mine.fontSize);
delete mine.lang;
console.log(mine.lang);
delete mine.fontSize;
console.log(mine.fontSize, defaults.fontSize);
bn 14
en
14 14

delete mine.fontSize defaults-এর কিছুই করে না, কারণ delete শুধু নিজের key মোছে, আর fontSize কখনো mine-এর নিজের ছিল না। তার পরের পড়াটা তবু ওপরে উঠে 14 পায়।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Node.js: তোমার বানানো প্রতিটা array link করা Array.prototype-এর দিকে, তাই প্রতিটা array-তে copy না করেও [1, 2].map থাকে।
  • V8: Chrome আর Node-এর engine মনে রাখে chain-এর কোথায় গিয়ে একটা খোঁজা শেষ হয়েছিল, তাই prototype method বারবার পড়াও দ্রুত থাকে।
  • Express: একটা route-এর req আর res object এমন prototype-এর দিকে link করা, যেগুলো Express res.json-এর মতো helper দিয়ে ভরে রাখে।

যে ভুলগুলো সবাই করে

১. Object থেকে method বের করে নেওয়া

const counter = {
  count: 0,
  add() {
    this.count += 1;
    return this.count;
  },
};
const add = counter.add;
console.log(add());
console.log(counter.count);
NaN
0

add() call-এর dot-এর সামনে কিছু নেই, তাই this counter না। এটা একটা না-থাকা count-এ 1 যোগ করে NaN print করে, আর counter.count থেকে যায় 0। counter.add() হিসেবে call করো, নয়তো Lesson 02-এর সমাধানগুলো দেখো।

২. লেখা দিয়ে prototype বদলানোর আশা

const shared = { color: "red" };
const a = Object.create(shared);
const b = Object.create(shared);
a.color = "blue";
console.log(b.color);
console.log(shared.color);
red
red

আশা ছিল সব object-এর রং বদলাবে। কিন্তু লেখাটা বসে a-এর নিজের ভেতরে, তাই b আর shared এখনো red বলে। সবাই যা দেখে সেটা বদলাতে চাইলে লেখো prototype-এ: shared.color = "blue"।

৩. Object.create-কে একটা নাম দেওয়া

const base = { kind: "base" };
const child = Object.create(base);
console.log(child.kind);
const broken = Object.create("base");
console.log(broken.kind);
base
TypeError: Object prototype may only be an Object or null: base

প্রথম call ঠিকঠাক চলে আর base print করে। দ্বিতীয়টা থামে TypeError: Object prototype may only be an Object or null: base দিয়ে। Object.create নেয় prototype object-টা নিজে, নয়তো null, কোনো variable-এর নাম string হিসেবে কখনো না।

মাথা খাটাও

শেষ কথাটা কার?

Zara তিনটা object-এর একটা chain বানিয়ে মাঝেরটায় লেখে।

const top = { word: "top" };
const mid = Object.create(top);
const low = Object.create(mid);
mid.word = "mid";
delete low.word;
console.log(low.word, Object.hasOwn(low, "word"));

এটা কী print করবে? প্রতিটা লাইনের পরে word কোন object-এর নিজের, সেটা ঠিক করো।

Exercise 1সহজ

Lima একটা quiz game বানাচ্ছে। একজন player-এর setting বসে team-এর setting-এর ওপরে, আর team-এর setting বসে game-এর default-এর ওপরে। Game কোনো setting পড়তে গেলে আগে দেখে player-কে, তারপর team, তারপর game। Lima এমন একটা tool চায়, যেটা বলে দেবে প্রতিটা পড়ার উত্তর কোন level থেকে এসেছে।

Input. প্রথম লাইনে n, মানে level কয়টা। পরের n টা লাইনের প্রতিটা একটা level: তার নাম, একটা সংখ্যা k, তারপর k জোড়া key আর value। প্রথম level-টা সবার ওপরে; পরেরটা বসে তার আগেরটার নিচে। তারপর আসে q, আর পড়ার জন্য q টা key।

Output. প্রতিটা key-এর জন্য এক লাইন। কোনো level-এ key-টা থাকলে print করো তার value, from শব্দটা, আর key-টা আছে এমন সবচেয়ে কাছের level-এর নাম। না থাকলে print করো missing।

Constraints. 1 <= n <= 10; 0 <= k <= 10 for every level; 1 <= q <= 1000; Level names and keys are 1 to 15 English letters. Values are 1 to 10 letters or digits. Keys in one level are different..

Sample.

3
player 2 theme dark volume 7
team 2 volume 5 language bn
game 3 theme light language en speed normal
5
volume
theme
speed
toString
language
7 from player
dark from player
normal from game
missing
bn from team
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));
Run in Compiler

chain-lookup নামে grade হয়।

Exercise 2মাঝারি

Sumon-এর club member card রাখে। প্রতিটা card একটা ভাগ করা prototype-এর দিকে link করা, যাতে line নামে একটা method আছে। Method-টা ফেরত দেয় নাম, তারপর শহর, তারপর কতবার এসেছে। প্রতিটা card-এর line print করো, তারপর প্রতিটা card নিজে কয়টা key-এর মালিক সেটা।

Input. n, তারপর n টা লাইন, প্রতিটায় একজন member-এর নাম, শহর আর আসার সংখ্যা।

Output. n টা লাইন, প্রতিটা card-এর line, মানে নাম, শহর আর আসার সংখ্যা, একটা করে space দিয়ে আলাদা। তারপর আরও এক লাইন: owned, একটা space, আর প্রথম card-এর নিজের key-এর সংখ্যা।

Constraints. 1 <= n <= 1000। নাম আর শহর 1 থেকে 10টা অক্ষর। আসার সংখ্যা 0 থেকে 1000।

Sample. নিচের input।

3
Rafi Dhaka 12
Mitu Khulna 3
Kenji Sylhet 40
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

Rafi Dhaka 12
Mitu Khulna 3
Kenji Sylhet 40
owned 3
Run in Compiler
Exercise 3কঠিন

Shadowing নিয়ে নিজের ছবিটা ঠিক কি না, Lima যাচাই করতে চায়। ওর script শুরু হয় base নামের একটা object দিয়ে, যাতে কোনো key নেই। তারপর চারটা command। make x y x বানায়, একটা নতুন object, যার prototype হলো y। set x k v x-এর ওপর key k লেখে, value v দিয়ে। del x k x-এর নিজের key k মুছে দেয়। get x k print করে x-এ k পড়লে যা আসত, না থাকলে none।

Input. n, তারপর n টা command, প্রতি লাইনে একটা।

Output. প্রতিটা get-এর জন্য এক লাইন।

Constraints. 1 <= n <= 1000। নাম, key আর value 1 থেকে 8টা ছোট হাতের অক্ষর। প্রতিটা নাম ব্যবহারের আগেই বানানো হয়, আর base শুরু থেকেই আছে।

Sample. নিচের input।

7
make team base
make me team
set base theme light
set team theme dark
get me theme
del team theme
get me theme
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

dark
light
Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Function-এ যে prototype property দেখেছি, prototype কি সেটাই?

    পুরোপুরি না, আর নাম দুটো সত্যিই গোলমেলে। প্রতিটা object-এর একটা prototype আছে, সেই লুকানো link, যেটা Object.getPrototypeOf দেয়। আবার একটা function-এর prototype নামে একটা সাধারণ property-ও থাকে, যেটা new দিয়ে বানানো object-দের prototype হয়ে যায়। দুটো কোথায় মেলে, Lesson 02 দেখাবে।

  • প্রতিটা object-এ method-গুলো copy করে দিলেই কি হয় না?

    হয়, Module 6-এর factory function ঠিক এটাই করে। তাতে প্রতিটা method-এর জন্য প্রতিটা object-এ একটা করে function লাগে। ভাগ করা prototype প্রতিটা method রাখে একবারই, যত object-ই ব্যবহার করুক। Lesson 04 দুটো পাশাপাশি মেলাবে।

  • __proto__ জিনিসটা কী?

    এটা Object.prototype-এর একটা পুরোনো accessor, যেটা কোনো object-এর prototype পড়ে বা বসায়। এখনো কাজ করে, কিন্তু Object.getPrototypeOf আর Object.create একই কথা পরিষ্কার করে বলে। __proto__ নামের key কেন একটা ফাঁদ, সেটাও Module 8 দেখিয়েছে।

  • একটা chain কি ঘুরে আবার শুরুতে ফিরতে পারে?

    না। যে prototype link একটা চক্র বানাবে, JavaScript সেটা বানাতে দেয় না, এর বদলে একটা TypeError throw করে। প্রতিটা chain শেষ হয় null-এ, তাই একটা খোঁজা সবসময় থামে।

মূল কথা

  • Function call হওয়ার সময় dot-এর সামনে যে object, সেটাই this, কোথায় লেখা হয়েছিল সেটা না।
  • প্রতিটা object একটা prototype-এর দিকে link করা, আর chain শেষ হয় Object.prototype-এর পরে null-এ।
  • পড়ার সময় খোঁজা ওঠে প্রথম যে object key-টার মালিক সেখান পর্যন্ত, না পেলে undefined।
  • লেখা সবসময় বসে object-এর নিজের ভেতরে, ওপরের যেকোনো key ঢেকে দিয়ে।
  • ভাগ করা prototype-এর method থাকে একবারই, আর প্রতিটা object-এ পৌঁছায় this দিয়ে।
  • আরও গভীরে যেতে চাইলে: ভেতরের কলকবজা, হাতে হাঁটা chain, instanceof, আর V8 class object-কে কীভাবে দেখে (Pro)।

এরপর: this ঠিক করার প্রতিটা নিয়ম, আর class syntax, যেটা একই chain বানায়।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: this-এর প্রতিটা নিয়ম, আর class syntax-এর প্রতিটা অংশ, একটা একটা করে