انواع داده‌ها و ساختارهای پیشرفته در جاوا اسکریپت

فهرست مطالب

جاوا اسکریپت، زبانی که در ابتدا برای تعاملات ساده در مرورگر طراحی شده بود، امروزه به یکی از قدرتمندترین و پرکاربردترین زبان‌ها در توسعه وب (فرانت‌اند و بک‌اند)، اپلیکیشن‌های موبایل، دسکتاپ و حتی هوش مصنوعی تبدیل شده است. بخش عمده‌ای از این قدرت، مدیون انعطاف‌پذیری و تکامل مستمر آن در زمینه مدیریت داده‌هاست. در حالی که بسیاری از توسعه‌دهندگان با انواع داده‌های اولیه (مانند String، Number، Boolean، Null، Undefined) و ساختارهای داده‌ای رایج (مانند Object و Array) آشنا هستند، جاوا اسکریپت مجموعه‌ای غنی‌تر و پیشرفته‌تر از انواع داده‌ها و ساختارها را ارائه می‌دهد که برای حل مسائل پیچیده‌تر، بهینه‌سازی عملکرد و مدیریت حافظه ضروری هستند.

در این مقاله جامع، ما به سفری عمیق در دنیای انواع داده‌ها و ساختارهای پیشرفته جاوا اسکریپت خواهیم رفت. هدف ما فراتر از یک معرفی ساده است؛ ما به بررسی جزئیات فنی، کاربردهای عملی، مزایا و معایب هر یک و چگونگی انتخاب بهترین ابزار برای هر موقعیت خاص خواهیم پرداخت. درک این مفاهیم نه تنها به شما کمک می‌کند کدهای کارآمدتر و قابل نگهداری‌تری بنویسید، بلکه دریچه‌ای جدید به سوی الگوهای طراحی پیشرفته و راه‌حل‌های خلاقانه برای چالش‌های برنامه‌نویسی باز می‌کند. این محتوا برای توسعه‌دهندگانی طراحی شده است که حداقل آشنایی اولیه با جاوا اسکریپت دارند و اکنون به دنبال تعمیق دانش خود و ارتقای مهارت‌هایشان به سطح حرفه‌ای هستند.

Symbol: فراتر از رشته‌ها و اعداد به عنوان کلید

در جاوا اسکریپت، تا قبل از ES6، کلیدهای ویژگی‌های (properties) شیءها تنها می‌توانستند رشته یا Symbol (قبل از ES6، تنها رشته) باشند. معرفی Symbol در ES6 یک نوع داده اولیه جدید را به ارمغان آورد که به شما امکان می‌دهد کلیدهای ویژگی منحصربه‌فردی ایجاد کنید که با هیچ رشته یا Symbol دیگری تداخل نداشته باشند. این ویژگی برای سناریوهایی که نیاز به ایجاد ویژگی‌های “خصوصی” یا “پنهان” در شیءها دارید، بدون خطر تصادم نام با کدهای خارجی یا کتابخانه‌ها، بسیار ارزشمند است.

ویژگی‌های کلیدی Symbol

  • منحصربه‌فرد بودن: هر Symbol ایجاد شده، حتی اگر توضیحات یکسانی داشته باشند، کاملاً منحصربه‌فرد است. این خاصیت تضمین می‌کند که یک Symbol هرگز با یک رشته یا Symbol دیگر تداخل نخواهد داشت.
  • عدم قابلیت تکرارپذیری با روش‌های معمول: Symbolها به طور پیش‌فرض توسط for...in لوپ‌ها یا Object.keys()، Object.values()، و Object.entries() نادیده گرفته می‌شوند. برای دسترسی به آنها باید از Object.getOwnPropertySymbols() یا Reflect.ownKeys() استفاده کرد. این ویژگی به آنها خاصیت “پنهان” بودن می‌دهد.
  • عدم تبدیل ضمنی به رشته: یک Symbol را نمی‌توان به طور ضمنی به یک رشته تبدیل کرد (مثلاً در الحاق رشته‌ها). برای نمایش آن به صورت رشته باید از String(sym) یا sym.toString() استفاده کرد.

ایجاد Symbol

Symbolها با فراخوانی تابع Symbol() ایجاد می‌شوند. می‌توانید یک رشته توضیحی اختیاری به عنوان آرگومان به آن پاس دهید که صرفاً برای اشکال‌زدایی (debugging) مفید است و بر منحصربه‌فرد بودن Symbol تأثیری ندارد:


const myUniqueSymbol = Symbol('This is a unique identifier');
const anotherUniqueSymbol = Symbol('This is a unique identifier');

console.log(myUniqueSymbol === anotherUniqueSymbol); // false, هر دو منحصربه‌فرد هستند

const obj = {
    [myUniqueSymbol]: 'Value for my unique symbol',
    'regularKey': 'Regular string key value'
};

console.log(obj[myUniqueSymbol]); // 'Value for my unique symbol'
console.log(obj.regularKey);    // 'Regular string key value'

// تلاش برای دسترسی به Symbol با کلید رشته‌ای ناموفق است
console.log(obj['myUniqueSymbol']); // undefined

استفاده از Symbol به عنوان ویژگی شیء

Symbolها به طور عمده برای ایجاد ویژگی‌های شیء استفاده می‌شوند. این ویژگی‌ها می‌توانند مانند هر ویژگی دیگری به شیء اضافه شوند، اما ماهیت منحصربه‌فرد و غیرقابل تکرار آنها مزایای خاصی دارد:


const userID = Symbol('User Identifier');
const userRole = Symbol('User Role');

class User {
    constructor(name, id, role) {
        this.name = name;
        this[userID] = id; // استفاده از Symbol به عنوان کلید
        this[userRole] = role;
    }

    getRole() {
        return this[userRole];
    }
}

const admin = new User('Alice', 101, 'Administrator');
console.log(admin.name); // Alice
console.log(admin.getRole()); // Administrator

// Symbolها با for...in یا Object.keys() دیده نمی‌شوند
for (let key in admin) {
    console.log(key); // فقط 'name' چاپ می‌شود
}
console.log(Object.keys(admin)); // ['name']
console.log(Object.getOwnPropertyNames(admin)); // ['name']

// برای دسترسی به Symbolها:
console.log(Object.getOwnPropertySymbols(admin)); // [Symbol(User Identifier), Symbol(User Role)]
console.log(admin[Object.getOwnPropertySymbols(admin)[0]]); // 101

Symbolهای سراسری (Global Symbols)

گاهی اوقات، نیاز به Symbolهایی دارید که در سراسر برنامه قابل اشتراک‌گذاری باشند و منحصربه‌فرد بودن آنها تضمین شود، اما نه به صورت یکتا در هر فراخوانی، بلکه یکتا در رجیستری سراسری Symbolها. برای این منظور، از Symbol.for(key) و Symbol.keyFor(symbol) استفاده می‌شود.


const globalSymbol1 = Symbol.for('app.config');
const globalSymbol2 = Symbol.for('app.config');

console.log(globalSymbol1 === globalSymbol2); // true (از رجیستری سراسری بازگردانده می‌شوند)
console.log(Symbol.keyFor(globalSymbol1)); // 'app.config'

const localSymbol = Symbol('app.config');
console.log(globalSymbol1 === localSymbol); // false (یکی سراسری، دیگری محلی)
console.log(Symbol.keyFor(localSymbol)); // undefined (فقط برای Symbolهای سراسری کار می‌کند)

Symbol.for() ابتدا بررسی می‌کند که آیا Symbol با کلید داده شده در رجیستری سراسری وجود دارد یا خیر. اگر وجود داشت، همان Symbol را برمی‌گرداند؛ در غیر این صورت، یک Symbol جدید ایجاد کرده و آن را در رجیستری ذخیره می‌کند. این مکانیسم برای تعریف Symbolهایی که قرار است توسط بخش‌های مختلف یک سیستم یا کتابخانه به اشتراک گذاشته شوند (مثلاً برای رویدادهای سفارشی یا پروتکل‌ها) بسیار مفید است.

