واجهات Proxy و Reflect
واجهات Proxy و Reflect هي ميزات ES6 قوية تسمح لك باعتراض وتخصيص العمليات الأساسية على الكائنات. تُمكّن Proxies من البرمجة التعريفية (meta-programming)، والتحقق من الصحة، والتسجيل، والمزيد. دعنا نستكشف كيفية استخدام هذه الميزات المتقدمة.
ما هو Proxy؟
يُغلّف Proxy كائناً ويعترض العمليات المُنفذة عليه. يمكنك تخصيص السلوك للعمليات الأساسية مثل الوصول إلى الخصائص، والتعيين، والعد، واستدعاء الدوال، والمزيد.
مفهوم أساسي: يعمل Proxy كوسيط بينك وبين الكائن الهدف، مما يسمح لك باعتراض وإعادة تعريف العمليات.
إنشاء Proxy أساسي
أنشئ proxy بكائن هدف ومُعالج يحتوي على فخاخ (traps):
// كائن الهدف
const target = {
name: "John",
age: 30
};
// المُعالج مع الفخاخ
const handler = {
// اعترض الوصول إلى الخاصية (فخ get)
get(target, property) {
console.log(`Getting property: ${property}`);
return target[property];
},
// اعترض تعيين الخاصية (فخ set)
set(target, property, value) {
console.log(`Setting property: ${property} = ${value}`);
target[property] = value;
return true; // يشير إلى النجاح
}
};
// أنشئ الـ proxy
const proxy = new Proxy(target, handler);
// استخدم الـ proxy
console.log(proxy.name); // يسجل: "Getting property: name"، ثم "John"
proxy.age = 31; // يسجل: "Setting property: age = 31"
console.log(proxy.age); // يسجل: "Getting property: age"، ثم 31
فخاخ Proxy الشائعة
تدعم Proxies العديد من الفخاخ لعمليات مختلفة:
الفخاخ الشائعة:
get(target, property, receiver):
- يعترض الوصول إلى الخاصية
- مثال: obj.prop أو obj["prop"]
set(target, property, value, receiver):
- يعترض تعيين الخاصية
- مثال: obj.prop = value
has(target, property):
- يعترض معامل "in"
- مثال: "prop" in obj
deleteProperty(target, property):
- يعترض حذف الخاصية
- مثال: delete obj.prop
apply(target, thisArg, argumentsList):
- يعترض استدعاءات الدوال
- مثال: func(...args)
construct(target, argumentsList, newTarget):
- يعترض معامل "new"
- مثال: new Func(...args)
التحقق من الصحة باستخدام Proxies
استخدم proxies للتحقق من البيانات قبل تعيين الخصائص:
const validator = {
set(target, property, value) {
if (property === "age") {
if (typeof value !== "number") {
throw new TypeError("Age must be a number");
}
if (value < 0 || value > 150) {
throw new RangeError("Age must be between 0 and 150");
}
}
if (property === "email") {
if (!value.includes("@")) {
throw new Error("Invalid email format");
}
}
target[property] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 30; // يعمل
console.log(person.age); // 30
person.email = "john@example.com"; // يعمل
console.log(person.email); // "john@example.com"
// هذه سترمي أخطاء:
// person.age = "thirty"; // TypeError: Age must be a number
// person.age = -5; // RangeError: Age must be between 0 and 150
// person.email = "invalid"; // Error: Invalid email format
حالة استخدام: Proxies ممتازة للتحقق من الأنواع في وقت التشغيل والتحقق من صحة البيانات دون ازدحام الكود بالفحوصات اليدوية.
القيم الافتراضية باستخدام Proxies
أرجع قيماً افتراضية للخصائص غير الموجودة:
const withDefaults = (target, defaultValue) => {
return new Proxy(target, {
get(target, property) {
if (property in target) {
return target[property];
}
return defaultValue;
}
});
};
const config = withDefaults({
port: 3000,
host: "localhost"
}, "Not configured");
console.log(config.port); // 3000
console.log(config.host); // "localhost"
console.log(config.database); // "Not configured"
console.log(config.anything); // "Not configured"
التسجيل وتصحيح الأخطاء باستخدام Proxies
تتبع جميع العمليات على كائن لتصحيح الأخطاء:
const createLogger = (target, name) => {
return new Proxy(target, {
get(target, property) {
console.log(`[${name}] GET: ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`[${name}] SET: ${property} = ${JSON.stringify(value)}`);
target[property] = value;
return true;
},
deleteProperty(target, property) {
console.log(`[${name}] DELETE: ${property}`);
delete target[property];
return true;
}
});
};
const user = createLogger({
name: "John",
age: 30
}, "User");
user.name; // [User] GET: name
user.age = 31; // [User] SET: age = 31
user.email = "john@example.com"; // [User] SET: email = "john@example.com"
delete user.age; // [User] DELETE: age
الخصائص الافتراضية باستخدام Proxies
أنشئ خصائص محسوبة لا توجد فعلياً على الكائن:
const createVirtualProps = (target) => {
return new Proxy(target, {
get(target, property) {
// الخصائص الحقيقية
if (property in target) {
return target[property];
}
// خاصية افتراضية: fullName
if (property === "fullName") {
return `${target.firstName} ${target.lastName}`;
}
// خاصية افتراضية: initials
if (property === "initials") {
return `${target.firstName[0]}.${target.lastName[0]}.`;
}
// خاصية افتراضية: age (محسوبة من birthYear)
if (property === "age") {
const currentYear = new Date().getFullYear();
return currentYear - target.birthYear;
}
return undefined;
}
});
};
const person = createVirtualProps({
firstName: "John",
lastName: "Doe",
birthYear: 1990
});
console.log(person.firstName); // "John"
console.log(person.fullName); // "John Doe" (افتراضية)
console.log(person.initials); // "J.D." (افتراضية)
console.log(person.age); // 36 (افتراضية، محسوبة)
مُعترضات الدوال باستخدام Proxies
اعترض استدعاءات الدوال وعدّل السلوك:
const createTrackedFunction = (func, name) => {
let callCount = 0;
return new Proxy(func, {
apply(target, thisArg, args) {
callCount++;
console.log(`[${name}] Call #${callCount} with args:`, args);
const startTime = Date.now();
const result = target.apply(thisArg, args);
const duration = Date.now() - startTime;
console.log(`[${name}] Completed in ${duration}ms, result:`, result);
return result;
}
});
};
const add = (a, b) => a + b;
const trackedAdd = createTrackedFunction(add, "add");
trackedAdd(5, 3);
// [add] Call #1 with args: [5, 3]
// [add] Completed in 0ms, result: 8
trackedAdd(10, 20);
// [add] Call #2 with args: [10, 20]
// [add] Completed in 0ms, result: 30
واجهة Reflect
توفر Reflect دوالاً لعمليات JavaScript القابلة للاعتراض. غالباً ما تُستخدم مع Proxies:
// دوال Reflect تعكس فخاخ Proxy
const target = {
name: "John",
age: 30
};
// Reflect.get() - احصل على قيمة الخاصية
console.log(Reflect.get(target, "name")); // "John"
// Reflect.set() - عيّن قيمة الخاصية
Reflect.set(target, "age", 31);
console.log(target.age); // 31
// Reflect.has() - تحقق مما إذا كانت الخاصية موجودة
console.log(Reflect.has(target, "name")); // true
console.log(Reflect.has(target, "email")); // false
// Reflect.deleteProperty() - احذف الخاصية
Reflect.deleteProperty(target, "age");
console.log(target.age); // undefined
// Reflect.ownKeys() - احصل على جميع المفاتيح
const obj = { a: 1, b: 2 };
console.log(Reflect.ownKeys(obj)); // ["a", "b"]
// Reflect.apply() - استدعِ الدالة
function greet(greeting) {
return `${greeting}, ${this.name}!`;
}
console.log(Reflect.apply(greet, { name: "John" }, ["Hello"])); // "Hello, John!"
لماذا Reflect؟ توفر Reflect واجهة برمجة أنظف وأكثر اتساقاً لعمليات البرمجة التعريفية. تُرجع النجاح/الفشل كـ boolean بدلاً من رمي أخطاء.
استخدام Reflect في فخاخ Proxy
تُستخدم Reflect عادةً داخل فخاخ proxy لإعادة توجيه العمليات:
const handler = {
get(target, property, receiver) {
console.log(`Accessing property: ${property}`);
// استخدم Reflect لإعادة توجيه العملية
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Setting ${property} to ${value}`);
// استخدم Reflect لإعادة توجيه العملية
return Reflect.set(target, property, value, receiver);
},
has(target, property) {
console.log(`Checking if ${property} exists`);
return Reflect.has(target, property);
}
};
const obj = new Proxy({ name: "John", age: 30 }, handler);
console.log(obj.name); // Accessing property: name، ثم "John"
obj.age = 31; // Setting age to 31
console.log("name" in obj); // Checking if name exists، ثم true
فهارس المصفوفة السالبة باستخدام Proxies
نفّذ الفهرسة السالبة بأسلوب Python للمصفوفات:
const createNegativeArray = (array) => {
return new Proxy(array, {
get(target, property) {
const index = Number(property);
// تعامل مع الفهارس السالبة
if (index < 0) {
return target[target.length + index];
}
return Reflect.get(target, property);
}
});
};
const arr = createNegativeArray(["a", "b", "c", "d", "e"]);
console.log(arr[0]); // "a"
console.log(arr[-1]); // "e" (العنصر الأخير)
console.log(arr[-2]); // "d" (قبل الأخير)
console.log(arr[-5]); // "a" (العنصر الأول عبر الفهرس السالب)
كائنات للقراءة فقط باستخدام Proxies
أنشئ كائنات غير قابلة للتغيير حقاً:
const createReadOnly = (target) => {
return new Proxy(target, {
set(target, property, value) {
throw new Error(`Cannot modify read-only property: ${property}`);
},
deleteProperty(target, property) {
throw new Error(`Cannot delete read-only property: ${property}`);
},
defineProperty(target, property, descriptor) {
throw new Error(`Cannot define property on read-only object: ${property}`);
}
});
};
const config = createReadOnly({
apiUrl: "https://api.example.com",
timeout: 5000
});
console.log(config.apiUrl); // "https://api.example.com"
// كل هذه سترمي أخطاء:
// config.apiUrl = "new-url"; // Error: Cannot modify read-only property: apiUrl
// delete config.timeout; // Error: Cannot delete read-only property: timeout
// config.newProp = "value"; // Error: Cannot modify read-only property: newProp
نمط Observable باستخدام Proxies
نفّذ أنماط البرمجة التفاعلية:
const createObservable = (target, callback) => {
return new Proxy(target, {
set(target, property, value) {
const oldValue = target[property];
target[property] = value;
// أخبر المراقبين بالتغيير
callback(property, oldValue, value);
return true;
}
});
};
const state = createObservable(
{ count: 0, name: "App" },
(property, oldValue, newValue) => {
console.log(`Property "${property}" changed from ${oldValue} to ${newValue}`);
}
);
state.count = 1;
// Property "count" changed from 0 to 1
state.count = 2;
// Property "count" changed from 1 to 2
state.name = "MyApp";
// Property "name" changed from App to MyApp
استخدام واقعي: هذا النمط هو أساس أطر العمل التفاعلية مثل Vue.js، التي تستخدم Proxies لربط البيانات التفاعلية.
تمرين تطبيقي:
التحدي: أنشئ نظام ذاكرة تخزين مؤقت ذكي باستخدام Proxy بالميزات التالية:
- تتبع نجاحات وإخفاقات الذاكرة المؤقتة
- حساب وتخزين العمليات المكلفة تلقائياً
- توفير إحصائيات الذاكرة المؤقتة (النجاحات، الإخفاقات، معدل النجاح)
- السماح بمسح الذاكرة المؤقتة
الحل:
const createSmartCache = (computeFunction) => {
const cache = new Map();
let hits = 0;
let misses = 0;
const handler = {
get(target, property) {
// دوال خاصة
if (property === "stats") {
return () => ({
hits,
misses,
total: hits + misses,
hitRate: hits / (hits + misses) || 0,
cacheSize: cache.size
});
}
if (property === "clear") {
return () => {
cache.clear();
hits = 0;
misses = 0;
};
}
// تحقق من الذاكرة المؤقتة
if (cache.has(property)) {
hits++;
console.log(`Cache HIT for: ${property}`);
return cache.get(property);
}
// إخفاق في الذاكرة المؤقتة - احسب القيمة
misses++;
console.log(`Cache MISS for: ${property}`);
const value = computeFunction(property);
cache.set(property, value);
return value;
}
};
return new Proxy({}, handler);
};
// مثال: حساب fibonacci المكلف
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const smartFib = createSmartCache((n) => fibonacci(Number(n)));
console.log(smartFib[10]); // Cache MISS for: 10، يحسب القيمة
console.log(smartFib[10]); // Cache HIT for: 10، يُرجع المخزّن
console.log(smartFib[5]); // Cache MISS for: 5، يحسب القيمة
console.log(smartFib[10]); // Cache HIT for: 10، يُرجع المخزّن
console.log(smartFib.stats());
// { hits: 2, misses: 2, total: 4, hitRate: 0.5, cacheSize: 2 }
smartFib.clear();
console.log(smartFib.stats());
// { hits: 0, misses: 0, total: 0, hitRate: 0, cacheSize: 0 }
اعتبارات الأداء
تُضيف Proxies حملاً زائداً على العمليات. استخدمها بحكمة:
// Proxies لها تكلفة أداء
const plainObject = { x: 1, y: 2 };
const proxiedObject = new Proxy({ x: 1, y: 2 }, {
get(target, property) {
return target[property];
}
});
// قياس الأداء
console.time("Plain object");
for (let i = 0; i < 1000000; i++) {
plainObject.x;
}
console.timeEnd("Plain object"); // ~3ms
console.time("Proxied object");
for (let i = 0; i < 1000000; i++) {
proxiedObject.x;
}
console.timeEnd("Proxied object"); // ~20ms (أبطأ)
نصيحة أداء: Proxies قوية لكنها تُضيف حملاً زائداً. استخدمها لأدوات المطورين، والتحقق من الصحة، والسيناريوهات المعقدة - وليس للمسارات الساخنة في الكود الحرج للأداء.
الملخص
في هذا الدرس، تعلمت:
- Proxies تعترض وتخصص عمليات الكائنات
- الفخاخ الشائعة: get, set, has, deleteProperty, apply, construct
- حالات الاستخدام: التحقق من الصحة، التسجيل، الخصائص الافتراضية، المراقبات
- واجهة Reflect توفر دوالاً لإعادة توجيه العمليات
- Proxies تُمكّن البرمجة التعريفية والأنماط التفاعلية
- Proxies لها حمل زائد في الأداء - استخدمها بشكل مناسب
- التطبيقات الواقعية في الأطر والأدوات
تهانينا! لقد أكملت الوحدة 5: JavaScript الكائنية! أنت الآن تفهم فئات ES6، والوراثة، والنماذج الأولية، ودوال الكائنات، وأنماط proxy المتقدمة. في الوحدة التالية، سنستكشف وحدات ES6 وتنظيم الكود!