أساسيات JavaScript

الرموز والرموز المعروفة

45 دقيقة الدرس 37 من 60

مقدمة إلى الرموز

الرموز (Symbols) هي نوع بيانات بدائي تم تقديمه في ES6 (ECMAScript 2015). إنها معرفات فريدة وغير قابلة للتغيير تخدم غرضا مختلفا جذريا عن السلاسل النصية أو الأرقام. كل قيمة Symbol مضمونة أن تكون فريدة مما يجعل الرموز مثالية لإنشاء مفاتيح خصائص لن تتعارض أبدا مع مفاتيح خصائص أخرى. على عكس السلاسل النصية فإن رمزين تم إنشاؤهما بنفس الوصف لا يكونان متساويين أبدا. ضمان الفرادة هذا هو ما يميز الرموز عن كل نوع بدائي آخر في JavaScript.

قبل وجود الرموز كان على المطورين الاعتماد على أسماء خصائص نصية يمكن أن تتعارض بسهولة عند دمج الكود من مكتبات أو فرق مختلفة. تحل الرموز هذه المشكلة من خلال توفير معرفات فريدة حقا لا يمكن أن تتصادم. تستخدم مواصفات JavaScript نفسها الرموز بكثافة لتحديد السلوكيات الداخلية للكائنات وفهمها بعمق سيساعدك على كتابة كود أكثر قوة وخال من التعارضات.

إنشاء الرموز باستخدام Symbol()

تنشئ رمزا عن طريق استدعاء الدالة Symbol(). يمكنك اختياريا تمرير سلسلة وصف لكن هذا الوصف هو لأغراض التصحيح فقط -- ولا يؤثر على فرادة الرمز.

مثال: إنشاء الرموز

// إنشاء رمز بدون وصف
const sym1 = Symbol();

// إنشاء رمز مع وصف
const sym2 = Symbol('user_id');
const sym3 = Symbol('user_id');

console.log(typeof sym1);      // "symbol"
console.log(sym2.toString());  // "Symbol(user_id)"
console.log(sym2.description); // "user_id"

// كل رمز فريد حتى مع نفس الوصف
console.log(sym2 === sym3);    // false
console.log(sym2 == sym3);     // false
خطأ شائع: لا تستخدم أبدا الكلمة المفتاحية new مع Symbol(). الرموز هي قيم بدائية وليست كائنات. كتابة new Symbol() سترمي خطأ TypeError. استدع دائما Symbol() كدالة عادية.

الوصف الذي تمرره إلى Symbol() يمكن الوصول إليه عبر خاصية .description التي تم تقديمها في ES2019. قبل ذلك كان عليك استخدام .toString() وتحليل المخرجات. الوصف مفيد للغاية أثناء التصحيح لأن رؤية Symbol(user_id) في وحدة التحكم أكثر إفادة بكثير من رؤية Symbol().

مثال: أوصاف الرموز لتسهيل التصحيح

const STATUS_ACTIVE = Symbol('STATUS_ACTIVE');
const STATUS_INACTIVE = Symbol('STATUS_INACTIVE');
const STATUS_PENDING = Symbol('STATUS_PENDING');

console.log(STATUS_ACTIVE.description);   // "STATUS_ACTIVE"
console.log(STATUS_INACTIVE.description); // "STATUS_INACTIVE"
console.log(STATUS_PENDING.description);  // "STATUS_PENDING"

// مفيد في الفحوصات الشرطية
function getStatusLabel(status) {
    switch (status) {
        case STATUS_ACTIVE: return 'نشط';
        case STATUS_INACTIVE: return 'غير نشط';
        case STATUS_PENDING: return 'قيد الانتظار';
        default: return 'غير معروف';
    }
}

console.log(getStatusLabel(STATUS_ACTIVE)); // "نشط"

سجل الرموز العالمي: Symbol.for() و Symbol.keyFor()

بينما Symbol() ينشئ دائما رمزا فريدا، أحيانا تحتاج لمشاركة رمز عبر أجزاء مختلفة من تطبيقك أو حتى عبر وحدات وإطارات مختلفة. يوفر سجل الرموز العالمي طريقة لإنشاء واسترجاع رموز مشتركة باستخدام Symbol.for().