Symbolهای شناخته شده (Well-known Symbols)

جاوا اسکریپت مجموعه‌ای از Symbolهای داخلی و از پیش تعریف شده را ارائه می‌دهد که به آنها “Symbolهای شناخته شده” می‌گویند. این Symbolها برای تغییر رفتار داخلی اشیاء در سناریوهای خاص استفاده می‌شوند و امکان متا برنامه‌نویسی (metaprogramming) را فراهم می‌کنند. برخی از مهم‌ترین آنها عبارتند از:

  • Symbol.iterator: تعیین می‌کند که یک شیء چگونه باید تکرارپذیر (iterable) باشد (مثلاً برای for...of).
  • Symbol.toStringTag: مقدار پیش‌فرض Object.prototype.toString() را تغییر می‌دهد.
  • Symbol.hasInstance: نحوه کار عملگر instanceof را سفارشی می‌کند.
  • Symbol.asyncIterator: برای تکرارکننده‌های ناهمزمان (async iterators) استفاده می‌شود.

مثال برای Symbol.iterator:


class MyCollection {
    constructor(...elements) {
        this.elements = elements;
    }

    // این متد شیء را تکرارپذیر می‌کند
    [Symbol.iterator]() {
        let index = 0;
        const elements = this.elements;
        return {
            next() {
                if (index < elements.length) {
                    return { value: elements[index++], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }
}

const collection = new MyCollection('a', 'b', 'c');
for (let item of collection) {
    console.log(item); // a, b, c
}

Symbolها ابزاری قدرتمند برای افزودن رفتارها یا داده‌های خاص به اشیاء بدون تداخل با ویژگی‌های موجود هستند و نقش کلیدی در طراحی APIهای منعطف و مقیاس‌پذیر در جاوا اسکریپت ایفا می‌کنند.

BigInt: مدیریت اعداد صحیح بسیار بزرگ با دقت بی‌نهایت

یکی از محدودیت‌های اساسی نوع داده Number در جاوا اسکریپت، عدم توانایی آن در نمایش دقیق اعداد صحیح بسیار بزرگ است. تمامی اعداد در جاوا اسکریپت به صورت اعداد ممیز شناور 64 بیتی با دقت مضاعف (double-precision floating-point numbers) طبق استاندارد IEEE 754 ذخیره می‌شوند. این استاندارد اجازه می‌دهد اعداد صحیح را به طور دقیق تنها تا 2^53 - 1 (یعنی Number.MAX_SAFE_INTEGER) و -(2^53 - 1) (یعنی Number.MIN_SAFE_INTEGER) نمایش داد. هر عدد صحیحی خارج از این محدوده ممکن است دقت خود را از دست بدهد و به طور نادرست نمایش داده شود.

نیاز به BigInt

در بسیاری از سناریوهای مدرن توسعه وب، از جمله کار با APIهای بانکی، شناسه‌های منحصر به فرد بزرگ (مانند شناسه‌های پایگاه داده 64 بیتی)، رمزنگاری، یا محاسبات علمی، نیاز به اعدادی داریم که از محدوده ایمن Number فراتر می‌روند. معرفی BigInt در ES2020 این مشکل را حل کرد. BigInt یک نوع داده اولیه جدید است که می‌تواند اعداد صحیح با دقت دلخواه (arbitrary-precision integers) را ذخیره و عملیات ریاضی روی آنها انجام دهد.

ایجاد BigInt

برای ایجاد یک BigInt، می‌توانید عدد صحیح را با پسوند n مشخص کنید یا از تابع سازنده BigInt() استفاده کنید:


const largeNumber = 123456789012345678901234567890n; // استفاده از پسوند 'n'
console.log(typeof largeNumber); // bigint

const anotherLargeNumber = BigInt("98765432109876543210"); // استفاده از تابع سازنده
console.log(anotherLargeNumber); // 98765432109876543210n

// تبدیل یک عدد Number به BigInt
const num = 123;
const bigNum = BigInt(num);
console.log(bigNum); // 123n

// نکته: نمی‌توانید یک عدد ممیز شناور را به BigInt تبدیل کنید.
// BigInt(3.14); // TypeError: Cannot convert 3.14 to a BigInt

عملیات ریاضی با BigInt

تقریباً تمام عملگرهای ریاضیاتی که روی Number کار می‌کنند، روی BigInt نیز قابل استفاده هستند: جمع (+)، تفریق (-)، ضرب (*)، تقسیم (/)، باقی‌مانده (%), توان (**). تنها عملگر بیتی تکین (>>>) که برای اعداد بدون علامت استفاده می‌شود، برای BigInt پشتیبانی نمی‌شود.


const a = 100n;
const b = 20n;

console.log(a + b); // 120n
console.log(a * b); // 2000n
console.log(a / b); // 5n (تقسیم BigInt همیشه نتیجه صحیح برمی‌گرداند و قسمت اعشاری را حذف می‌کند)
console.log(a % b); // 0n
console.log(a ** 2n); // 10000n

ملاحظات مهم: عدم ترکیب با Number

مهم‌ترین نکته در کار با BigInt این است که نمی‌توانید BigInt را مستقیماً با Number در عملیات ریاضی ترکیب کنید. این کار منجر به TypeError می‌شود. برای انجام عملیات، باید هر دو عدد از یک نوع باشند:


const bigNum = 10n;
const regularNum = 5;

// console.log(bigNum + regularNum); // TypeError: Cannot mix BigInt and other types, use explicit conversions.

// تبدیل صریح برای انجام عملیات:
console.log(bigNum + BigInt(regularNum)); // 15n
console.log(Number(bigNum) + regularNum); // 15

این محدودیت به دلیل ماهیت متفاوت نمایش داخلی و جلوگیری از از دست دادن دقت است. تبدیل بین BigInt و Number باید به صورت صریح انجام شود و در هنگام تبدیل BigInt به Number باید مراقب از دست دادن احتمالی دقت برای اعداد بسیار بزرگ بود.

مقایسه‌ها

مقایسه BigInt با BigInt یا BigInt با Number با استفاده از عملگرهای مقایسه‌ای (==, ===, <, >, <=, >=) مجاز است. BigInt و Number زمانی که مقادیر عددی یکسانی دارند با == برابر در نظر گرفته می‌شوند، اما با === (برابر مطلق) خیر، زیرا انواع آنها متفاوت است.


console.log(10n == 10);     // true
console.log(10n === 10);    // false (انواع متفاوت)
console.log(10n > 5);       // true
console.log(10n < 15n);     // true

کاربردهای BigInt

BigInt در سناریوهای زیر بسیار مفید است:

  • شناسه‌های پایگاه داده: بسیاری از سیستم‌های پایگاه داده از شناسه‌های 64 بیتی استفاده می‌کنند که از محدوده Number.MAX_SAFE_INTEGER فراتر می‌روند. BigInt امکان مدیریت دقیق این شناسه‌ها را در سمت کلاینت یا سرور (در Node.js) فراهم می‌کند.
  • رمزنگاری: عملیات رمزنگاری غالباً شامل اعداد بسیار بزرگ است که برای دقت بالا به BigInt نیاز دارند.
  • سیستم‌های مالی: برای جلوگیری از خطاهای گرد کردن در محاسبات پولی که نیاز به دقت بسیار بالا دارند.
  • سیستم‌های اندازه‌گیری بسیار بزرگ: در محاسبات علمی یا فیزیکی که مقادیر ممکن است بسیار بزرگ یا بسیار کوچک باشند (اگرچه برای مقادیر بسیار کوچک نیاز به ممیز شناور با دقت بالا است).

با وجود BigInt، جاوا اسکریپت اکنون ابزار قدرتمندی برای کار با اعداد صحیح با هر اندازه دلخواه در اختیار دارد و محدودیت‌های دقت عددی گذشته را از بین برده است.

Map: کلید-مقدار با انعطاف‌پذیری نامحدود

Map یک ساختار داده است که مجموعه‌ای از جفت‌های کلید-مقدار را ذخیره می‌کند، شبیه به Object. با این حال، تفاوت‌های مهمی با Object دارد که آن را در بسیاری از سناریوها به گزینه‌ای قدرتمندتر تبدیل می‌کند. در Object، کلیدها همیشه به رشته (String) یا Symbol تبدیل می‌شوند. اما در Map، کلیدها می‌توانند از هر نوع داده‌ای باشند: رشته، عدد، شیء، تابع، حتی null یا undefined. این انعطاف‌پذیری Map را برای ذخیره‌سازی داده‌هایی که کلیدهای آنها پیچیده‌تر از رشته‌های ساده هستند، ایده‌آل می‌کند.

ویژگی‌های کلیدی Map

  • کلیدهای از هر نوع: قابلیت استفاده از هر مقدار (از جمله اشیاء و توابع) به عنوان کلید.
  • حفظ ترتیب درج: Map ترتیب درج عناصر را به خاطر می‌سپارد و هنگام تکرار روی آن، عناصر به همان ترتیبی که اضافه شده‌اند بازگردانده می‌شوند. (برخلاف اشیاء قدیمی‌تر که ترتیب کلیدها تضمین شده نبود، اگرچه در ES2015 به بعد، ترتیب کلیدهای عددی و سپس رشته‌ای در اشیاء تضمین شده است، اما برای کلیدهای Symbol همچنان متفاوت است).
  • اندازه (Size) قابل دسترسی: Map دارای یک ویژگی .size است که به راحتی تعداد عناصر موجود در آن را برمی‌گرداند. (در Object برای شمارش باید از Object.keys().length استفاده کرد).
  • عملکرد بهتر برای عملیات افزودن/حذف: در سناریوهای خاص، عملیات افزودن و حذف در Map می‌تواند عملکرد بهتری نسبت به Object داشته باشد.
  • قابلیت تکرارپذیری مستقیم: Map به طور پیش‌فرض تکرارپذیر (iterable) است، به این معنی که می‌توانید به طور مستقیم از for...of روی آن استفاده کنید تا جفت‌های کلید-مقدار را پیمایش کنید.

ایجاد و استفاده از Map

Map با فراخوانی تابع سازنده new Map() ایجاد می‌شود. می‌توانید یک آرایه از آرایه‌های دو عنصری ([key, value]) برای مقداردهی اولیه به آن پاس دهید.


// ایجاد یک Map خالی
const myMap = new Map();

// اضافه کردن عناصر با متد .set()
myMap.set('name', 'Alice');
myMap.set(1, 'One');
myMap.set(true, 'Boolean Key');
const someObject = { id: 1 };
myMap.set(someObject, 'Value for an object key');
myMap.set(() => {}, 'Value for a function key');

console.log(myMap.size); // 5

// دریافت مقادیر با متد .get()
console.log(myMap.get('name')); // Alice
console.log(myMap.get(1)); // One
console.log(myMap.get(someObject)); // Value for an object key
console.log(myMap.get({ id: 1 })); // undefined (این یک شیء جدید است، نه همان شیء قبلی)

// بررسی وجود کلید با متد .has()
console.log(myMap.has('name')); // true
console.log(myMap.has('age')); // false

// حذف یک عنصر با متد .delete()
myMap.delete('name');
console.log(myMap.has('name')); // false
console.log(myMap.size); // 4

// پاک کردن تمام عناصر با متد .clear()
myMap.clear();
console.log(myMap.size); // 0

// ایجاد Map با مقداردهی اولیه از آرایه جفت‌ها
const initialData = [['key1', 'value1'], ['key2', 'value2']];
const initializedMap = new Map(initialData);
console.log(initializedMap.get('key1')); // value1

پیمایش Map

Mapها تکرارپذیر هستند و می‌توانند با for...of یا forEach پیمایش شوند:


const userRoles = new Map([
    ['Alice', 'Admin'],
    ['Bob', 'Editor'],
    ['Charlie', 'Viewer']
]);

// پیمایش با for...of برای دسترسی به جفت‌های [key, value]
for (const [name, role] of userRoles) {
    console.log(`${name} is a ${role}`);
}
// خروجی:
// Alice is a Admin
// Bob is a Editor
// Charlie is a Viewer

// پیمایش با forEach
userRoles.forEach((role, name) => {
    console.log(`${name} has role: ${role}`);
});

// دریافت کلیدها، مقادیر یا جفت‌های ورودی به صورت جداگانه
console.log(userRoles.keys());   // MapIterator { 'Alice', 'Bob', 'Charlie' }
console.log(userRoles.values()); // MapIterator { 'Admin', 'Editor', 'Viewer' }
console.log(userRoles.entries());// MapIterator { ['Alice', 'Admin'], ['Bob', 'Editor'], ['Charlie', 'Viewer'] }

// تبدیل Map به آرایه
const rolesArray = [...userRoles];
console.log(rolesArray); // [['Alice', 'Admin'], ['Bob', 'Editor'], ['Charlie', 'Viewer']]

کاربردهای Map

Map در سناریوهای زیر بسیار مفید است:

  • ذخیره‌سازی داده‌های مرتبط با اشیاء DOM: زمانی که می‌خواهید داده‌های اضافی را به عناصر DOM پیوند دهید بدون اینکه آنها را مستقیماً به عنوان ویژگی به عناصر اضافه کنید (که می‌تواند منجر به نشت حافظه یا تداخل شود).
  • پایگاه داده‌های موقت یا کش (Cache): برای ذخیره‌سازی نتایج عملیات گران‌قیمت یا داده‌هایی که به سرعت نیاز به بازیابی دارند، با استفاده از کلیدهای پیچیده.
  • پیاده‌سازی memoization: ذخیره نتایج توابع با ورودی‌های خاص برای جلوگیری از محاسبات تکراری.
  • گراف‌ها و درخت‌ها: برای نمایش ساختارهایی که نیاز به ارجاعات پیچیده با استفاده از اشیاء به عنوان کلید دارند.
  • جایگزینی برای اشیاء در مواردی که کلیدها غیر رشته‌ای هستند: هرگاه نیاز به استفاده از اعداد، اشیاء، یا حتی توابع به عنوان کلید دارید، Map گزینه برتر است.

با توجه به انعطاف‌پذیری و عملکرد Map، این ساختار داده اغلب جایگزین مناسبی برای Object در مواقعی است که نیاز به کنترل بیشتر بر کلیدها و ترتیب عناصر دارید.

Set: مجموعه‌هایی از مقادیر منحصربه‌فرد

Set یک ساختار داده است که مجموعه‌ای از مقادیر منحصربه‌فرد را ذخیره می‌کند. برخلاف Array که می‌تواند شامل عناصر تکراری باشد، هر عنصر در یک Set فقط یک بار می‌تواند وجود داشته باشد. این ویژگی Set را برای سناریوهایی که نیاز به اطمینان از منحصربه‌فرد بودن عناصر دارید، ایده‌آل می‌کند. Set همچنین مانند Map، ترتیب درج عناصر را حفظ می‌کند.

ویژگی‌های کلیدی Set

  • منحصربه‌فرد بودن عناصر: Set به طور خودکار مقادیر تکراری را حذف می‌کند. هنگامی که سعی می‌کنید یک مقدار موجود را اضافه کنید، هیچ اتفاقی نمی‌افتد و Set بدون تغییر باقی می‌ماند.
  • قابلیت نگهداری هر نوع داده: همانند Map، هر نوع داده‌ای می‌تواند به عنوان عنصر در Set ذخیره شود، از جمله اشیاء، توابع، null، undefined، NaN (NaN یکتاست، یعنی یک بار در Set می‌تواند باشد).
  • حفظ ترتیب درج: عناصر در همان ترتیبی که به Set اضافه شده‌اند، پیمایش می‌شوند.
  • اندازه (Size) قابل دسترسی: ویژگی .size تعداد عناصر در Set را برمی‌گرداند.
  • قابلیت تکرارپذیری مستقیم: Set تکرارپذیر است و می‌توان آن را با for...of یا forEach پیمایش کرد.

ایجاد و استفاده از Set

Set با فراخوانی تابع سازنده new Set() ایجاد می‌شود. می‌توانید یک آرایه یا هر شیء تکرارپذیر دیگری را برای مقداردهی اولیه به آن پاس دهید.


// ایجاد یک Set خالی
const mySet = new Set();

// اضافه کردن عناصر با متد .add()
mySet.add(1);
mySet.add(5);
mySet.add('text');
mySet.add(1); // تلاش برای افزودن مقدار تکراری، نادیده گرفته می‌شود

const obj1 = { id: 1 };
const obj2 = { id: 2 };
mySet.add(obj1);
mySet.add(obj2);
mySet.add(obj1); // شیء obj1 مجدداً اضافه نمی‌شود

console.log(mySet.size); // 5 (1, 5, 'text', obj1, obj2)

// بررسی وجود عنصر با متد .has()
console.log(mySet.has(1)); // true
console.log(mySet.has(10)); // false
console.log(mySet.has(obj1)); // true
console.log(mySet.has({ id: 1 })); // false (این یک شیء جدید است)

// حذف یک عنصر با متد .delete()
mySet.delete(5);
console.log(mySet.size); // 4
console.log(mySet.has(5)); // false

// پاک کردن تمام عناصر با متد .clear()
mySet.clear();
console.log(mySet.size); // 0

// ایجاد Set با مقداردهی اولیه از آرایه
const numbers = [1, 2, 3, 2, 1, 4, 5, 4];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers); // Set { 1, 2, 3, 4, 5 }

پیمایش Set

Setها می‌توانند با for...of یا forEach پیمایش شوند:


const fruits = new Set(['Apple', 'Banana', 'Orange']);

// پیمایش با for...of
for (const fruit of fruits) {
    console.log(fruit);
}
// خروجی:
// Apple
// Banana
// Orange

// پیمایش با forEach
fruits.forEach(fruit => {
    console.log(`I love ${fruit}`);
});

// دریافت iterator برای مقادیر (کلیدها و ورودی‌ها در Set یکی هستند)
console.log(fruits.values()); // SetIterator { 'Apple', 'Banana', 'Orange' }
console.log(fruits.keys());   // SetIterator { 'Apple', 'Banana', 'Orange' }
console.log(fruits.entries());// SetIterator { ['Apple', 'Apple'], ['Banana', 'Banana'], ['Orange', 'Orange'] }

// تبدیل Set به آرایه
const fruitsArray = [...fruits];
console.log(fruitsArray); // ['Apple', 'Banana', 'Orange']

عملیات مجموعه‌ای (Set Operations)

هرچند Set متدهای داخلی برای عملیات مجموعه‌ای مانند اجتماع (union)، اشتراک (intersection) و تفاضل (difference) ندارد، اما می‌توان این عملیات را به راحتی با ترکیب متدهای Set و Array انجام داد:


const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// اجتماع (Union): تمام عناصر موجود در حداقل یکی از مجموعه‌ها
const union = new Set([...setA, ...setB]);
console.log(union); // Set { 1, 2, 3, 4, 5, 6 }

// اشتراک (Intersection): تمام عناصر موجود در هر دو مجموعه
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set { 3, 4 }

// تفاضل (Difference): تمام عناصر موجود در setA که در setB نیستند
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set { 1, 2 }

کاربردهای Set

Set در سناریوهای زیر بسیار مفید است:

  • حذف عناصر تکراری از یک آرایه: ساده‌ترین و کارآمدترین راه برای تبدیل یک آرایه با عناصر تکراری به آرایه‌ای با عناصر منحصربه‌فرد.
  • ردیابی موارد منحصربه‌فرد: مثلاً در یک سیستم ثبت بازدیدها، برای ردیابی IPهای منحصربه‌فرد بازدیدکنندگان.
  • بررسی سریع وجود یک عنصر: عملیات .has() در Set به طور متوسط پیچیدگی زمانی O(1) دارد که برای مجموعه داده‌های بزرگ بسیار کارآمدتر از جستجو در یک آرایه است (O(n)).
  • مدیریت تگ‌ها یا برچسب‌ها: برای اطمینان از اینکه هر تگ فقط یک بار به یک آیتم اختصاص داده شده است.

Set ابزاری قدرتمند برای مدیریت مجموعه‌هایی از داده‌های منحصربه‌فرد است و در ترکیب با Array و Map، امکان ایجاد ساختارهای داده‌ای پیچیده‌تر و بهینه‌تر را فراهم می‌کند.

WeakMap و WeakSet: بهینه‌سازی حافظه با ارجاعات ضعیف

WeakMap و WeakSet نسخه‌های خاصی از Map و Set هستند که با هدف بهینه‌سازی مصرف حافظه و جلوگیری از نشت حافظه (memory leaks) طراحی شده‌اند. تفاوت اصلی آنها در نحوه نگهداری ارجاعات به عناصرشان است: WeakMap و WeakSet از "ارجاعات ضعیف" (weak references) استفاده می‌کنند، در حالی که Map و Set از "ارجاعات قوی" (strong references) استفاده می‌کنند.

ارجاعات قوی در مقابل ارجاعات ضعیف

  • ارجاع قوی (Strong Reference): تا زمانی که یک شیء توسط حداقل یک ارجاع قوی نگهداری می‌شود، JavaScript Garbage Collector (GC) آن شیء را از حافظه حذف نمی‌کند. اگر یک شیء به عنوان کلید در Map یا عنصر در Set استفاده شود، Map/Set یک ارجاع قوی به آن شیء نگه می‌دارد، حتی اگر هیچ ارجاع دیگری به آن شیء در برنامه وجود نداشته باشد. این می‌تواند منجر به نشت حافظه شود.
  • ارجاع ضعیف (Weak Reference): یک ارجاع ضعیف، مانع از حذف شیء توسط GC نمی‌شود. اگر تنها ارجاع به یک شیء، یک ارجاع ضعیف (مثلاً از WeakMap یا WeakSet) باشد، GC می‌تواند آن شیء را از حافظه آزاد کند. هنگامی که یک شیء از حافظه حذف می‌شود، تمام ارجاعات ضعیف به آن شیء نیز به طور خودکار حذف می‌شوند.

WeakMap

WeakMap شبیه به Map است با این تفاوت‌های کلیدی:

  • کلیدها فقط می‌توانند شیء باشند: کلیدها در WeakMap باید حتماً شیء باشند (null، undefined، Boolean، Number، String، Symbol به عنوان کلید مجاز نیستند).
  • ارجاعات ضعیف به کلیدها: WeakMap ارجاع ضعیفی به کلیدهای خود نگه می‌دارد. اگر تنها ارجاع به یک شیء، کلید آن در WeakMap باشد، آن شیء و جفت کلید-مقدار مربوطه از WeakMap حذف خواهند شد زمانی که GC آن شیء را جمع‌آوری کند.
  • عدم قابلیت پیمایش: WeakMap قابلیت پیمایش (iteration) ندارد (یعنی نمی‌توانید از for...of، forEach، .keys()، .values()، .entries() استفاده کنید). دلیل این محدودیت این است که نمی‌توان تضمین کرد چه زمانی یک کلید ممکن است توسط GC حذف شود، بنابراین حفظ ثبات در حین پیمایش غیرممکن است.
  • عدم دسترسی به .size: به دلیل عدم قابلیت پیمایش و ماهیت پویا، WeakMap ویژگی .size را ندارد.

متدهای WeakMap

WeakMap متدهای .set(key, value)، .get(key)، .has(key) و .delete(key) را ارائه می‌دهد. متد .clear() ندارد.


let obj1 = { name: 'Alice' };
let obj2 = { name: 'Bob' };

const myWeakMap = new WeakMap();

myWeakMap.set(obj1, 'Data for Alice');
myWeakMap.set(obj2, 'Data for Bob');

console.log(myWeakMap.get(obj1)); // Data for Alice
console.log(myWeakMap.has(obj2)); // true

obj1 = null; // ارجاع قوی به obj1 را حذف می‌کنیم

// در این مرحله، obj1 (و جفت آن در WeakMap) ممکن است توسط GC جمع‌آوری شود.
// نمی‌توانیم بلافاصله آن را تأیید کنیم زیرا GC به صورت غیرقطعی عمل می‌کند.
// اما تضمین شده است که در نهایت حذف خواهد شد.

کاربردهای WeakMap

  • داده‌های خصوصی یا Metadata برای اشیاء: زمانی که می‌خواهید داده‌های خصوصی یا اطلاعات اضافی را به یک شیء اضافه کنید، اما این داده‌ها باید همراه با شیء اصلی از بین بروند. به عنوان مثال، اطلاعات پیکربندی برای یک شیء DOM.
  • کشینگ (Caching) با قابلیت GC: ایجاد یک کش که در آن آیتم‌های کش شده به محض اینکه ارجاع دیگری به آنها نباشد، از حافظه حذف می‌شوند.

WeakSet

WeakSet شبیه به Set است با این تفاوت‌های کلیدی:

  • عناصر فقط می‌توانند شیء باشند: عناصر در WeakSet باید حتماً شیء باشند (همان محدودیت‌های کلید در WeakMap).
  • ارجاعات ضعیف به عناصر: WeakSet ارجاع ضعیفی به عناصر خود نگه می‌دارد. اگر تنها ارجاع به یک شیء، وجود آن در WeakSet باشد، آن شیء از WeakSet حذف خواهد شد زمانی که GC آن را جمع‌آوری کند.
  • عدم قابلیت پیمایش: همانند WeakMap، WeakSet نیز قابلیت پیمایش ندارد.
  • عدم دسترسی به .size: WeakSet نیز ویژگی .size را ندارد.

متدهای WeakSet

WeakSet متدهای .add(value)، .has(value) و .delete(value) را ارائه می‌دهد. متد .clear() ندارد.


let elem1 = { id: 1 };
let elem2 = { id: 2 };

const myWeakSet = new WeakSet();

myWeakSet.add(elem1);
myWeakSet.add(elem2);

console.log(myWeakSet.has(elem1)); // true

elem1 = null; // ارجاع قوی به elem1 را حذف می‌کنیم

// elem1 (و وجود آن در WeakSet) ممکن است توسط GC جمع‌آوری شود.

کاربردهای WeakSet

  • ردیابی اشیاء بدون جلوگیری از GC: زمانی که می‌خواهید ردیابی کنید که آیا یک شیء خاص قبلاً دیده شده است یا خیر، بدون اینکه مانع از جمع‌آوری آن توسط GC شوید. به عنوان مثال، برای جلوگیری از اضافه شدن یک شیء به یک پردازش خاص در صورت وجود قبلی آن.
  • ثبت شنوندگان رویداد برای اشیاء DOM: می‌توان از WeakSet برای نگهداری ارجاعات به عناصر DOM که شنونده رویداد به آنها اضافه شده است، استفاده کرد. هنگامی که یک عنصر DOM از صفحه حذف شود و هیچ ارجاع دیگری به آن نباشد، GC می‌تواند آن را همراه با ارجاع آن در WeakSet حذف کند.

WeakMap و WeakSet ابزارهای پیشرفته‌ای هستند که در موارد خاصی که مدیریت حافظه و جلوگیری از نشت آن اهمیت حیاتی دارد، به کار می‌روند. درک تفاوت آنها با Map و Set و زمان استفاده از هر کدام، نشان‌دهنده تسلط بر جنبه‌های عمیق‌تر جاوا اسکریپت است.

Typed Arrays و ArrayBuffer: کارایی بالا با داده‌های باینری

در حالی که آرایه‌های معمولی جاوا اسکریپت (Array) بسیار انعطاف‌پذیر هستند و می‌توانند انواع داده‌های مختلف را در خود نگه دارند، اما برای کار با داده‌های عددی با حجم بالا و بهینه‌سازی عملکرد، به خصوص در عملیات سطح پایین‌تر مانند دستکاری پیکسل‌ها در Canvas، پردازش صدا، WebSockets یا تعامل با WebAssembly، همیشه ایده‌آل نیستند. اینجا جایی است که Typed Arrays و ArrayBuffer به میدان می‌آیند.

ArrayBuffer: بافر حافظه خام

ArrayBuffer یک شیء است که یک قطعه حافظه دودویی (binary data buffer) با اندازه ثابت را نمایش می‌دهد. این به خودی خود هیچ گونه فرمت خاصی ندارد و نمی‌توانید مستقیماً با آن کار کنید یا محتویات آن را دستکاری کنید. ArrayBuffer فقط یک "نقطه" در حافظه است که می‌توانید آن را به عنوان یک آرایه از بایت‌ها در نظر بگیرید.


// ایجاد یک ArrayBuffer با ظرفیت 16 بایت
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16

برای خواندن یا نوشتن داده‌ها در یک ArrayBuffer، به یک "نما" (view) نیاز دارید.

Typed Arrays: نماهایی با نوع مشخص

Typed Arrays نماهایی (views) هستند که به شما امکان می‌دهند به داده‌های باینری ذخیره شده در یک ArrayBuffer با نوع خاص و قالب مشخصی دسترسی پیدا کنید. هر Typed Array برای یک نوع داده عددی خاص طراحی شده است (مانند اعداد صحیح 8 بیتی، 16 بیتی، 32 بیتی یا اعداد ممیز شناور). این نماها عملکرد بهتری برای عملیات ریاضیاتی روی داده‌های عددی فراهم می‌کنند، زیرا موتور جاوا اسکریپت دقیقاً می‌داند هر بایت چه نوع داده‌ای را نشان می‌دهد.

انواع متداول Typed Arrays عبارتند از:

  • Int8Array: اعداد صحیح 8 بیتی علامت‌دار (-128 تا 127)
  • Uint8Array: اعداد صحیح 8 بیتی بدون علامت (0 تا 255)
  • Int16Array: اعداد صحیح 16 بیتی علامت‌دار
  • Uint16Array: اعداد صحیح 16 بیتی بدون علامت
  • Int32Array: اعداد صحیح 32 بیتی علامت‌دار
  • Uint32Array: اعداد صحیح 32 بیتی بدون علامت
  • Float32Array: اعداد ممیز شناور 32 بیتی
  • Float64Array: اعداد ممیز شناور 64 بیتی (دقت مضاعف)
  • BigInt64Array: اعداد صحیح 64 بیتی علامت‌دار (از نوع BigInt)
  • BigUint64Array: اعداد صحیح 64 بیتی بدون علامت (از نوع BigInt)

مثال استفاده از Typed Array


// ایجاد یک ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 بایت حافظه خام

// ایجاد یک Uint32Array (اعداد صحیح 32 بیتی بدون علامت) به عنوان نما روی بافر
// هر عدد 32 بیتی 4 بایت فضا اشغال می‌کند. 16 بایت / 4 بایت = 4 عنصر.
const uint32Array = new Uint32Array(buffer);

console.log(uint32Array.length); // 4 (تعداد عناصر قابل ذخیره در این نما)
console.log(uint32Array.byteLength); // 16 (اندازه کلی نما در بایت)
console.log(uint32Array.byteOffset); // 0 (محل شروع نما از ابتدای بافر)

// اختصاص دادن مقادیر
uint32Array[0] = 42;
uint32Array[1] = 100;
uint32Array[2] = 200;
uint32Array[3] = 300;

console.log(uint32Array); // Uint32Array [ 42, 100, 200, 300 ]

// ایجاد یک نما دیگر با نوع متفاوت روی همان بافر
// هر عدد 8 بیتی 1 بایت فضا اشغال می‌کند. 16 بایت / 1 بایت = 16 عنصر.
const uint8Array = new Uint8Array(buffer);
console.log(uint8Array.length); // 16

// ببینید چگونه تغییرات در یک نما، در دیگری نیز منعکس می‌شود
console.log(uint8Array[0]); // 42 (بایت اول از 42)
console.log(uint8Array[1]); // 0
console.log(uint8Array[2]); // 0
console.log(uint8Array[3]); // 0 (اگر 42 به عنوان یک عدد 32 بیتی در نظر گرفته شود، تنها بایت اول آن 42 است و بقیه‌ی بایت‌ها 0 خواهند بود.)

uint8Array[0] = 255; // تغییر بایت اول
console.log(uint32Array[0]); // 255 (مقدار 42 در uint32Array به 255 تغییر کرد!)
// این به دلیل Endianness است (نحوه ذخیره بایت‌ها برای یک عدد چند بایتی).
// در اکثر سیستم‌ها، Little-endian رایج است، یعنی بایت کم‌ارزش‌تر در آدرس حافظه پایین‌تر قرار می‌گیرد.
// 42 در 32-bit: 0x0000002A
// uint8Array[0] = 0x2A, uint8Array[1] = 0x00, uint8Array[2] = 0x00, uint8Array[3] = 0x00
// وقتی uint8Array[0] را به 255 (0xFF) تغییر می‌دهیم:
// 0xFF000000 (در Little-endian) که برابر با 255 است.

DataView: کنترل دقیق‌تر بر بایت‌ها

DataView یک نما (view) دیگر برای ArrayBuffer است که برخلاف Typed Arrays، هیچ نوع داده‌ای به آن متصل نیست و به شما امکان می‌دهد بایت‌ها را در هر آفستی (offset) با هر نوع داده‌ای بخوانید یا بنویسید و حتی Endianness را مشخص کنید (Big-endian یا Little-endian).


const buffer = new ArrayBuffer(8); // 8 بایت
const view = new DataView(buffer);

// نوشتن یک عدد صحیح 32 بیتی در آفست 0 (با Little-endian)
view.setInt32(0, 12345678, true); // true برای Little-endian

// خواندن یک عدد صحیح 16 بیتی از آفست 2 (با Little-endian)
console.log(view.getInt16(2, true)); // خروجی: 189 (بایت‌های 2 و 3 از 12345678)

DataView زمانی مفید است که شما نیاز دارید داده‌های باینری را از یک منبع خارجی با فرمت‌های پیچیده (مانند پروتکل‌های شبکه) تجزیه کنید.

کاربردهای Typed Arrays و ArrayBuffer

  • WebGL و Canvas 2D: برای کارایی بالا در دستکاری پیکسل‌ها، داده‌های هندسی و بافت‌ها.
  • پردازش صدا و ویدئو: برای کار با داده‌های صوتی و تصویری خام.
  • WebSockets: ارسال و دریافت داده‌های باینری بهینه.
  • WebAssembly: تعامل با کد WebAssembly که با حافظه خام کار می‌کند.
  • فایل‌های باینری: خواندن و نوشتن فایل‌ها با فرمت‌های خاص (مانند تصاویر).
  • محاسبات عددی سنگین: زمانی که نیاز به انجام عملیات ریاضیاتی روی مجموعه‌های بزرگ اعداد با عملکرد بالا دارید.

با استفاده از Typed Arrays و ArrayBuffer، توسعه‌دهندگان جاوا اسکریپت می‌توانند به صورت کارآمدتری با داده‌های باینری سطح پایین کار کنند و به عملکردی نزدیک به زبان‌های سطح پایین‌تر دست یابند، که این امکان را برای کاربردهای پیچیده‌تر و پربارتر در مرورگر و Node.js فراهم می‌آورد.

Proxy و Reflect: متا برنامه‌نویسی و کنترل رفتار شیء

Proxy و Reflect دو ویژگی قدرتمند در ES6 هستند که امکان متا برنامه‌نویسی (metaprogramming) در جاوا اسکریپت را فراهم می‌کنند. متا برنامه‌نویسی به معنی نوشتن کدی است که کد دیگر را دستکاری یا تجزیه و تحلیل می‌کند. در جاوا اسکریپت، این به معنای توانایی "رهگیری" و "سفارشی‌سازی" عملیات اساسی روی اشیاء مانند دسترسی به ویژگی‌ها، انتساب مقادیر، فراخوانی توابع و غیره است.

Proxy: رهگیر عملیات شیء

یک شیء Proxy به شما امکان می‌دهد رفتار یک شیء دیگر (که به آن target می‌گویند) را در عملیات اساسی (مانند دسترسی به ویژگی‌ها، انتساب، فراخوانی) رهگیری و سفارشی‌سازی کنید. شما یک Proxy را با دو آرگومان ایجاد می‌کنید: target (شیء که قرار است پروکسی شود) و handler (یک شیء شامل متدهایی به نام "traps" که عملیات رهگیری شده را تعریف می‌کنند).


// شیء هدف
const target = {
    message1: "Hello",
    message2: "World"
};

// شیء handler با traps
const handler = {
    // trap برای عملیات 'get' (دسترسی به ویژگی)
    get(target, property, receiver) {
        console.log(`Getting property "${String(property)}"`);
        // بازگرداندن مقدار اصلی ویژگی
        return Reflect.get(target, property, receiver);
    },
    // trap برای عملیات 'set' (انتساب ویژگی)
    set(target, property, value, receiver) {
        if (property === 'message1' && typeof value !== 'string') {
            throw new TypeError('message1 must be a string!');
        }
        console.log(`Setting property "${String(property)}" to "${value}"`);
        // انتساب مقدار اصلی ویژگی
        return Reflect.set(target, property, value, receiver);
    }
};

// ایجاد یک شیء Proxy
const proxy = new Proxy(target, handler);

console.log(proxy.message1); // Getting property "message1"
                             // Hello

proxy.message2 = "Proxy World"; // Setting property "message2" to "Proxy World"
console.log(proxy.message2);    // Getting property "message2"
                                // Proxy World

// proxy.message1 = 123; // TypeError: message1 must be a string!

در این مثال، هر بار که به یک ویژگی proxy دسترسی پیدا می‌کنید (get) یا مقداری به آن اختصاص می‌دهید (set)، توابع get و set در handler فراخوانی می‌شوند و به شما امکان می‌دهند قبل، در حین یا بعد از عملیات اصلی، منطق سفارشی را اجرا کنید. این مکانیسم بسیار قدرتمند است و بسیاری از فریم‌ورک‌های مدرن (مانند Vue 3.x برای Reactive System) از آن استفاده می‌کنند.

تعدادی از traps مهم Proxy

  • get(target, property, receiver): رهگیری دسترسی به ویژگی‌ها.
  • set(target, property, value, receiver): رهگیری انتساب ویژگی‌ها.
  • has(target, property): رهگیری عملگر in.
  • deleteProperty(target, property): رهگیری عملگر delete.
  • apply(target, thisArg, argumentsList): رهگیری فراخوانی یک تابع پروکسی شده.
  • construct(target, argumentsList, newTarget): رهگیری فراخوانی یک سازنده پروکسی شده با new.
  • defineProperty(target, property, descriptor): رهگیری Object.defineProperty().
  • getOwnPropertyDescriptor(target, property): رهگیری Object.getOwnPropertyDescriptor().
  • getPrototypeOf(target): رهگیری Object.getPrototypeOf().
  • setPrototypeOf(target, prototype): رهگیری Object.setPrototypeOf().
  • isExtensible(target): رهگیری Object.isExtensible().
  • preventExtensions(target): رهگیری Object.preventExtensions().
  • ownKeys(target): رهگیری Object.keys()، Object.getOwnPropertyNames()، Object.getOwnPropertySymbols().

Reflect: یک API سازگار برای عملیات شیء

Reflect یک شیء داخلی جدید در ES6 است که متدهایی را برای فراخوانی عملیات شیء فراهم می‌کند. این متدها تقریباً معادل همان عملیاتی هستند که به طور معمول با استفاده از عملگرها (مانند in، delete) یا متدهای Object (مانند Object.keys()، Object.defineProperty()) انجام می‌دهیم. اما Reflect این عملیات را در یک API واحد و سازگار گروه‌بندی می‌کند.

مزایای Reflect

  • استانداردسازی: Reflect متدهایی برای انجام عملیات پیش‌فرض جاوا اسکریپت روی اشیاء فراهم می‌کند که به صورت تابعی و با استفاده از پارامترهای مشخص قابل فراخوانی هستند. این به جای استفاده از عملگرها که همیشه در یک تابع قابل استفاده نیستند، یک رویکرد تمیزتر ارائه می‌دهد.
  • سادگی و سازگاری با Proxy: متدهای Reflect دارای همان سیگنچر (signature) ورودی هستند که traps در Proxy دارند، که استفاده از آنها را در داخل handlerهای پروکسی بسیار آسان و توصیه شده می‌کند.
  • بازگشت مقادیر منطقی: برخلاف برخی عملیات Object که در صورت شکست خطا پرتاب می‌کنند، متدهای Reflect معمولاً یک مقدار بولین (true/false) برمی‌گردانند که نشان‌دهنده موفقیت یا شکست عملیات است.

const myObject = {};

// معادل myObject.a = 1;
Reflect.set(myObject, 'a', 1);
console.log(myObject.a); // 1

// معادل 'a' in myObject;
console.log(Reflect.has(myObject, 'a')); // true

// معادل delete myObject.a;
Reflect.deleteProperty(myObject, 'a');
console.log(myObject.a); // undefined

// استفاده در Proxy (مثالی که در بالا هم بود)
const handler2 = {
    get(target, property, receiver) {
        console.log(`Accessing ${property}`);
        return Reflect.get(target, property, receiver);
    }
};
const proxy2 = new Proxy({ x: 10 }, handler2);
console.log(proxy2.x); // Accessing x \n 10

کاربردهای Proxy و Reflect

  • Validation (اعتبارسنجی): اطمینان از اینکه مقادیر اختصاص داده شده به ویژگی‌ها با قوانین خاصی مطابقت دارند.
  • Logging و Debugging: ثبت تمام عملیات انجام شده روی یک شیء برای اهداف اشکال‌زدایی یا نظارت.
  • Access Control: محدود کردن دسترسی به ویژگی‌ها یا متدهای خاص بر اساس مجوزهای کاربر.
  • Reactivity (واکنش‌گرایی): ساخت سیستم‌هایی که به طور خودکار به تغییرات داده‌ها واکنش نشان می‌دهند (مثل Vue.js).
  • Auto-Populating Properties: ایجاد ویژگی‌ها به صورت پویا در صورت درخواست.
  • Memoization: کش کردن نتایج توابع با استفاده از پروکسی روی ورودی‌ها.
  • ORM (Object-Relational Mapping): ایجاد شیءهایی که به طور شفاف با یک پایگاه داده تعامل دارند.

Proxy و Reflect ابزارهای فوق‌العاده قدرتمندی هستند که امکان دستکاری رفتار اشیاء در زمان اجرا را فراهم می‌کنند و پایه و اساس بسیاری از الگوهای طراحی پیشرفته و فریم‌ورک‌های مدرن جاوا اسکریپت را تشکیل می‌دهند. درک عمیق آنها شما را قادر می‌سازد به سطوح جدیدی از انتزاع و انعطاف‌پذیری در کد جاوا اسکریپت دست یابید.

انتخاب ساختار داده مناسب و ملاحظات عملکردی

تا اینجا، ما به بررسی دقیق انواع داده‌ها و ساختارهای پیشرفته در جاوا اسکریپت پرداختیم: Symbol، BigInt، Map، Set، WeakMap، WeakSet، Typed Arrays، ArrayBuffer، Proxy و Reflect. هر کدام از اینها برای حل مسائل خاصی بهینه‌سازی شده‌اند و درک زمان و نحوه استفاده از آنها برای نوشتن کدهای کارآمد، مقیاس‌پذیر و قابل نگهداری ضروری است.

راهنمای انتخاب ساختار داده

انتخاب ساختار داده مناسب به عوامل مختلفی بستگی دارد، از جمله:

  1. نوع داده‌ای که می‌خواهید ذخیره کنید: آیا اعداد بزرگ هستند؟ آیا نیاز به کلیدهای غیر رشته‌ای دارید؟ آیا فقط مقادیر منحصربه‌فرد می‌خواهید؟
  2. عملیات غالب: بیشتر چه عملیاتی را روی داده‌ها انجام خواهید داد؟ افزودن، حذف، جستجو، پیمایش، یا دستکاری بایت‌ها؟
  3. ملاحظات حافظه: آیا نشت حافظه یک نگرانی است؟ آیا با داده‌های باینری بزرگ کار می‌کنید؟
  4. کارایی (Performance): در مجموعه داده‌های بزرگ، عملکرد عملیات اهمیت پیدا می‌کند.

در ادامه یک راهنمای خلاصه برای انتخاب ارائه شده است:

  • Array (آرایه):
    • کی استفاده کنیم:
      • زمانی که نیاز به یک لیست مرتب از آیتم‌ها دارید.
      • زمانی که ترتیب عناصر اهمیت دارد.
      • برای مجموعه‌ای از آیتم‌ها که ممکن است شامل تکراری‌ها باشند.
      • زمانی که عملیات اصلی شما شامل افزودن/حذف در انتها یا ابتدای لیست، دسترسی به عنصر با ایندکس، یا پیمایش خطی است.
    • ملاحظات: عملیات جستجو (indexOf، includes) برای آرایه‌های بزرگ می‌تواند کند باشد (O(n)). افزودن/حذف از وسط آرایه نیز می‌تواند گران باشد.
  • Object (شیء):
    • کی استفاده کنیم:
      • زمانی که نیاز به ذخیره‌سازی جفت‌های کلید-مقدار دارید و کلیدهای شما عمدتاً رشته‌ها یا Symbolهای ثابت هستند.
      • برای مدل‌سازی موجودیت‌ها با ویژگی‌های مشخص (مثلاً یک کاربر با نام، سن، ایمیل).
      • زمانی که به دسترسی سریع به مقادیر با استفاده از کلید آنها نیاز دارید (O(1) به طور متوسط).
    • ملاحظات: کلیدها فقط می‌توانند رشته یا Symbol باشند. ترتیب ویژگی‌ها تضمین شده نیست (اگرچه در نسخه‌های جدید تا حدودی قابل پیش‌بینی است).
  • Map:
    • کی استفاده کنیم:
      • زمانی که نیاز به جفت‌های کلید-مقدار دارید و کلیدها می‌توانند از هر نوع داده‌ای (اشیاء، توابع، اعداد و غیره) باشند.
      • زمانی که حفظ ترتیب درج کلیدها اهمیت دارد.
      • برای بهبود عملکرد در سناریوهایی با افزودن/حذف مکرر کلیدها.
      • به عنوان یک کش ساده یا برای memoization.
    • ملاحظات: مصرف حافظه کمی بیشتر از Object برای مجموعه داده‌های کوچک، اما برای مقادیر زیاد می‌تواند کارآمدتر باشد.
  • Set:
    • کی استفاده کنیم:
      • زمانی که نیاز به ذخیره‌سازی مجموعه‌ای از مقادیر منحصربه‌فرد دارید.
      • برای حذف عناصر تکراری از آرایه‌ها.
      • برای بررسی سریع وجود یک عنصر در مجموعه (O(1) به طور متوسط).
      • برای انجام عملیات مجموعه‌ای مانند اجتماع، اشتراک و تفاضل.
    • ملاحظات: عدم وجود ایندکس و دسترسی مستقیم به عنصر بر اساس موقعیت.
  • Symbol:
    • کی استفاده کنیم:
      • برای ایجاد کلیدهای ویژگی منحصربه‌فرد و جلوگیری از تداخل نام در اشیاء، به ویژه در کتابخانه‌ها و فریم‌ورک‌ها.
      • برای تعریف Symbolهای شناخته شده که رفتار داخلی اشیاء را تغییر می‌دهند (متا برنامه‌نویسی).
    • ملاحظات: نمی‌توان به طور مستقیم تکرار یا به رشته تبدیل کرد.
  • BigInt:
    • کی استفاده کنیم:
      • زمانی که نیاز به کار با اعداد صحیح بسیار بزرگتر از Number.MAX_SAFE_INTEGER (2^53 - 1) یا کوچکتر از Number.MIN_SAFE_INTEGER دارید.
      • در سناریوهایی مانند شناسه‌های پایگاه داده 64 بیتی، محاسبات رمزنگاری، یا سیستم‌های مالی که دقت مطلق عددی حیاتی است.
    • ملاحظات: نمی‌توان با نوع Number ترکیب کرد و نیاز به تبدیل صریح دارد.
  • WeakMap / WeakSet:
    • کی استفاده کنیم:
      • زمانی که نیاز به ذخیره‌سازی داده‌های مرتبط با اشیاء (مانند metadata خصوصی) دارید، اما نمی‌خواهید این ذخیره‌سازی مانع از جمع‌آوری شیء اصلی توسط Garbage Collector شود.
      • برای مدیریت کش‌هایی که نیاز به تخلیه خودکار دارند.
      • زمانی که نمی‌توانید از کلیدها/عناصر غیرشیئی استفاده کنید.
    • ملاحظات: قابلیت پیمایش (iteration) و دسترسی به .size را ندارند.
  • Typed Arrays (و ArrayBuffer/DataView):
    • کی استفاده کنیم:
      • زمانی که نیاز به کار با داده‌های باینری با کارایی بالا دارید.
      • در سناریوهایی مانند WebGL، پردازش صدا/تصویر، WebSockets، و تعامل با WebAssembly.
      • برای محاسبات عددی سنگین که نیاز به حافظه پیوسته و دسترسی مستقیم به بایت‌ها دارند.
    • ملاحظات: فقط برای انواع عددی. نیاز به درک Endianness و ساختار داده باینری.
  • Proxy و Reflect:
    • کی استفاده کنیم:
      • برای پیاده‌سازی الگوهای متا برنامه‌نویسی، مانند اعتبارسنجی خودکار، لاگ‌برداری، کنترل دسترسی، یا سیستم‌های واکنش‌گرا.
      • زمانی که نیاز به سفارشی‌سازی رفتار پیش‌فرض اشیاء دارید.
    • ملاحظات: استفاده نادرست می‌تواند کد را پیچیده کند. Overhead جزئی در عملکرد به دلیل رهگیری عملیات.

ملاحظات عملکردی (Performance Considerations)

عملکرد هر ساختار داده‌ای به طور کلی به پیاده‌سازی موتور جاوا اسکریپت (V8 در Chrome و Node.js، SpiderMonkey در Firefox و غیره) و الگوریتم‌های زیربنایی آن بستگی دارد. اما می‌توانیم اصول کلی را در نظر بگیریم:

  • پیچیدگی زمانی (Time Complexity - Big O Notation):
    • جستجو (Lookup):
      • Object و Map (بر اساس کلید): به طور متوسط O(1).
      • Set (با .has()): به طور متوسط O(1).
      • Array (با indexOf/includes): O(n) در بدترین حالت.
    • افزودن/حذف (Insertion/Deletion):
      • Object، Map، Set: به طور متوسط O(1).
      • Array (در انتها): O(1). (در ابتدا یا وسط): O(n) به دلیل نیاز به جابجایی عناصر.
    • پیمایش (Iteration):
      • Array، Map، Set: O(n).

    درک پیچیدگی زمانی به شما کمک می‌کند تا بهترین ساختار را برای عملیات غالب خود انتخاب کنید، به خصوص زمانی که با حجم زیادی از داده‌ها سروکار دارید.

  • مصرف حافظه (Memory Usage):
    • Objectها ممکن است برای تعداد کمی از ویژگی‌ها، سربار کمتری نسبت به Map داشته باشند.
    • Map و Set می‌توانند برای مجموعه داده‌های بزرگتر از Object و Array (که نیاز به بازسازی یا حذف تکراری دارند) کارآمدتر باشند.
    • Typed Arrays از حافظه فشرده‌تری استفاده می‌کنند زیرا بایت‌ها به طور پیوسته و با نوع مشخص ذخیره می‌شوند، که برای داده‌های عددی حجم بالا بسیار بهینه است.
    • WeakMap و WeakSet به طور خاص برای بهینه‌سازی حافظه و جلوگیری از نشت حافظه در سناریوهای مدیریت چرخه حیات شیء طراحی شده‌اند.
  • پرهیز از بهینه‌سازی زودهنگام (Premature Optimization):

    همیشه قبل از اینکه بهینه‌سازی‌های پیچیده انجام دهید، پروفایل‌سازی (profiling) کنید. گاهی اوقات، خوانایی و سادگی کد مهم‌تر از افزایش عملکرد جزئی است که شاید هرگز مورد نیاز نباشد. از ساختار داده‌ای استفاده کنید که منطقی‌ترین و خواناترین راه حل را ارائه می‌دهد، مگر اینکه پروفایل‌سازی نشان دهد که در آن بخش خاصی از کد گلوگاه عملکردی وجود دارد.

نتیجه‌گیری

جاوا اسکریپت از یک زبان ساده اسکریپت‌نویسی به یک اکوسیستم قدرتمند و چندوجهی تبدیل شده است. بخش عمده‌ای از این تحول به دلیل تکامل مستمر در زمینه مدیریت داده‌ها و معرفی انواع و ساختارهای داده‌ای پیشرفته است. درک عمیق Symbol، BigInt، Map، Set، WeakMap، WeakSet، Typed Arrays، ArrayBuffer، Proxy و Reflect، شما را به ابزارهایی مجهز می‌کند که می‌توانید با آنها چالش‌های پیچیده برنامه‌نویسی را حل کنید، کدهای بهینه‌تر و مقیاس‌پذیرتر بنویسید، و از آخرین قابلیت‌های زبان جاوا اسکریپت بهره‌مند شوید. با تسلط بر این مفاهیم، نه تنها یک برنامه‌نویس جاوا اسکریپت ماهرتر خواهید بود، بلکه قادر خواهید بود راه‌حل‌های نوآورانه‌ای برای مسائل دنیای واقعی ارائه دهید. همیشه به یاد داشته باشید که انتخاب درست ساختار داده، ستون فقرات یک نرم‌افزار کارآمد و قابل نگهداری است.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان