دليل شامل للمطورين العالميين لإتقان واجهة برمجة تطبيقات Proxy في JavaScript. تعلم اعتراض وتخصيص عمليات الكائنات بأمثلة عملية وحالات استخدام ونصائح أداء.
واجهة برمجة تطبيقات Proxy في JavaScript: نظرة عميقة على تعديل سلوك الكائنات
في المشهد المتطور لـ JavaScript الحديثة، يبحث المطورون باستمرار عن طرق أكثر قوة وأناقة لإدارة البيانات والتفاعل معها. بينما أحدثت ميزات مثل الأصناف (classes) والوحدات (modules) و async/await ثورة في كيفية كتابتنا للشيفرة البرمجية، هناك ميزة قوية للبرمجة الوصفية (metaprogramming) تم تقديمها في ECMAScript 2015 (ES6) والتي غالبًا ما تظل غير مستغلة بالكامل: واجهة برمجة تطبيقات Proxy.
قد تبدو البرمجة الوصفية مصطلحًا مخيفًا، لكنها ببساطة مفهوم كتابة شيفرة برمجية تعمل على شيفرة برمجية أخرى. واجهة برمجة تطبيقات Proxy هي أداة JavaScript الأساسية لهذا الغرض، مما يسمح لك بإنشاء 'بروكسي' لكائن آخر، والذي يمكنه اعتراض وإعادة تعريف العمليات الأساسية لذلك الكائن. الأمر أشبه بوضع حارس بوابة قابل للتخصيص أمام كائن ما، مما يمنحك تحكمًا كاملاً في كيفية الوصول إليه وتعديله.
سيزيل هذا الدليل الشامل الغموض عن واجهة برمجة تطبيقات Proxy. سنستكشف مفاهيمها الأساسية، ونحلل قدراتها المختلفة بأمثلة عملية، ونناقش حالات الاستخدام المتقدمة واعتبارات الأداء. بنهاية هذا الدليل، ستفهم لماذا تُعتبر كائنات Proxy حجر الزاوية في الأطر الحديثة وكيف يمكنك الاستفادة منها لكتابة شيفرة برمجية أنظف وأكثر قوة وقابلية للصيانة.
فهم المفاهيم الأساسية: الهدف (Target)، المعالج (Handler)، والفخاخ (Traps)
تعتمد واجهة برمجة تطبيقات Proxy على ثلاثة مكونات أساسية. فهم أدوارها هو مفتاح إتقان كائنات Proxy.
- الهدف (Target): هذا هو الكائن الأصلي الذي تريد تغليفه. يمكن أن يكون أي نوع من الكائنات، بما في ذلك المصفوفات أو الدوال أو حتى بروكسي آخر. يقوم البروكسي بمحاكاة هذا الهدف افتراضيًا، ويتم في النهاية (وإن لم يكن بالضرورة) توجيه جميع العمليات إليه.
- المعالج (Handler): هذا كائن يحتوي على المنطق الخاص بالبروكسي. إنه كائن نائب تكون خصائصه عبارة عن دوال، تُعرف باسم 'الفخاخ' (traps). عندما تحدث عملية على البروكسي، فإنه يبحث عن فخ مقابل في المعالج.
- الفخاخ (Traps): هي التوابع الموجودة في المعالج والتي توفر الوصول إلى الخصائص. يتوافق كل فخ مع عملية كائن أساسية. على سبيل المثال، يعترض الفخ
get
قراءة الخصائص، ويعترض الفخset
كتابة الخصائص. إذا لم يتم تعريف فخ في المعالج، يتم ببساطة توجيه العملية إلى الهدف كما لو أن البروكسي لم يكن موجودًا.
صيغة إنشاء بروكسي بسيطة ومباشرة:
const proxy = new Proxy(target, handler);
لنلقِ نظرة على مثال أساسي جدًا. سننشئ بروكسي يقوم ببساطة بتمرير جميع العمليات إلى الكائن الهدف باستخدام معالج فارغ.
// الكائن الأصلي
const target = {
message: "Hello, World!"
};
// معالج فارغ. سيتم توجيه جميع العمليات إلى الهدف.
const handler = {};
// كائن البروكسي
const proxy = new Proxy(target, handler);
// الوصول إلى خاصية على البروكسي
console.log(proxy.message); // المخرج: Hello, World!
// تم توجيه العملية إلى الهدف
console.log(target.message); // المخرج: Hello, World!
// تعديل خاصية عبر البروكسي
proxy.anotherMessage = "Hello, Proxy!";
console.log(proxy.anotherMessage); // المخرج: Hello, Proxy!
console.log(target.anotherMessage); // المخرج: Hello, Proxy!
في هذا المثال، يتصرف البروكسي تمامًا مثل الكائن الأصلي. تكمن القوة الحقيقية عندما نبدأ في تحديد الفخاخ في المعالج.
تشريح البروكسي: استكشاف الفخاخ الشائعة
يمكن أن يحتوي كائن المعالج على ما يصل إلى 13 فخًا مختلفًا، كل منها يتوافق مع تابع داخلي أساسي لكائنات JavaScript. دعنا نستكشف الأكثر شيوعًا وفائدة منها.
فخاخ الوصول إلى الخصائص
1. `get(target, property, receiver)`
يمكن القول إن هذا هو الفخ الأكثر استخدامًا. يتم تشغيله عند قراءة خاصية من البروكسي.
target
: الكائن الأصلي.property
: اسم الخاصية التي يتم الوصول إليها.receiver
: البروكسي نفسه، أو كائن يرث منه.
مثال: قيم افتراضية للخصائص غير الموجودة.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// إذا كانت الخاصية موجودة في الهدف، قم بإرجاعها.
// وإلا، قم بإرجاع رسالة افتراضية.
return property in target ? target[property] : `الخاصية '${property}' غير موجودة.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // المخرج: John
console.log(userProxy.age); // المخرج: 30
console.log(userProxy.country); // المخرج: الخاصية 'country' غير موجودة.
2. `set(target, property, value, receiver)`
يتم استدعاء الفخ set
عند تعيين قيمة لخاصية في البروكسي. إنه مثالي للتحقق من الصحة أو التسجيل أو إنشاء كائنات للقراءة فقط.
value
: القيمة الجديدة التي يتم تعيينها للخاصية.- يجب أن يُرجع الفخ قيمة منطقية (boolean):
true
إذا نجح التعيين، وfalse
خلاف ذلك (مما سيؤدي إلى إطلاقTypeError
في الوضع الصارم - strict mode).
مثال: التحقق من صحة البيانات.
const person = {
name: 'Jane Doe',
age: 25
};
const validationHandler = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError('يجب أن يكون العمر عددًا صحيحًا.');
}
if (value <= 0) {
throw new RangeError('يجب أن يكون العمر رقمًا موجبًا.');
}
}
// إذا نجح التحقق من الصحة، قم بتعيين القيمة على الكائن الهدف.
target[property] = value;
// إشارة إلى النجاح.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // هذا صالح
console.log(personProxy.age); // المخرج: 30
try {
personProxy.age = 'thirty'; // يطلق TypeError
} catch (e) {
console.error(e.message); // المخرج: يجب أن يكون العمر عددًا صحيحًا.
}
try {
personProxy.age = -5; // يطلق RangeError
} catch (e) {
console.error(e.message); // المخرج: يجب أن يكون العمر رقمًا موجبًا.
}
3. `has(target, property)`
يعترض هذا الفخ عامل التشغيل in
. يسمح لك بالتحكم في الخصائص التي تبدو موجودة في الكائن.
مثال: إخفاء الخصائص 'الخاصة'.
في JavaScript، من المتعارف عليه وضع شرطة سفلية (_) قبل الخصائص الخاصة. يمكننا استخدام الفخ has
لإخفائها عن عامل التشغيل in
.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // تظاهر بأنها غير موجودة
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // المخرج: true
console.log('_apiKey' in dataProxy); // المخرج: false (على الرغم من أنها موجودة في الهدف)
console.log('id' in dataProxy); // المخرج: true
ملاحظة: هذا يؤثر فقط على عامل التشغيل in
. الوصول المباشر مثل dataProxy._apiKey
سيظل يعمل ما لم تقم أيضًا بتنفيذ فخ get
مقابل.
4. `deleteProperty(target, property)`
يتم تنفيذ هذا الفخ عند حذف خاصية باستخدام عامل التشغيل delete
. إنه مفيد لمنع حذف الخصائص المهمة.
يجب أن يُرجع الفخ true
للحذف الناجح أو false
للفشل.
مثال: منع حذف الخصائص.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`محاولة حذف خاصية محمية: '${property}'. تم رفض العملية.`);
return false;
}
return true; // الخاصية لم تكن موجودة على أي حال
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// مخرج وحدة التحكم: Attempted to delete protected property: 'port'. Operation denied.
console.log(configProxy.port); // المخرج: 8080 (لم يتم حذفها)
فخاخ تعداد ووصف الكائنات
5. `ownKeys(target)`
يتم تشغيل هذا الفخ بواسطة العمليات التي تحصل على قائمة خصائص الكائن الخاصة، مثل Object.keys()
و Object.getOwnPropertyNames()
و Object.getOwnPropertySymbols()
و Reflect.ownKeys()
.
مثال: تصفية المفاتيح.
لندمج هذا مع مثالنا السابق للخاصية 'الخاصة' لإخفائها تمامًا.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const keyHidingHandler = {
has(target, property) {
return !property.startsWith('_') && property in target;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
},
get(target, property, receiver) {
// منع الوصول المباشر أيضًا
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // المخرج: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // المخرج: true
console.log('_apiKey' in fullProxy); // المخرج: false
console.log(fullProxy._apiKey); // المخرج: undefined
لاحظ أننا نستخدم Reflect
هنا. يوفر كائن Reflect
توابع لعمليات JavaScript القابلة للاعتراض، وتوابعه لها نفس الأسماء والتوقيعات مثل فخاخ البروكسي. من أفضل الممارسات استخدام Reflect
لتوجيه العملية الأصلية إلى الهدف، مما يضمن الحفاظ على السلوك الافتراضي بشكل صحيح.
فخاخ الدوال والمنشئات
لا تقتصر كائنات Proxy على الكائنات العادية. عندما يكون الهدف دالة، يمكنك اعتراض الاستدعاءات والإنشاءات.
6. `apply(target, thisArg, argumentsList)`
يتم استدعاء هذا الفخ عند تنفيذ بروكسي لدالة. يعترض استدعاء الدالة.
target
: الدالة الأصلية.thisArg
: سياقthis
للاستدعاء.argumentsList
: قائمة الوسائط التي تم تمريرها إلى الدالة.
مثال: تسجيل استدعاءات الدوال ووسائطها.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`استدعاء الدالة '${target.name}' بالوسائط: ${argumentsList}`);
// تنفيذ الدالة الأصلية بالسياق والوسائط الصحيحة
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`الدالة '${target.name}' أعادت: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// مخرج وحدة التحكم:
// Calling function 'sum' with arguments: 5,10
// Function 'sum' returned: 15
7. `construct(target, argumentsList, newTarget)`
يعترض هذا الفخ استخدام عامل التشغيل new
على بروكسي لصنف أو دالة.
مثال: تنفيذ نمط Singleton.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`جارٍ الاتصال بـ ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('جارٍ إنشاء نسخة جديدة.');
instance = Reflect.construct(target, argumentsList);
}
console.log('جارٍ إرجاع النسخة الحالية.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// مخرج وحدة التحكم:
// Creating new instance.
// Connecting to db://primary...
// Returning existing instance.
const conn2 = new ProxiedConnection('db://secondary'); // سيتم تجاهل عنوان URL
// مخرج وحدة التحكم:
// Returning existing instance.
console.log(conn1 === conn2); // المخرج: true
console.log(conn1.url); // المخرج: db://primary
console.log(conn2.url); // المخرج: db://primary
حالات الاستخدام العملية والأنماط المتقدمة
الآن بعد أن غطينا الفخاخ الفردية، دعنا نرى كيف يمكن دمجها لحل مشاكل العالم الحقيقي.
1. تجريد واجهة برمجة التطبيقات وتحويل البيانات
غالبًا ما تُرجع واجهات برمجة التطبيقات البيانات بتنسيق لا يتطابق مع اصطلاحات تطبيقك (على سبيل المثال، snake_case
مقابل camelCase
). يمكن للبروكسي التعامل مع هذا التحويل بشفافية.
function snakeToCamel(s) {
return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}
// تخيل أن هذه هي بياناتنا الأولية من واجهة برمجة تطبيقات
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// تحقق مما إذا كانت نسخة camelCase موجودة مباشرة
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// الرجوع إلى اسم الخاصية الأصلي
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// يمكننا الآن الوصول إلى الخصائص باستخدام camelCase، على الرغم من أنها مخزنة كـ snake_case
console.log(userModel.userId); // المخرج: 123
console.log(userModel.firstName); // المخرج: Alice
console.log(userModel.accountStatus); // المخرج: active
2. الكائنات القابلة للمراقبة وربط البيانات (جوهر الأطر الحديثة)
كائنات Proxy هي المحرك وراء أنظمة التفاعلية (reactivity) في الأطر الحديثة مثل Vue 3. عند تغيير خاصية على كائن حالة (state) مُغلّف ببروكسي، يمكن استخدام الفخ set
لتشغيل التحديثات في واجهة المستخدم أو أجزاء أخرى من التطبيق.
إليك مثال مبسط للغاية:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // تشغيل رد الاتصال عند التغيير
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Hello'
};
function render(prop, value) {
console.log(`تم اكتشاف تغيير: تم تعيين الخاصية '${prop}' إلى '${value}'. جارٍ إعادة عرض واجهة المستخدم...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// مخرج وحدة التحكم: CHANGE DETECTED: The property 'count' was set to '1'. Re-rendering UI...
observableState.message = 'Goodbye';
// مخرج وحدة التحكم: CHANGE DETECTED: The property 'message' was set to 'Goodbye'. Re-rendering UI...
3. مؤشرات المصفوفة السالبة
مثال كلاسيكي وممتع هو توسيع سلوك المصفوفة الأصلي لدعم المؤشرات السالبة، حيث يشير -1
إلى العنصر الأخير، على غرار لغات مثل Python.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// تحويل المؤشر السالب إلى مؤشر موجب من النهاية
property = String(target.length + index);
}
return Reflect.get(target, property);
}
};
return new Proxy(arr, handler);
}
const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);
console.log(proxiedArray[0]); // المخرج: a
console.log(proxiedArray[-1]); // المخرج: e
console.log(proxiedArray[-2]); // المخرج: d
console.log(proxiedArray.length); // المخرج: 5
اعتبارات الأداء وأفضل الممارسات
بينما تتمتع كائنات Proxy بقوة لا تصدق، إلا أنها ليست حلاً سحريًا. من الضروري فهم آثارها.
الحمل الزائد على الأداء
يقدم البروكسي طبقة من التوجيه غير المباشر. يجب أن تمر كل عملية على كائن مُغلّف ببروكسي عبر المعالج، مما يضيف قدرًا صغيرًا من الحمل الزائد مقارنة بالعملية المباشرة على كائن عادي. بالنسبة لمعظم التطبيقات (مثل التحقق من صحة البيانات أو التفاعلية على مستوى الإطار)، فإن هذا الحمل الزائد لا يذكر. ومع ذلك، في الشيفرة البرمجية ذات الأهمية الحاسمة للأداء، مثل حلقة ضيقة تعالج ملايين العناصر، يمكن أن يصبح هذا عنق زجاجة. قم دائمًا بقياس الأداء إذا كان الأداء مصدر قلق أساسي.
ثوابت البروكسي
لا يمكن للفخ أن يكذب تمامًا بشأن طبيعة الكائن الهدف. تفرض JavaScript مجموعة من القواعد تسمى 'الثوابت' (invariants) التي يجب أن تلتزم بها فخاخ البروكسي. سيؤدي انتهاك أحد الثوابت إلى إطلاق TypeError
.
على سبيل المثال، أحد ثوابت فخ deleteProperty
هو أنه لا يمكنه إرجاع true
(مما يشير إلى النجاح) إذا كانت الخاصية المقابلة في الكائن الهدف غير قابلة للتكوين (non-configurable). هذا يمنع البروكسي من الادعاء بأنه حذف خاصية لا يمكن حذفها.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// هذا سينتهك الثابت
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // سيطلق هذا خطأ
} catch (e) {
console.error(e.message);
// المخرج: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}
متى تستخدم كائنات Proxy (ومتى لا تستخدمها)
- جيدة لـ: بناء الأطر والمكتبات (مثل إدارة الحالة، ORMs)، التصحيح والتسجيل، تنفيذ أنظمة تحقق قوية، وإنشاء واجهات برمجة تطبيقات قوية تجرد هياكل البيانات الأساسية.
- فكر في بدائل لـ: الخوارزميات ذات الأهمية الحاسمة للأداء، توسيعات الكائنات البسيطة حيث يكون الصنف أو دالة المصنع كافية، أو عندما تحتاج إلى دعم متصفحات قديمة جدًا لا تدعم ES6.
كائنات Proxy القابلة للإلغاء
للسيناريوهات التي قد تحتاج فيها إلى 'إيقاف' بروكسي (لأسباب أمنية أو لإدارة الذاكرة)، توفر JavaScript Proxy.revocable()
. تُرجع كائنًا يحتوي على كل من البروكسي ودالة revoke
.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // المخرج: sensitive
// الآن، نلغي وصول البروكسي
revoke();
try {
console.log(proxy.data); // سيطلق هذا خطأ
} catch (e) {
console.error(e.message);
// المخرج: Cannot perform 'get' on a proxy that has been revoked
}
مقارنة بين البروكسي وتقنيات البرمجة الوصفية الأخرى
قبل ظهور كائنات Proxy، استخدم المطورون طرقًا أخرى لتحقيق أهداف مماثلة. من المفيد فهم كيفية مقارنة كائنات Proxy بها.
`Object.defineProperty()`
تعدل Object.defineProperty()
كائنًا مباشرةً عن طريق تحديد دوال getter و setter لخصائص معينة. من ناحية أخرى، لا تعدل كائنات Proxy الكائن الأصلي على الإطلاق؛ بل تقوم بتغليفه.
- النطاق: تعمل `defineProperty` على أساس كل خاصية على حدة. يجب عليك تحديد getter/setter لكل خاصية تريد مراقبتها. فخاخ
get
وset
في Proxy عالمية، حيث تلتقط العمليات على أي خاصية، بما في ذلك الخصائص الجديدة المضافة لاحقًا. - القدرات: يمكن لكائنات Proxy اعتراض نطاق أوسع من العمليات، مثل
deleteProperty
، وعامل التشغيلin
، واستدعاءات الدوال، وهو ما لا تستطيع `defineProperty` القيام به.
الخاتمة: قوة المحاكاة الافتراضية
إن واجهة برمجة تطبيقات Proxy في JavaScript هي أكثر من مجرد ميزة ذكية؛ إنها تحول أساسي في كيفية تصميمنا للكائنات والتفاعل معها. من خلال السماح لنا باعتراض وتخصيص العمليات الأساسية، تفتح كائنات Proxy الباب أمام عالم من الأنماط القوية: من التحقق من صحة البيانات وتحويلها بسلاسة إلى الأنظمة التفاعلية التي تشغل واجهات المستخدم الحديثة.
على الرغم من أنها تأتي بتكلفة أداء صغيرة ومجموعة من القواعد التي يجب اتباعها، إلا أن قدرتها على إنشاء تجريدات نظيفة ومنفصلة وقوية لا مثيل لها. من خلال محاكاة الكائنات افتراضيًا، يمكنك بناء أنظمة أكثر قوة وقابلية للصيانة وتعبيرًا. في المرة القادمة التي تواجه فيها تحديًا معقدًا يتعلق بإدارة البيانات أو التحقق من صحتها أو المراقبة، فكر فيما إذا كان Proxy هو الأداة المناسبة لهذه المهمة. قد يكون هو الحل الأكثر أناقة في مجموعة أدواتك.