عند استدعاء Symbol.for('key') يفحص JavaScript أولا السجل العالمي بحثا عن رمز بهذا المفتاح. إذا وجد واحدا يعيده. وإذا لم يجد ينشئ رمزا جديدا ويضيفه إلى السجل ثم يعيده. هذا يعني أن Symbol.for() بنفس المفتاح يعيد دائما نفس الرمز بغض النظر عن مكان استدعائه في الكود.

مثال: Symbol.for() والسجل العالمي

// Symbol.for() ينشئ أو يسترجع من السجل العالمي
const globalSym1 = Symbol.for('app.config');
const globalSym2 = Symbol.for('app.config');

// نفس المفتاح يعيد نفس الرمز
console.log(globalSym1 === globalSym2); // true

// قارن مع Symbol() العادي
const localSym = Symbol('app.config');
console.log(globalSym1 === localSym);   // false

// Symbol.keyFor() يسترجع المفتاح لرمز عالمي
console.log(Symbol.keyFor(globalSym1)); // "app.config"
console.log(Symbol.keyFor(localSym));   // undefined (ليس في السجل العالمي)
ملاحظة: سجل الرموز العالمي مشترك عبر جميع البيئات في محرك JavaScript بما في ذلك الإطارات المضمنة (iframes) والعاملين في الخلفية (web workers). هذا يجعل Symbol.for() الخيار المثالي عندما تحتاج رمزا يمكن التعرف عليه عبر سياقات تنفيذ مختلفة بينما Symbol() أفضل لإنشاء معرفات خاصة ومحلية.

نمط شائع هو استخدام مفاتيح مفصولة بنقاط مع Symbol.for() لتجنب التصادمات في السجل العالمي مشابه لكيفية استخدام Java لتدوين النطاق العكسي لأسماء الحزم. على سبيل المثال Symbol.for('mylib.internal.state') أقل احتمالا للتصادم بكثير من Symbol.for('state').

مثال: تسمية الرموز العالمية بفضاءات الأسماء

// المكتبة A تستخدم رموزا بفضاءات أسماء
const LIB_A_ID = Symbol.for('libraryA.entityId');
const LIB_A_TYPE = Symbol.for('libraryA.entityType');

// المكتبة B تستخدم فضاء أسمائها الخاص
const LIB_B_ID = Symbol.for('libraryB.entityId');
const LIB_B_TYPE = Symbol.for('libraryB.entityType');

// لا تصادمات حتى لو كلاهما يستخدم "entityId"
console.log(LIB_A_ID === LIB_B_ID); // false

const entity = {};
entity[LIB_A_ID] = 'a-001';
entity[LIB_B_ID] = 'b-999';

console.log(entity[LIB_A_ID]); // "a-001"
console.log(entity[LIB_B_ID]); // "b-999"

الرموز كمفاتيح خصائص الكائنات

أحد أقوى استخدامات الرموز هو كمفاتيح خصائص على الكائنات. خصائص مفاتيح الرموز لا تظهر في حلقات for...in أو Object.keys() أو Object.getOwnPropertyNames(). هذا يجعلها ممتازة لإرفاق بيانات وصفية أو حالة داخلية بالكائنات دون التدخل في واجهة الكائن العامة.

مثال: الخصائص بمفاتيح رمزية

const id = Symbol('id');
const secret = Symbol('secret');

const user = {
    name: 'Alice',
    email: 'alice@example.com',
    [id]: 12345,
    [secret]: 's3cr3t_token'
};

// الوصول إلى خصائص الرموز
console.log(user[id]);     // 12345
console.log(user[secret]); // "s3cr3t_token"

// خصائص الرموز مخفية عن طرق التعداد الشائعة
console.log(Object.keys(user));                // ["name", "email"]
console.log(Object.getOwnPropertyNames(user)); // ["name", "email"]
console.log(JSON.stringify(user));              // '{"name":"Alice","email":"alice@example.com"}'

