مفاهیم پیشرفته جاوااسکریپت
بهنام نوبخت · ۱۴۰۵/۷/۱۱
تا اینجا با بخش بزرگی از Syntax و قابلیتهای جاوااسکریپت آشنا شدیم؛ از متغیرها، شرطها و حلقهها گرفته تا Function، Array، Object، DOM، Event، Form، Regular Expression و امکانات مدرن ES6.
اما برای اینکه بتوانیم در پروژههای واقعی جاوااسکریپت کدهای بهتر و قابلاعتمادتری بنویسیم، فقط دانستن Syntax کافی نیست.
باید بدانیم جاوااسکریپت چگونه کدها را اجرا میکند و مفاهیمی مانند Scope، Execution Context، Hoisting، Closure، this، Prototype و Call Stack چگونه کار میکنند.
این مفاهیم در ابتدا ممکن است کمی پیچیده به نظر برسند، اما اگر آنها را مرحلهبهمرحله یاد بگیریم، بسیاری از رفتارهایی که قبلاً در جاوااسکریپت عجیب به نظر میرسیدند، قابل درک خواهند شد.
در این مقاله با مفاهیم زیر آشنا میشویم:
Scope
Global Scope
Function Scope
Block Scope
Lexical Scope
Execution Context
Call Stack
Hoisting
Temporal Dead Zone
Closure
thisPrototype
Prototype Chain
Constructor Function
newShallow Copy و Reference
Immutability
Garbage Collection
و چند نکته مهم درباره نحوه اجرای جاوااسکریپت
چرا مفاهیم پیشرفته جاوااسکریپت مهم هستند؟
فرض کنید کد زیر را اجرا کنیم:
console.log(name);
var name = "Ali";ممکن است انتظار داشته باشیم جاوااسکریپت خطا بدهد، اما نتیجه:
undefinedخواهد بود.
اما اگر بنویسیم:
console.log(name);
let name = "Ali";خطا دریافت میکنیم.
چرا؟
پاسخ این سؤال به مفاهیمی مانند Hoisting، Scope و Temporal Dead Zone مربوط است.
Scope چیست؟
Scope مشخص میکند یک متغیر در کدام قسمت از برنامه قابل دسترسی است.
مثلاً:
const name = "Ali";
console.log(name);چون متغیر در سطح بالای برنامه تعریف شده است، از همان محدوده قابل دسترسی است.
Global Scope
اگر متغیری خارج از Function و Block تعریف شود، معمولاً در محدودهای قرار میگیرد که به آن Global Scope میگوییم.
مثلاً:
const appName = "My App";
function showAppName() {
console.log(appName);
}
showAppName();تابع میتواند به appName دسترسی داشته باشد.
Function Scope
متغیری که با var داخل یک Function تعریف شود، در محدوده همان Function قرار دارد.
مثلاً:
function test() {
var message = "Hello";
console.log(message);
}
test();اما:
console.log(message);خطا ایجاد میکند، زیرا message داخل Function تعریف شده است.
Block Scope
let و const دارای Block Scope هستند.
مثلاً:
if (true) {
const message = "Hello";
console.log(message);
}اما خارج از Block:
console.log(message);به message دسترسی نداریم.
Block چیست؟
ساختارهایی مانند:
if (condition) {
}یا:
for (...) {
}
دارای Block هستند.
همچنین میتوانیم یک Block مستقل داشته باشیم:
{
let value = 10;
console.log(value);
}تفاوت var و let در Scope
مثال:
if (true) {
var a = 10;
let b = 20;
}
console.log(a);
console.log(b);a با var خارج از Block نیز قابل دسترسی است.
اما b با let فقط داخل Block قابل دسترسی است.
به همین دلیل در کدهای مدرن جاوااسکریپت معمولاً استفاده از let و const نسبت به var ترجیح داده میشود.
Lexical Scope
جاوااسکریپت از Lexical Scope استفاده میکند.
یعنی محدوده دسترسی متغیرها بر اساس محل نوشته شدن کد مشخص میشود.
مثلاً:
const name = "Ali";
function outer() {
const age = 25;
function inner() {
console.log(name);
console.log(age);
}
inner();
}
outer();تابع inner میتواند به متغیرهای محدوده خودش و محدودههای بیرونی دسترسی داشته باشد.
Scope Chain
اگر جاوااسکریپت متغیری را در Scope فعلی پیدا نکند، به Scope بیرونی مراجعه میکند.
مثلاً:
const country = "Türkiye";
function outer() {
const city = "Istanbul";
function inner() {
const name = "Ali";
console.log(name);
console.log(city);
console.log(country);
}
inner();
}
outer();برای پیدا کردن country، جاوااسکریپت از Scope داخلی به سمت Scopeهای بیرونی حرکت میکند.
میتوان این ساختار را به شکل زیر تصور کرد:
Global Scope
↓
outer Scope
↓
inner ScopeExecution Context چیست؟
هر بار که جاوااسکریپت کدی را اجرا میکند، محیطی برای اجرای آن ایجاد میشود که به آن Execution Context گفته میشود.
برای شروع میتوانیم سه مفهوم مهم را در نظر بگیریم:
Global Execution Context
Function Execution Context
Eval Execution Contextدر برنامههای معمول جاوااسکریپت، بیشتر با Global و Function Execution Context سر و کار داریم.
Global Execution Context
هنگام اجرای فایل جاوااسکریپت، ابتدا یک Global Execution Context ایجاد میشود.
مثلاً:
const name = "Ali";
console.log(name);اجرای برنامه از Context مربوط به سطح Global شروع میشود.
Function Execution Context
هر بار که یک Function اجرا میشود، Execution Context مربوط به آن Function ایجاد میشود.
مثلاً:
function greet() {
const message = "Hello";
console.log(message);
}
greet();وقتی greet() اجرا میشود، محیط اجرای مربوط به آن ایجاد میشود.
Call Stack چیست؟
جاوااسکریپت برای مدیریت اجرای Functionها از ساختاری به نام Call Stack استفاده میکند.
Stack یعنی ساختاری که معمولاً با قانون:
Last In, First Outکار میکند.
یعنی آخرین چیزی که وارد Stack شود، زودتر خارج میشود.
مثال Call Stack
function first() {
second();
}
function second() {
third();
}
function third() {
console.log("Hello");
}
first();میتوان اجرای آن را به صورت ساده اینگونه تصور کرد:
third()
second()
first()
Globalابتدا first() وارد Stack میشود.
سپس second().
سپس third().
وقتی third() تمام شود، از Stack خارج میشود.
سپس second() و بعد first().
Stack Overflow
اگر یک Function بدون توقف خودش را صدا بزند:
function test() {
test();
}
test();Function به صورت بینهایت فراخوانی میشود.
در نهایت Call Stack پر شده و خطایی مانند:
Maximum call stack size exceededدریافت میکنیم.
این یکی از دلایل اهمیت کنترل Recursive Functionها است.
Hoisting چیست؟
Hoisting رفتاری از جاوااسکریپت است که در آن برخی Declarationها پیش از اجرای کد، در فرآیند آمادهسازی Execution Context مورد پردازش قرار میگیرند.
یکی از سادهترین مثالها:
console.log(name);
var name = "Ali";نتیجه:
undefinedمیتوان رفتار آن را به شکل مفهومی اینگونه در نظر گرفت:
var name;
console.log(name);
name = "Ali";البته این فقط یک مدل ساده برای درک رفتار است و به این معنی نیست که موتور جاوااسکریپت واقعاً کد را دقیقاً به این شکل جابهجا میکند.
Hoisting و Function Declaration
Function Declaration نیز رفتار Hoisting دارد.
مثلاً:
sayHello();
function sayHello() {
console.log("Hello");
}این کد اجرا میشود.
چون Function Declaration پیش از اجرای بخش مربوط به Call قابل شناسایی است.
Hoisting و let
حالا:
console.log(name);
let name = "Ali";با خطا مواجه میشویم.
دلیل آن مفهوم مهمی به نام Temporal Dead Zone است.
Temporal Dead Zone
برای let و const، از ابتدای Scope تا محل Declaration، متغیر در وضعیتی قرار دارد که نمیتوان قبل از Declaration به آن دسترسی داشت.
این محدوده را Temporal Dead Zone یا TDZ مینامیم.
مثلاً:
console.log(age);
let age = 25;باعث خطا میشود.
const و TDZ
همین رفتار برای const نیز وجود دارد:
console.log(country);
const country = "Türkiye";این کد نیز خطا ایجاد میکند.
Closure چیست؟
Closure یکی از مهمترین مفاهیم جاوااسکریپت است.
Closure زمانی ایجاد میشود که یک Function بتواند به متغیرهای Scope بیرونی خود دسترسی داشته باشد، حتی زمانی که Function بیرونی اجرای خود را تمام کرده است.
این تعریف در ابتدا ممکن است کمی پیچیده باشد.
با مثال بهتر میتوان آن را فهمید.
مثال ساده Closure
function outer() {
const message = "Hello";
function inner() {
console.log(message);
}
return inner;
}حالا:
const result = outer();
result();نتیجه:
Helloنکته مهم این است که outer() تمام شده است، اما inner() هنوز به message دسترسی دارد.
این یکی از ویژگیهای مهم Closure است.
مثال کاربردی Closure
میتوانیم یک Counter ایجاد کنیم:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}حالا:
const counter = createCounter();هر بار:
console.log(counter());
console.log(counter());
console.log(counter());نتیجه:
1
2
3متغیر count مستقیماً از بیرون قابل دسترسی نیست، اما Function داخلی میتواند آن را حفظ و تغییر دهد.
چرا Closure کاربردی است؟
Closure در موارد مختلفی استفاده میشود، مانند:
نگهداری State خصوصی
ساخت Functionهای قابل تنظیم
Callbackها
Event Handlerها
Moduleها
مدیریت State
برخی الگوهای طراحی
متغیر خصوصی با Closure
مثلاً:
function createAccount() {
let balance = 0;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
}
};
}استفاده:
const account =
createAccount();
account.deposit(1000);
console.log(
account.getBalance()
);نتیجه:
1000اما:
console.log(account.balance);نتیجه:
undefinedزیرا balance مستقیماً Property آن Object نیست.
this چیست؟
this یکی از موضوعات مهم جاوااسکریپت است.
مقدار this به نحوه فراخوانی Function بستگی دارد و در حالتهای مختلف میتواند مقدار متفاوتی داشته باشد.
مثلاً در یک Method:
const user = {
name: "Ali",
sayHello() {
console.log(this.name);
}
};
user.sayHello();در این حالت this به Object مربوط به user اشاره میکند.
this در Method
مثال:
const person = {
name: "Reza",
introduce() {
console.log(
`My name is ${this.name}`
);
}
};
person.introduce();نتیجه:
My name is Rezaتغییر مقدار Property با this
const user = {
name: "Ali",
age: 25,
birthday() {
this.age++;
}
};
user.birthday();
console.log(user.age);نتیجه:
26this در Function معمولی
نباید تصور کنیم this همیشه به Object اشاره میکند.
در Function معمولی:
function showThis() {
console.log(this);
}مقدار this به نحوه اجرا و محیط کد بستگی دارد.
در جاوااسکریپت مدرن، مخصوصاً در Strict Mode، رفتار this در Functionهای معمولی با مثال Method تفاوت دارد.
برای همین بهتر است this را بر اساس نحوه فراخوانی Function بررسی کنیم.
this در Arrow Function
Arrow Function رفتار متفاوتی با this دارد.
Arrow Function خودش this جدید ایجاد نمیکند و this را از Scope بیرونی دریافت میکند.
مثال:
const user = {
name: "Ali",
showName: function () {
const show = () => {
console.log(this.name);
};
show();
}
};
user.showName();نتیجه:
Aliدر اینجا Arrow Function مقدار this را از Function بیرونی دریافت کرده است.
یک اشتباه رایج درباره Arrow Function
بعضی افراد تصور میکنند:
const user = {
name: "Ali",
sayHello: () => {
console.log(this.name);
}
};حتماً this به user اشاره میکند.
این تصور درست نیست.
Arrow Function this مخصوص خودش را ایجاد نمیکند.
به همین دلیل برای Methodهایی که به this مربوط به Object نیاز دارند، معمولاً Function Method معمولی انتخاب مناسبتری است.
Prototype چیست؟
هر Object در جاوااسکریپت دارای ارتباطی با یک Object دیگر است که به آن Prototype گفته میشود.
Prototype یکی از پایههای مهم مدل شیءگرایی جاوااسکریپت است.
برای مثال:
const user = {
name: "Ali"
};این Object به یک Prototype متصل است که از آن برخی امکانات را به ارث میبرد.
به همین دلیل میتوانیم بنویسیم:
user.toString();در حالی که خودمان toString را داخل Object تعریف نکردهایم.
Prototype Chain
اگر جاوااسکریپت یک Property یا Method را در Object پیدا نکند، میتواند در Prototype آن جستجو کند.
این روند میتواند ادامه پیدا کند.
به این زنجیره:
Object
↓
Prototype
↓
Another Prototype
↓
nullPrototype Chain گفته میشود.
مشاهده Prototype
برای مشاهده Prototype یک Object میتوان از:
Object.getPrototypeOf(user);استفاده کرد.
Constructor Function
قبل از معرفی Classها در جاوااسکریپت، برای ساخت Objectهای مشابه از Constructor Functionها استفاده میشد.
مثلاً:
function User(name, age) {
this.name = name;
this.age = age;
}سپس:
const user1 =
new User("Ali", 25);
const user2 =
new User("Reza", 30);عملگر new
وقتی مینویسیم:
new User("Ali", 25);جاوااسکریپت فرآیندهایی را برای ایجاد Object جدید انجام میدهد.
به صورت ساده میتوان گفت:
یک Object جدید ایجاد میشود.
Prototype مناسب به آن متصل میشود.
thisدر Constructor به Object جدید مربوط میشود.Constructor اجرا میشود.
Object جدید برگردانده میشود.
این توضیح سادهشده است، اما برای درک اولیه new مناسب است.
افزودن Method به Prototype
اگر Method را داخل Constructor ایجاد کنیم:
function User(name) {
this.name = name;
this.sayHello = function () {
console.log(
`Hello ${this.name}`
);
};
}برای هر Instance یک Function جدید ساخته میشود.
میتوانیم Method را روی Prototype قرار دهیم:
User.prototype.sayHello =
function () {
console.log(
`Hello ${this.name}`
);
};اکنون Instanceها میتوانند Method را از Prototype دریافت کنند.
Class و Prototype
وقتی در جاوااسکریپت از:
class User {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(
`Hello ${this.name}`
);
}
}استفاده میکنیم، در پشت مدل شیءگرایی جاوااسکریپت همچنان مفهوم Prototype وجود دارد.
Class در جاوااسکریپت Syntax راحتتری برای کار با این مدل ارائه میکند.
Reference چیست؟
یکی از نکات مهم جاوااسکریپت این است که Objectها و Arrayها با Reference رفتار میکنند.
مثلاً:
const user1 = {
name: "Ali"
};
const user2 = user1;
user2.name = "Reza";
console.log(user1.name);نتیجه:
Rezaچرا؟
چون user1 و user2 به همان Object اشاره میکنند.
مقایسه با Primitive
برای مقدارهای Primitive مانند Number:
let a = 10;
let b = a;
b = 20;
console.log(a);
console.log(b);نتیجه:
10
20در اینجا تغییر b مقدار a را تغییر نمیدهد.
Shallow Copy
برای ایجاد یک Object جدید میتوانیم از Spread استفاده کنیم:
const user1 = {
name: "Ali",
age: 25
};
const user2 = {
...user1
};
user2.name = "Reza";
console.log(user1.name);
console.log(user2.name);نتیجه:
Ali
Rezaمحدودیت Shallow Copy
Spread فقط یک Shallow Copy ایجاد میکند.
مثلاً:
const user1 = {
name: "Ali",
address: {
city: "Istanbul"
}
};
const user2 = {
...user1
};
user2.address.city =
"Ankara";ممکن است:
console.log(
user1.address.city
);نیز مقدار جدید را نشان دهد.
چون Object داخلی همچنان Reference مشترک دارد.
Deep Copy
برای برخی دادههای ساده میتوان از:
structuredClone()استفاده کرد.
مثلاً:
const user1 = {
name: "Ali",
address: {
city: "Istanbul"
}
};
const user2 =
structuredClone(user1);
user2.address.city =
"Ankara";
console.log(
user1.address.city
);
console.log(
user2.address.city
);در این حالت Object داخلی نیز مستقل کپی میشود.
البته روش مناسب کپی کردن دادهها به نوع داده و نیاز پروژه بستگی دارد.
Immutability چیست؟
Immutability به معنای این است که به جای تغییر مستقیم داده، نسخه جدیدی از آن ایجاد کنیم.
مثلاً:
const user = {
name: "Ali",
age: 25
};به جای تغییر مستقیم:
user.age = 26;میتوانیم Object جدید ایجاد کنیم:
const updatedUser = {
...user,
age: 26
};این تکنیک در بسیاری از معماریها و کتابخانههای مدرن جاوااسکریپت اهمیت زیادی دارد.
Garbage Collection چیست؟
جاوااسکریپت دارای مکانیزمی برای مدیریت حافظه است که به آن Garbage Collection گفته میشود.
به صورت ساده، وقتی دیگر هیچ Reference قابل دسترسی به یک Object وجود نداشته باشد، موتور جاوااسکریپت میتواند حافظه مربوط به آن را در زمان مناسب آزاد کند.
مثلاً:
let user = {
name: "Ali"
};
user = null;اگر Reference دیگری به Object وجود نداشته باشد، آن Object دیگر از طریق user قابل دسترسی نیست و میتواند توسط Garbage Collector جمعآوری شود.
Memory Leak چیست؟
گاهی برنامه به دلیل نگه داشتن Referenceهای غیرضروری، حافظه را بیشتر از نیاز مصرف میکند.
به این وضعیت میتوان Memory Leak گفت.
یکی از نمونههای رایج در برنامههای Browser، Event Listenerهایی هستند که دیگر لازم نیستند اما همچنان به عناصر یا Objectها متصل باقی ماندهاند.
برای همین مدیریت صحیح Eventها و منابع اهمیت دارد.
مثال ترکیبی
اکنون چند مفهوم را با هم ترکیب کنیم.
function createCounter(start = 0) {
let count = start;
return {
increment() {
count++;
},
decrement() {
count--;
},
getValue() {
return count;
}
};
}استفاده:
const counter =
createCounter(10);
counter.increment();
counter.increment();
console.log(
counter.getValue()
);نتیجه:
12در این مثال از چند مفهوم استفاده شده است:
Function
Default Parameter
Closure
Object
Method
State خصوصی
یک مثال دیگر با Scope و Closure
function createGreeting(greeting) {
return function (name) {
return `${greeting}, ${name}!`;
};
}حالا:
const sayHello =
createGreeting("Hello");
const sayWelcome =
createGreeting("Welcome");استفاده:
console.log(
sayHello("Ali")
);
console.log(
sayWelcome("Reza")
);نتیجه:
Hello, Ali!
Welcome, Reza!هر Function مقدار greeting مربوط به خودش را از طریق Closure حفظ کرده است.
نکات مهم درباره مفاهیم پیشرفته
همه چیز را یکباره حفظ نکنید
مفاهیمی مانند:
Closure
Prototype
Execution Context
Call Stackبا مطالعه یکباره کاملاً جا نمیافتند.
مهم این است که مثالهای مختلف را اجرا و رفتار آنها را مشاهده کنید.
رفتار this را از روی محل تعریف Function تعیین نکنید
بهخصوص در Functionهای معمولی، باید به نحوه فراخوانی Function توجه کنید.
var را با let و const مقایسه کنید
در پروژههای جدید بهتر است تفاوت Scope و Hoisting آنها را به خوبی بدانید.
Object و Array را Reference در نظر بگیرید
این موضوع در زمان Copy کردن اطلاعات بسیار مهم است.
اشتباهات رایج
اشتباه اول: تصور اینکه let و const اصلاً Hoist نمیشوند
رفتار آنها با var متفاوت است، اما Declaration آنها در فرآیند آمادهسازی Scope در نظر گرفته میشود و دسترسی قبل از Declaration به دلیل TDZ امکانپذیر نیست.
اشتباه دوم: تصور اینکه this همیشه Object فعلی است
this به نحوه فراخوانی Function وابسته است و در Arrow Function نیز رفتار متفاوتی دارد.
اشتباه سوم: تصور اینکه Spread یک Deep Copy است
این کد:
const copy = {
...original
};فقط یک Shallow Copy ایجاد میکند.
اشتباه چهارم: تصور اینکه Closure فقط یک مفهوم تئوری است
Closure در جاوااسکریپت واقعی کاربرد زیادی دارد و در Callbackها، Eventها، Moduleها و مدیریت State دیده میشود.
اشتباه پنجم: استفاده از Recursion بدون شرط پایان
اگر Function خودش را بدون شرط پایان صدا بزند، ممکن است Call Stack پر شود.
تمرینها
تمرین ۱ — Scope
کدی بنویسید که یک متغیر Global و یک متغیر Local داشته باشد.
بررسی کنید کدام Functionها به هرکدام دسترسی دارند.
تمرین ۲ — Hoisting
خروجی کد زیر را پیشبینی کنید:
console.log(a);
var a = 10;سپس همین مثال را با let اجرا کنید و تفاوت را بررسی کنید.
تمرین ۳ — Closure
تابعی به نام createCounter بنویسید که هر بار اجرای Function داخلی، عدد را یکی افزایش دهد.
تمرین ۴ — this
یک Object به نام person ایجاد کنید که دارای:
name
age
introduce()باشد و داخل introduce() از this استفاده کنید.
تمرین ۵ — Prototype
یک Constructor Function به نام Product بسازید و Method آن را روی Prototype قرار دهید.
تمرین ۶ — Shallow Copy
یک Object تو در تو ایجاد کنید و با Spread آن را کپی کنید.
سپس تغییر Object داخلی را بررسی کنید.
تمرین ۷ — Deep Copy
همان مثال را با structuredClone() انجام دهید و نتیجه را با Shallow Copy مقایسه کنید.
چالش عملی
یک سیستم ساده برای ساخت حساب کاربری با Closure ایجاد کنید.
تابعی به نام:
createUser()بسازید که اطلاعات زیر را مدیریت کند:
name
balanceاما balance نباید مستقیماً از بیرون قابل تغییر باشد.
تابع باید متدهای زیر را برگرداند:
deposit()
withdraw()
getBalance()
getName()قوانین:
balanceباید خصوصی باشد.deposit()مبلغ را به موجودی اضافه کند.withdraw()فقط در صورت کافی بودن موجودی برداشت کند.getBalance()موجودی فعلی را برگرداند.getName()نام کاربر را برگرداند.برای مقدار اولیه Balance یک Default Parameter قرار دهید.
از Closure برای نگهداری اطلاعات خصوصی استفاده کنید.
نمونه استفاده:
const user =
createUser("Ali", 1000);
user.deposit(500);
user.withdraw(200);
console.log(
user.getBalance()
);نتیجه:
1300و این نباید امکانپذیر باشد:
user.balance = 999999;چون balance نباید Property عمومی باشد.
جمعبندی
در این مقاله وارد بخش مهمی از مفاهیم پیشرفته جاوااسکریپت شدیم.
با مفاهیم زیر آشنا شدیم:
Scope
Global Scope
Function Scope
Block Scope
Lexical Scope
Scope Chain
Execution Context
Global Execution Context
Function Execution Context
Call Stack
Stack Overflow
Hoisting
Temporal Dead Zone
Closure
thisتفاوت
thisدر Function معمولی و Arrow FunctionPrototype
Prototype Chain
Constructor Function
newReference
Shallow Copy
Deep Copy
Immutability
Garbage Collection
Memory Leak
مهمترین نکته این است که این مفاهیم را فقط به صورت تئوری حفظ نکنید. کدهای مثال را خودتان اجرا کنید، مقدارها را تغییر دهید و نتیجه را مشاهده کنید.
اکنون درک بهتری از نحوه اجرای جاوااسکریپت و رفتار بخشهای مختلف زبان دارید.
نظرات
هنوز نظری ثبت نشده است.