// حلقة for...in تتخطى خصائص الرموز
for (const key in user) {
    console.log(key); // "name", "email" -- لا مفاتيح رمزية
}

// للوصول إلى خصائص الرموز استخدم:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(secret)]

// Reflect.ownKeys() يعيد جميع المفاتيح بما فيها الرموز
console.log(Reflect.ownKeys(user)); // ["name", "email", Symbol(id), Symbol(secret)]
نصيحة احترافية: استخدم Object.getOwnPropertySymbols() لاسترجاع جميع الخصائص بمفاتيح رمزية على كائن. استخدم Reflect.ownKeys() للحصول على المفاتيح النصية والرمزية معا. هذه هي الطرق القياسية الوحيدة لاكتشاف خصائص الرموز مما يجعلها شبه خاصة وليست خاصة حقا.

الرموز لإخفاء الخصائص والتغليف

لأن الخصائص بمفاتيح رمزية غير مرئية لمعظم أنماط التكرار الشائعة فإنها توفر شكلا خفيفا من التغليف. بينما ليست خاصة حقا (يمكنك الوصول إليها عبر Object.getOwnPropertySymbols()) فإنها تخفي تفاصيل التنفيذ بشكل فعال عن الكود الذي لا يبحث تحديدا عن الرموز.

مثال: استخدام الرموز للحالة الداخلية

// وحدة: user-validator.js
const _validationRules = Symbol('validationRules');
const _isValidated = Symbol('isValidated');

class UserValidator {
    constructor(rules) {
        this[_validationRules] = rules;
        this[_isValidated] = false;
    }

    validate(data) {
        const rules = this[_validationRules];
        let isValid = true;

        for (const [field, rule] of Object.entries(rules)) {
            if (!rule(data[field])) {
                isValid = false;
                break;
            }
        }

        this[_isValidated] = true;
        return isValid;
    }

    get validated() {
        return this[_isValidated];
    }
}

const validator = new UserValidator({
    name: (val) => typeof val === 'string' && val.length > 0,
    age: (val) => typeof val === 'number' && val >= 18
});

console.log(Object.keys(validator));              // []
console.log(validator.validate({ name: 'Bob', age: 25 })); // true
console.log(validator.validated);                  // true

الرموز المعروفة: تخصيص سلوك الكائنات

يعرف JavaScript مجموعة من الرموز المدمجة تسمى الرموز المعروفة (well-known Symbols). تستخدم هذه الرموز بواسطة محرك JavaScript للبحث عن سلوكيات محددة على كائناتك. من خلال تنفيذ هذه الطرق بمفاتيح رمزية يمكنك تخصيص كيفية تفاعل كائناتك مع بنيات اللغة مثل حلقات for...of وتحويل الأنواع وفحوصات instanceof والمزيد. الرموز المعروفة هي الآلية التي تربط كائناتك المخصصة بالسلوكيات الأساسية للغة.

Symbol.iterator -- جعل الكائنات قابلة للتكرار

Symbol.iterator يحدد المكرر الافتراضي للكائن. عندما تستخدم for...of أو عامل النشر أو التفكيك على كائن يستدعي JavaScript طريقة [Symbol.iterator]() للحصول على مكرر. من خلال تنفيذ هذه الطريقة يمكنك جعل أي كائن قابلا للتكرار.

مثال: كائن قابل للتكرار مع Symbol.iterator

class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        const step = this.step;

        return {
            next() {
                if (current <= end) {
                    const value = current;
                    current += step;
                    return { value, done: false };
                }
                return { done: true };
            }
        };
    }
}

const range = new Range(1, 10, 2);

// الاستخدام في for...of
for (const num of range) {
    console.log(num); // 1, 3, 5, 7, 9
}

// الاستخدام مع عامل النشر
console.log([...range]); // [1, 3, 5, 7, 9]

// الاستخدام مع التفكيك
const [first, second, third] = new Range(100, 500, 100);
console.log(first, second, third); // 100 200 300

Symbol.toPrimitive -- تحويل الأنواع المخصص

Symbol.toPrimitive يسمح لك بالتحكم في كيفية تحويل كائنك إلى قيمة بدائية. يتلقى معامل تلميح يشير إلى النوع المفضل: "number" أو "string" أو "default". يتم استدعاؤه كلما احتاج JavaScript لتحويل كائنك إلى قيمة بدائية مثل العمليات الحسابية أو دمج السلاسل النصية أو المقارنة بـ ==.

مثال: تحويل الأنواع المخصص مع Symbol.toPrimitive

class Currency {
    constructor(amount, code) {
        this.amount = amount;
        this.code = code;
    }

    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case 'number':
                return this.amount;
            case 'string':
                return `${this.amount} ${this.code}`;
            default:
                return this.amount;
        }
    }
}

const price = new Currency(29.99, 'USD');

// تلميح السلسلة النصية
console.log(`Price: ${price}`);    // "Price: 29.99 USD"
console.log(String(price));         // "29.99 USD"

// تلميح الرقم
console.log(+price);                // 29.99
console.log(price * 2);             // 59.98
console.log(Number(price));         // 29.99

// التلميح الافتراضي (يستخدم مع == و+)
console.log(price + 10);            // 39.99
console.log(price == 29.99);        // true

Symbol.toStringTag -- مخرجات toString() مخصصة

Symbol.toStringTag يحدد سلسلة نصية تستخدم عند استدعاء Object.prototype.toString() على كائنك. افتراضيا تظهر الكائنات المخصصة [object Object]. يمكنك تغيير هذا لعرض اسم نوع ذي معنى وهو مفيد بشكل خاص للتصحيح وفحص الأنواع.

مثال: علامة السلسلة النصية المخصصة

class Database {
    get [Symbol.toStringTag]() {
        return 'Database';
    }
}

class QueryBuilder {
    get [Symbol.toStringTag]() {
        return 'QueryBuilder';
    }
}

const db = new Database();
const qb = new QueryBuilder();

console.log(Object.prototype.toString.call(db)); // "[object Database]"
console.log(Object.prototype.toString.call(qb)); // "[object QueryBuilder]"

// قارن مع السلوك الافتراضي
console.log(Object.prototype.toString.call({}));  // "[object Object]"
console.log(Object.prototype.toString.call([]));  // "[object Array]"

Symbol.hasInstance -- سلوك instanceof مخصص

Symbol.hasInstance يسمح لك بتخصيص سلوك عامل instanceof. عندما تكتب obj instanceof MyClass يستدعي JavaScript الطريقة MyClass[Symbol.hasInstance](obj). بتعريف طريقة ثابتة بهذا الرمز يمكنك التحكم في ما يعتبر نسخة من صفك.

مثال: instanceof مخصص مع Symbol.hasInstance

class TypeValidator {
    static [Symbol.hasInstance](instance) {
        return (
            instance !== null &&
            typeof instance === 'object' &&
            typeof instance.validate === 'function' &&
            typeof instance.getErrors === 'function'
        );
    }
}

// أي كائن يحتوي validate() و getErrors() يجتاز الفحص
const formValidator = {
    validate() { return true; },
    getErrors() { return []; }
};

const plainObject = { name: 'test' };

console.log(formValidator instanceof TypeValidator); // true
console.log(plainObject instanceof TypeValidator);   // false

// يعمل مع نمط البطة -- فحص السلوك وليس الوراثة
class EmailValidator {
    validate() { return true; }
    getErrors() { return []; }
}

const ev = new EmailValidator();
console.log(ev instanceof TypeValidator); // true

Symbol.species -- التحكم في أنواع الكائنات المشتقة

Symbol.species يحدد أي مُنشئ يجب استخدامه عند إنشاء كائنات مشتقة. على سبيل المثال عندما تستدعي .map() أو .filter() على صف فرعي من Array يستخدم JavaScript Symbol.species لتحديد ما إذا كانت النتيجة يجب أن تكون نسخة من الصف الفرعي أو صف Array الأصلي.

مثال: Symbol.species في الأصناف الفرعية للمصفوفات

class TrackedArray extends Array {
    // إعادة Array عادي للعمليات المشتقة
    static get [Symbol.species]() {
        return Array;
    }

    track(label) {
        console.log(`[${label}] المصفوفة تحتوي ${this.length} عناصر`);
        return this;
    }
}

const tracked = new TrackedArray(1, 2, 3, 4, 5);
tracked.track('initial'); // "[initial] المصفوفة تحتوي 5 عناصر"

// .filter() يستخدم Symbol.species لتحديد نوع النتيجة
const filtered = tracked.filter(n => n > 2);

console.log(filtered instanceof TrackedArray); // false
console.log(filtered instanceof Array);        // true
console.log(filtered);                         // [3, 4, 5]

// بدون Symbol.species كانت filtered ستكون TrackedArray
// معه نحصل على Array عادي -- مفيد للأداء
// ولتجنب السلوك غير المتوقع في سلاسل الطرق

الرموز وتسلسل JSON

الخصائص بمفاتيح رمزية يتم تجاهلها تماما بواسطة JSON.stringify(). هذا السلوك مصمم عمدا ويجعل الرموز مثالية لإرفاق بيانات وصفية لا يجب تسلسلها. إذا كنت بحاجة لتضمين بيانات مبنية على الرموز في مخرجات JSON يجب معالجتها صراحة في طريقة toJSON() مخصصة أو دالة بديلة.

مثال: الرموز و JSON

const metadata = Symbol('metadata');
const internal = Symbol('internal');

const apiResponse = {
    id: 1,
    name: 'Product A',
    price: 49.99,
    [metadata]: { fetchedAt: Date.now(), cached: true },
    [internal]: { rawQuery: 'SELECT * FROM products WHERE id=1' }
};

// الرموز غير مرئية لـ JSON.stringify()
console.log(JSON.stringify(apiResponse));
// '{"id":1,"name":"Product A","price":49.99}'

// لتضمين البيانات الوصفية في التسلسل استخدم toJSON()
const serializableResponse = {
    ...apiResponse,
    toJSON() {
        return {
            id: this.id,
            name: this.name,
            price: this.price,
            _meta: {
                fetchedAt: this[metadata].fetchedAt,
                cached: this[metadata].cached
            }
        };
    }
};

console.log(JSON.stringify(serializableResponse));
// '{"id":1,"name":"Product A","price":49.99,"_meta":{"fetchedAt":...,"cached":true}}'
ملاحظة: الخصائص ذات القيم الرمزية يتم تجاهلها أيضا عند استخدام Object.assign() مع هدف يعدد فقط المفاتيح النصية. ومع ذلك فإن Object.assign() ينسخ الخصائص بمفاتيح رمزية عند معالجة الكائنات المصدر. عامل النشر {...obj} ينسخ أيضا الخصائص بمفاتيح رمزية. لذا الرموز مخفية من JSON و for...in لكنها تبقى عند نشر الكائنات وتعيينها.

الرموز في المكتبات وأطر العمل الحقيقية

تستخدم العديد من مكتبات وأطر عمل JavaScript الشائعة الرموز داخليا. فهم هذه الأنماط سيساعدك على تقدير سبب وجود الرموز وكيف تحل مشاكل معمارية حقيقية.

مثال: إنشاء نظام إضافات باستخدام الرموز

// نواة إطار العمل تستخدم رموزا لخطافات دورة الحياة
const HOOKS = {
    onInit: Symbol('plugin.onInit'),
    onDestroy: Symbol('plugin.onDestroy'),
    onUpdate: Symbol('plugin.onUpdate'),
    version: Symbol('plugin.version')
};

class PluginManager {
    constructor() {
        this.plugins = [];
    }

    register(plugin) {
        if (typeof plugin[HOOKS.onInit] !== 'function') {
            throw new Error('يجب على الإضافة تنفيذ خطاف onInit');
        }
        this.plugins.push(plugin);
        plugin[HOOKS.onInit]();
    }

    updateAll(data) {
        for (const plugin of this.plugins) {
            if (typeof plugin[HOOKS.onUpdate] === 'function') {
                plugin[HOOKS.onUpdate](data);
            }
        }
    }

    destroyAll() {
        for (const plugin of this.plugins) {
            if (typeof plugin[HOOKS.onDestroy] === 'function') {
                plugin[HOOKS.onDestroy]();
            }
        }
        this.plugins = [];
    }
}

// تنفيذ الإضافة
const loggingPlugin = {
    name: 'Logger',
    [HOOKS.version]: '1.0.0',
    [HOOKS.onInit]() {
        console.log('تم تهيئة إضافة التسجيل');
    },
    [HOOKS.onUpdate](data) {
        console.log('تم استلام تحديث:', data);
    },
    [HOOKS.onDestroy]() {
        console.log('تم تدمير إضافة التسجيل');
    }
};

const manager = new PluginManager();
manager.register(loggingPlugin);  // "تم تهيئة إضافة التسجيل"
manager.updateAll({ event: 'click' }); // "تم استلام تحديث: { event: 'click' }"

// خطافات الإضافة غير مرئية للكود الخارجي
console.log(Object.keys(loggingPlugin)); // ["name"]

مثال: استخدام الرموز لقيم التعداد الفريدة

// الرموز كقيم تعداد تمنع المقارنة العرضية مع السلاسل النصية
const Direction = Object.freeze({
    UP: Symbol('Direction.UP'),
    DOWN: Symbol('Direction.DOWN'),
    LEFT: Symbol('Direction.LEFT'),
    RIGHT: Symbol('Direction.RIGHT')
});

function move(direction) {
    switch (direction) {
        case Direction.UP:
            return { x: 0, y: -1 };
        case Direction.DOWN:
            return { x: 0, y: 1 };
        case Direction.LEFT:
            return { x: -1, y: 0 };
        case Direction.RIGHT:
            return { x: 1, y: 0 };
        default:
            throw new Error(`اتجاه غير صالح: ${String(direction)}`);
    }
}

console.log(move(Direction.UP));    // { x: 0, y: -1 }
console.log(move(Direction.RIGHT)); // { x: 1, y: 0 }

// لا يمكن تمرير سلسلة نصية عرضيا بدلا منه
// move('UP') سترمي خطأ -- لا خطر تصادم

مثال: الرموز لتنفيذ بروتوكول خاص

// تعريف بروتوكول تسلسل باستخدام الرموز
const Serializable = {
    serialize: Symbol('Serializable.serialize'),
    deserialize: Symbol('Serializable.deserialize')
};

class User {
    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.password = password;
    }

    // تنفيذ بروتوكول التسلسل
    [Serializable.serialize]() {
        // استبعاد كلمة المرور عمدا من التسلسل
        return JSON.stringify({
            name: this.name,
            email: this.email
        });
    }

    static [Serializable.deserialize](data) {
        const parsed = JSON.parse(data);
        return new User(parsed.name, parsed.email, '');
    }
}

// دالة تسلسل عامة تعمل مع أي كائن Serializable
function serialize(obj) {
    if (typeof obj[Serializable.serialize] === 'function') {
        return obj[Serializable.serialize]();
    }
    throw new Error('الكائن لا ينفذ بروتوكول Serializable');
}

const user = new User('Alice', 'alice@example.com', 'super_secret');
const serialized = serialize(user);
console.log(serialized); // '{"name":"Alice","email":"alice@example.com"}' -- كلمة المرور مستبعدة

const restored = User[Serializable.deserialize](serialized);
console.log(restored.name);     // "Alice"
console.log(restored.password); // "" -- لم يتم تسلسل كلمة المرور

رموز معروفة إضافية

بالإضافة إلى الرموز المعروفة الرئيسية المشروحة أعلاه يتضمن JavaScript عدة رموز أخرى تتحكم في سلوكيات محددة. إليك ملخص لأكثرها فائدة:

  • Symbol.asyncIterator -- يحدد المكرر غير المتزامن الافتراضي المستخدم بواسطة حلقات for await...of. نفذ هذا لإنشاء كائنات قابلة للتكرار غير المتزامن.
  • Symbol.isConcatSpreadable -- قيمة منطقية تتحكم في ما إذا كان Array.prototype.concat() يفرد الكائن. اضبطها على false لمنع الإفراد أو true على الكائنات غير المصفوفات لتمكينه.
  • Symbol.match وSymbol.replace وSymbol.search وSymbol.split -- تسمح هذه الرموز للكائنات بأن تستخدم كمعاملات لطرق String المقابلة (.match() و.replace() و.search() و.split()).
  • Symbol.unscopables -- كائن تستبعد أسماء خصائصه من ربطات عبارة with. يستخدم داخليا بواسطة Array.prototype لمنع بعض الطرق الأحدث من كسر كود with القديم.

مثال: Symbol.asyncIterator للتكرار غير المتزامن

class AsyncRange {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    async *[Symbol.asyncIterator]() {
        for (let i = this.start; i <= this.end; i++) {
            // محاكاة جلب بيانات غير متزامن
            await new Promise(resolve => setTimeout(resolve, 100));
            yield i;
        }
    }
}

// الاستخدام مع for await...of
async function main() {
    const range = new AsyncRange(1, 5);
    for await (const num of range) {
        console.log(num); // 1, 2, 3, 4, 5 (كل واحد بعد 100 مللي ثانية)
    }
}

main();
نصيحة احترافية: عند بناء مكتبات أو أطر عمل استخدم الرموز لطرق البروتوكول الداخلية بدلا من الطرق المسماة بالسلاسل النصية. هذا يمنع مستخدمي مكتبتك من الكتابة فوق السلوك الداخلي عرضيا. كما يجعل واجهتك الداخلية غير مرئية لـ Object.keys() وحلقات for...in مما يبقي الواجهة العامة نظيفة ويمكن التنبؤ بها.

ملخص المفاهيم الرئيسية

  • Symbol() ينشئ قيمة بدائية فريدة وغير قابلة للتغيير. كل استدعاء ينتج رمزا جديدا ومميزا.
  • Symbol.for() يستخدم السجل العالمي لإنشاء أو استرجاع رموز مشتركة بمفتاح نصي.
  • Symbol.keyFor() يعيد مفتاح السجل لرمز مسجل عالميا.
  • الخصائص بمفاتيح رمزية مخفية من for...in وObject.keys() وJSON.stringify() لكنها متاحة عبر Object.getOwnPropertySymbols() وReflect.ownKeys().
  • الرموز المعروفة مثل Symbol.iterator وSymbol.toPrimitive وSymbol.toStringTag وSymbol.hasInstance وSymbol.species تسمح لك بتخصيص كيفية تفاعل الكائنات مع العوامل المدمجة في JavaScript وبنيات اللغة.
  • الاستخدام الواقعي يشمل التعدادات وأنظمة الإضافات وبروتوكولات التسلسل والمكونات الداخلية لأطر العمل.

تمرين عملي

أنشئ صفا Money يمثل قيمة مالية مع رمز العملة. نفذ Symbol.toPrimitive بحيث يعيد المبلغ الرقمي عند الاستخدام في العمليات الحسابية وسلسلة نصية منسقة مثل "150.00 EUR" عند الاستخدام في سياق النص. أضف Symbol.iterator ينتج عملات وحدة فردية يكون مجموعها المبلغ الإجمالي (على سبيل المثال new Money(3.50, 'USD') ستنتج 1 و1 و1 و0.25 و0.25). أنشئ محدد Symbol.toStringTag يعيد "Money". ثم أنشئ صفا Wallet يحمل عدة كائنات Money ويستخدم Symbol.hasInstance بحيث يعتبر أي كائن لديه خاصية balance وطريقة add() نسخة منه. أخيرا حول كائن Money إلى JSON باستخدام طريقة toJSON() مخصصة تتضمن المبلغ والعملة لكن تستبعد أي خصائص داخلية بمفاتيح رمزية. اختبر جميع سلوكيات الرموز المعروفة الخمسة في برنامج وتحقق من أن المخرجات تطابق توقعاتك.