الوسيط Proxy و الـReflect
الكائن Proxy
يُحيط كائن آخر و يتوسط العمليات التى تُجري على هذا الكائن، مثل القراءة أو التعديل على الخصائص وغيرها والتعامل معهم بشكل اختياري أو تجعل الكائن الأساسي يتعامل معهم دون تدخل.
إن الـProxies مُستخدمة فى كثير من المكتبات وبعض أطر العمل. سنرى الكثير من التطبيقات العملية فى هذا المقال.
الوسيط Proxy
الشكل:
let proxy = new Proxy(target, handler);
target
– هو الكائن الذي يتم إحاطته ويمكن أن يكون أى شيئ حتي الدوال (functions).handler
– إعدادت الـproxy: هو كائن يحتوي علي “traps” والتي هي عبارة عن دوال تعمل فى العمليات. مثلًا الـget
trap لقراءة خاصية من الـobjecttarget
وكذلكset
trap لتعديل\إضافة خاصية للـobjecttarget
وهكذا.
بالنسبة للعمليات فى الـ proxy
، فإنه إذا كان هناك trap فى الـobject handler
وثم بعد ذلك تعمل وبعد ذلك يكون لديها الفرصة للتعامل معه وإذا لم يوجد trap فإن العملية تُجري علي target
.
كمثال مبدأى، هيا ننشئ proxy بدون traps:
let target = {};
let proxy = new Proxy(target, {}); // handler فارغ
proxy.test = 5; // التعديل علي البروكسي (1)
alert(target.test); // 5, ظهور الخاصية فى الكائن الأصلي!
alert(proxy.test); // 5, ويمكننا قرائته من البروكسي أيضًا (2)
for (let key in proxy) alert(key); // test, التكرار يعمل (3)
وبما أنه لا توجد traps فإن كل العمليات التي التي تُجرى على الـ proxy
يتم تحويلها إلى الـ target
.
- عملية التعديل
proxy.test=
تُعدل القيمة فى الـtarget
. - عملية القراءة
proxy.test
تقوم بإرجاع القيمة منtarget
. - التكرار على الـ
proxy
يقوم بإرجاع القيم منtarget
.
كما نري فإن الـproxy
بدون traps هو محيط شفاف حول target
. أى أنه لا يفعل أى شيئ فى المنتصف.
Proxy
إن الـ كائن من نوع خاص لا يحتوي علي أى خصائص وعندما يكون handler
فارغًا فإنه يحوّل كل العمليات إلى target
.
للحصول علي قدرات أكثر هيا نضيف traps.
ماذا يمكننا أن نتدخّل (intercept) بهم؟
لأغلب العمليات علي الـobjects توجد دوال تسمي “الدوال الداخلية” “internal method” فى مصدر الجافاسكريبت والتي تصف كيفية عملها عند أقل مستوي. علي سبيل المثال الدالة [[Get]]
هي دالة داخلية لقراءة خاصية والدالة [[Set]]
هي دالة داخلية لإضافة\تعديل خاصية وهكذا. هذه الدوال تستخدم فقط داخليًا ولا يمكننا استعمالهم بشكل مباشر.
تتدخّل الـ Proxy traps فى استدعاء هذه الدوال. وهم موجودون بالتفصيل في المصدر وفي الجدول أدناه.
لكل دالة داخلية يوجد trap في هذا الجدول: اسم الدالة التي يمكننا إضافتها للمتغير الذي يسمي handler
والذي نضيفه للـ new Proxy
للتدخل فى العملية:
الدالة الداخلية | الدالة العاملة | تعمل عند… |
---|---|---|
[[Get]] |
get |
قراءة خاصية |
[[Set]] |
set |
التعديل علي خاصية |
[[HasProperty]] |
has |
in المعامل |
[[Delete]] |
deleteProperty |
delete المعامل |
[[Call]] |
apply |
استدعاء دالة |
[[Construct]] |
construct |
new المعامل |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in , Object/keys/values/entries |
تفرض الجافاسكريبت بعض الثوابت – شروط يجب أن تتحقق بالmethods و الtraps.
أغلبهم لإرجاع قيم:
- الدالة
[[Set]]
يجب أن تقوم بإرجاعtrue
إذا كُتبت القيمة بشكل صحيح أو تقوم بإرجاعfalse
إذا لم تكن كذلك. - الدالة
[[Delete]]
يجب أن اقةم بإرجاعtrue
إذا تم حذف القيمة بشكل صحيح وإلا تقوم بإرجاعfalse
. - …وهكذا، وسنري المزيد في الأمثلة القادمة.
هناك المزيد من الثوابت، مثلًا:
- الدالة
[[GetPrototypeOf]]
الموجودة في البروكسي، يجب أن تُرجع نفس القيمة التي تُرجعها الدالة[[GetPrototypeOf]]
الموجودة في الأوبجكت المستهدف (target). أو يمكننا القول بطريقة أخري، أن استرجاع القيم من الprototype الخاص بالبروكسي يجب دائما أن تُرجع الprototype الخاص بالأوبجكت المستهدف (target).
تستطيع الtraps أن تعترض طريق هذه العمليات، ولكن يجب أن يتبعو هذه القواعد.
تضمن الثوابت صحة وتناسق سلوك مزايا اللغة. وللإطلاع علي القائمة الكاملة للثوابت فهي موجودة في المصدر. لا تقلق، لن تقوم بمخالفة هذه الثوابت مالم تكن تقوم بعمل غريب.
هيا نري كيف يتم تطبيق ذلك في أمثلة عملية.
إرجاع قيمة افتراضية بالtrap “get”
أكثر الtraps استعمالًا التي تسترجع أو تعدل الخصائص (properties).
لاعتراض طريق عملية الإسترجاع، يجب أن يحتوي الhandler
علي الدالة get(target, property, receiver)
.
يتم تشغيلها عندما تكون الخاصية للإسترجاع بالمتغيرات التالية:
target
– هو الأوبجكت المستهدف والذي يتم تمريره كمتغير أول للnew Proxy
,property
– اسم الخاصية,receiver
– إذا كانت الخاصية للإسترجاع getter فإن الreceiver
هو الأوبجكت الذي سيتم استخدامه كقيمة لthis
عند استرجاعها. وعادة مايكون البروكسي نفسه (أو أوبجكت يرث منه). والآن لا نريد هذا المتغير، ولذلك سيتم شرحه لاحقًا بالتفصيل.
هيا نستخدم get
لتطبيق القيمة الإفتراضية لأوبجكت.
سنقوم بإنشاء array تحتوي علي أرقام والتي تقوم بإرجاع 0
للقيم الغير موجودة.
عادة، عندما يحاول أحد أن يسترجع قيمة غير موجودة في الarray فإنه يحصل علي undefined
, ولكننا سنحيط الarray العادية ببروكسي والذي سيعترض عملية الاسترجاع ويقوم بإرجاع 0
إذا لم توجد الخاصية:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // القيمة الإفتراضية
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (لا يوجد عنصر كهذا)
كما نري، فأن هذا يمكن تحقيقه بسهوله باستخدام الtrap get
.
يمكننا استخدام Proxy
لتطبيق أي طريقة للقيم الإفتراضية.
تخيل أن لدينا قاموسًا، بالجمل وترجماتها:
let dictionary = {
Hello: "مرحبًا",
Bye: "إلي اللقاء",
};
alert(dictionary["Hello"]); // مرحبًا
alert(dictionary["Welcome"]); // undefined
حاليًا، ليست هناك جملة، فعند الإسترجاع من القاموس تقوم بإرجاع undefined
. ولكن في التطبيق العملي، فإن ترك الجملة غير مترجمة أفضل من undefined
. ولذلك سنجعلها تقوم بإرجاع الجملة غير مترجمة بدلًا من undefined
.
لتحقيق ذلك، سنحيط الdictionary
ببروكسي والذي سيقوم باعتراض طريق استرجاع الخصائص:
let dictionary = {
'Hello': 'مرحبًا',
'Bye': 'إلي اللقاء'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // تعترض طريق استرجاع الخصائص من القاموس
if (phrase in target) { // إذا كانت لدينا بالفعل
return target[phrase]; // قم بإرجاع الترجمة
} else {
// وإلا فلتقم بإرجاع الجملة كما هي
return phrase;
}
}
});
// قم باكتشاف الجمل الاعتباطية!
// في أسوأ الحالت لن يتم ترجمتهم.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (لا توجد ترجمة)
لاحظ كيف يستبدل البروكسي المتغير:
dictionary = new Proxy(dictionary, ...);
يجب أن يقوم المتغير باستبدال الأوبجكت المستهدف\المستقبل بشكل تام. يجب ألا يستطيع أحد استدعاء الأوبجكت المستقبل بعد أن تمت إحاطته ببروكسي. وإلا سيكون من السهل أن تفسد كل شيئ.
التحقق من القيم باستخدام الtrap “set”
دعنا نقول أننا نريد array للأرقام فقط. وإذا تمت إضافة قيمة من نوع آخر، يجب أن يكون هناك خطأ.
تعمل الtrap set
عند التعديل علي خاصية.
set(target, property, value, receiver)
:
target
– هو الأوبجكت المستقبل، هو الذي يتم تمريره كمتغير أول لـnew Proxy
,property
– إسم الخاصية,value
– قيمة الخاصية,receiver
– شبيه بالJtrapget
، ولكن مفيد فقط للخصائص التي يتم التعديل عليها.
يجب أن يقوم الtrap set
بإرجاع true
إذا نجح التعديل، وإلا يقوم بإرجاع false
(يقوم بتشغيل TypeError
).
هيا نستخدمه للتحقق من القيم الجديدة:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // لاعتراض عملية التعديل
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // تمت إضافته بنجاح
numbers.push(2); // تمت إضافته بنجاح
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("لن يتم الوصول إلي هذا السطر أبدا، فهناك خطأ فى السطر الأعلي");
لاحظ أن: الوظيفة الأساسية للarray ما زالت تعمل كما هي! تُضاف القيم باستخدام push
. وتزداد الخاصية length
تلقائيًا عند إضافة قيم جديدة. لا يعدل البروكسي أي شيئ.
لسنا مضطرين لاستبدال الدوال المسؤولة عن إضافة قيم للarray مثل push
و unshift
, وهكذا, لإضافة تحققات هناك, لأنهم ضمنيًا يستخدمون الدالة [[Set]]
والتي يتم اعتراضها بالبروكسي.
وهكذا يكون الكود نظيف ومتناسق.
true
كما قيل بالأعلي، هناك ثوابت يجب الإلتزام بها.
ففي الدالة set
، يجب أن تقوم بإرجاع true
في التعديل الناجح.
إذا نسينا أن نفعل ذلك أو قمنا بإرجاع أى قيمة غير حقيقية (falsy)، يتقوم العملية بتفعيل الخطأ TypeError
.
التكرار باستخدام “ownKeys” و “getOwnPropertyDescriptor”
التكرارات Object.keys
, for..in
وأغلب الدوال الأخري اللتي تقوم بالتكرار علي خصائص الأوبجكت تستخدم ضمنيًا الدالة [[OwnPropertyKeys]]
(والتي يتم اعتراضها عن طريق الtrapownKeys
) لاسترجاع قائمة من الخصائص.
دوال كهذه تختلف فى التفاصيل:
- تقوم الدالة
Object.getOwnPropertyNames(obj)
بإرجاع الخصائص التي ليست من نوع الرمز (symbol). - تقوم الدالة
Object.getOwnPropertySymbols(obj)
بإرجاع الخصائص من نوع الرمز. - تقوم الدالة
Object.keys/values()
بإرجاع الخصائص والقيم التي ليست من نوع الرمز والتي تحتوى علي المعرٌفenumerable
(تم شرح المعرفات في المقال رايات الخصائص و واصفاتها). - التكرارات
for..in
تقوم بالتكرار علي الخصائص التي ليست من نوع الرمز والمحتوية علي المعرفenumerable
وأيضًا الخصائص الموجودة فى الprototype.
…ولكن جميعهم يبدأون بهذه القائمة.
في المثال أدناه استخدمنا الtrap ownKeys
لجعل التكرار for..in
علي الأوبجكت user
، وكذلك Object.keys
و Object.values
، لتخطي الخصائص اللتي تبدأ بـ _
:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
إنها تعمل الآن.
علي الرغم من ذلك، إذا قمنا بإرجاع خاصية ليست موجودة في الأوبجكت فإن Object.keys
لن تعرضه:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <فارغ>
لماذا؟ السبب بسيط: تقوم Object.keys
بإرجاع الخصائص المحتويه علي المعرف enumerable
فقط. للتحقق من ذلك، هي تقوم باستدعاء الدالة [[GetOwnProperty]]
لكل خاصية لاسترجاع المعرف الخاص بها. وهنا، بما أنه لا يوجد خصائص، فإن معرفها فارغ، ولا يوجد المعرف enumerable
فيتم تخطيها.
لجعل Object.keys
تقوم بإرجاع خاصية، نحتاج إلي أن تكون موجودة في الأوبجكت ومحتوية علي المعرف enumerable
، أو يمكننا اعتراض استدعاء الدالة [[GetOwnProperty]]
(يقوم بهذا الtrap getOwnPropertyDescriptor
)، ويقوم بإرجاع واصف (descriptor) والراية enumerable
: true.
هنا مثال علي ذلك:
let user = {};
user = new Proxy(user, {
ownKeys(target) {
// يتم استدعاؤها مرة لإرجاع قائمة
return ["a", "b", "c"];
},
getOwnPropertyDescriptor(target, prop) {
// يتم استدعاؤها لكل خاصية
return {
enumerable: true,
configurable: true,
/* ...other flags, probable "value:..." */
};
},
});
alert(Object.keys(user)); // a, b, c
هيا نسجل ذلك مرة أخري: نحتاج لاعتراض [[GetOwnProperty]]
فقط إذا كانت الخاصية غير موجودة في الأوبجكت.
الخصائص المحمية باستخدام “deleteProperty” وغيره من الtraps
هناك شئ شائع متفق عليه وهو أن الخصائص التي تبدأ بـ_
هي ضمنية ولا يجب أن يتم الوصول إليها من خارج الأوبجكت.
وهذا ممكن تقنيًا:
let user = {
name: "John",
_password: "secret",
};
alert(user._password); // secret
هيا نستخدم الproxies لمنع أي وصول إلي الخسائص البادئة بـ _
.
سنحتاج إلي الtraps:
get
لإطهار خطأ عند استرجاع خاصية كهذه,set
لإظهار خطأ عند التعديل,deleteProperty
لإظهار خطأ عند الحذف,ownKeys
لاستثناء الخصائص البادئة بـ_
من التكرارfor..in
والدوال الأخري مثلObject.keys
.
إليك الكود:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // لاعتراض التعديل علي الخاصية
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // لاعتراض حذف الخاصية
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // لاعتراض عرض الخصائص في قائمة
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" لا تسمح بإرجاع _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" لا تسمح بتعديل _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" لا تسمج بحذف _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" تستثني _password
for(let key in user) alert(key); // name
لاحظ التفصيلة المهمه في الtrap get
في السطر (*)
:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
لماذا نحتاج إلي دالة لاستدعاء value.bind(target)
؟
والسبب أن دوال أﻷوبجكت، مثل user.checkPassword()
، يجب أن تقدر علي الوصول إلى _password
:
user = {
// ...
checkPassword(value) {
// دالة الأوبجكت يجب أن تقدر علي الوصول إلي _password
return value === this._password;
},
};
استدعاء user.checkPassword()
يقوم بإرجاع user
المُحاط ببروكسي كقيمة لـ this
(الأوبجكت قبل علامة النقطة هو قيمة this
)، ولذلك فعندما تحاول الوصول إلي this._password
ينشط الـtrap get
(تعمل مع كل استدعاء لخاصية) وتظهر خطأًا.
ولذلك نقوم بربط سياق دوال الأوبجكت بالأوبجكت الأصلي، target
، في لاسطر (*)
. وبعد ذلك فإن استدعائهم في المستقبل سيسختدم target
كقيمة لـ this
، بدون trap.
هذا الحل عادة ما يعمل، ولكنه ليس مثاليًا، فإن دالة كهذه يمكنها أن ترجع الأوبجكت غير محاط ببروكسي في أي مكان آخر وهكذا سيفسد كل شيئ: أين الأوبجكت الأصلي؟ وأين المحاط ببروكسي؟
إلي جانب ذلك، فإن أوبجكت كهذا يمكن إحاطته ببروكسي أكثر من مره (كل بروكسي يمكن أن يضيف تعديلات غير منتهية للأوبجكت)، وإذا قمنا بتمرير أوبجكت غير محاط لأوبجكت، فإنه يمكن أن يكون هناك نتائج غير متوقعه.
ولذلك فإن بروكسي كهذا لا يجب أن يتم استخدامه في كل مكان.
محركات الجافاسكريبت الحديثة تدعم الخصائص الخاصة (private properties) في الكلاس، مسبوقة بالعلامة #
, تم شرحهم في المقال الخواص والدوال الـ `private` و الـ `protected`. لا نحتاج إلي بروكسي.
خصائص كهذه لها مشاكلها الخاصة. تحديدًا، لا يمكن توارثها.
“In range” مع الtrap “has”
هيا نري أمثلة أخري.
لدينا الأوبجكت range:
let range = {
start: 1,
end: 10,
};
نود أن نستعمل in
للتحقق من وجود هذا الرقم في الـrange
.
الtrap has
الذي يعترض اسدعاء in
.
has(target, property)
target
– هو الأوبجكت المستهدف، يتم تمريره كمتغير أول لـnew Proxy
,property
– اسم الخاصية
هنا التطبيق:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
مصطلح بديل لطيف، أليس كذلك؟ وسهل تطبيقه.
الدوال المُحاطة: "apply"
يمكن أن نحيط دالة ببروكسي أيضًا.
الtrap apply(target, thisArg, args)
يتعامل مع استدعاء البروكسي كدالة:
target
هو الأوبجكت المستهدف (الدوال ماهي إلا أوبجكت في الجافاسكريبتthisArg
هو قيمةthis
.args
هو قائمة من المتغيرات.
علي سبيل المثال هيا نعيد استذكار delay(f, ms)
، والتي قمنا بإنشائها في المقال المزخرفات decorators والتمرير forwarding: التابعان call وapply.
في هذا المقال أنشأناها بدون بروكسي. فإن استدعاء delay(f, ms)
قام بإرجاع دالة تفوض كل الإستدعاءات إلي f
بعد ms
مللي ثانيه.
هنا الكود السابق، من غير بروكسي:
function delay(f, ms) {
// تُرجع غلاف يقوم بتمرير الاستدعاء ﻹلي f بعد انتهاء الوقت
return function () {
// (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// بعد هذه الإحاطه فإن استدعاء الدالة سيتأخر ل 3 ثواني
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
هذا يعمل كما رأينا بالفعل. الدالة المُحيطة (*)
تقوم بالإستدعاء بعد انتهاء الوقت.
ولكن لا تقوم الدالة المحيطه بتمرير قراءة أو تعديل خاصية أو أي شيء آخر. بعد الإحاطه، تتم خسارة الوصول إلي الخاصائص الخاصة بالدالة الأصليه، مثل name
, length
وغيرهم:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
إن ال Proxy
أقوي بكثير لأنه يقوم بتمرير كل شيئ إلي الأوبجكت المستهدف.
هيا نستخدم بروكسي بلًا من الدالة المحيطة:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)
النتيجة مماثله، ولكن الآن ليست الاستدعاءات فقط مايتم تمريرها ولكن الكل العمليات أيضًا. ولذلك فإن sayHi.length
يتم استرجاعها بشكل صحيح بعد الإحاطه في السطر (*)
.
لدينا غلاف أقوي.
هناك traps أخري: القائمة الكاملة موجودة في بداية المقال. وطريقة استخدامهم مشابهة لما سبق.
الأوبجكت Reflect
الأوبجكت Reflect
هو أوبجكت موجود في اللغة والذي يقوم بتبسيط إنشاء Proxy
.
لقد قيل سابقًا أن الدوال الضمنية مثل [[Get]]
, [[Set]]
وغيرهم هم دوال مصدرية فقط، لا يمكن استدعاؤهم بشكل مباشر.
الكائن Reflect
يجعل ذلك بسيطا نوعا ما. الدوال الخاصة به هي غلاف مباشر للدوال الضمنية.
هنا أمثلة لبعض العمليات وكذلك استدعاءات ال Reflect
والتي تقوم بعمل نفس الشيئ:
Operation | Reflect call |
Internal method |
---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
… | … | … |
علي سبيل المثال:
let user = {};
Reflect.set(user, "name", "John");
alert(user.name); // John
بالتحديد، يسمح لنا الReflect
باستدعاء العمليات (new
, delete
…) كدوال (Reflect.construct
, Reflect.deleteProperty
, …). وهذه ميزة جيدة ومثيرة، ولكن هنا شيئ آخر مهم.
لكل خاصية ضمنية، تم اعتراضها ببروكسي، دالة في ال Reflect
، بنفس الإسم والمتغيرات الخاصة بالtrap.
لذلك يمكننا استخدام Reflect
لتمرير عملية إلي الكائن الأصلي.
في هذا المثال، كلا من get
و set
يقومان بتمرير القراءة والتعديل إلي الأوبجكت بشكل شفاف، ويظهران رسالة:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
هنا:
Reflect.get
تقرأ خاصية لأوبجكت.Reflect.set
تقوم بتعديل خاصية لأوبجت وتُرجعtrue
في حالة النجاح وfalse
في غير ذلك.
وهكذا كل شيئ بسيط: إذا أراد trap أن يمرر استدعاءًا لأوبجكت فإنه من الكافي استدعاء Reflect.<method>
بنفس الخصائص.
في أغلب الحالات يمكننا فعل نفس الشيئ بدون Reflect
، علي سبيل المثال، قراءة خاصية بـ Reflect.get(target, prop, receiver)
يمكن استبداله بـ target[prop]
. ولكن مع ذلك هناك فروق مهمه.
إحاطة الـgetter أو الجالب ببروكسي
هيا نري مثالًا يوضح لماذا Reflect.get
أفضل. وسنري أيضًا لماذا get/set
لديهم المتغير الرابع receiver
الذي لم نستخدمه من فيل.
لدينا الأوبجكت user
الذي يحتوي علي الخاصية _name
وجالب لها.
هنا بروكسي حولها:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
الtrap get
شفاف هنا، حيث تقوم بإرجاع الخاصية الأصلية ولا تفعل أي شيئ آخر. وهذا طافٍ لمثالنا.
كل شيئ يبدو كأنه صحيح. ولكن هيا ننشئ مثالًا أكثر تعقيدًا.
بعد وراثة أوبجكت آخر admin
من user
، يمكننا مشاهدة السلوك الخاطئ:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
استرجاع admin.name
يجب أن ينتج “Admin”, وليس
“Guest”`!
ماذا حدث؟ من الممكن أننا فعلنا شيئًا خاطئًا مع الوراثة؟
ولكن إذا قمنا بإزالة البروكسي، سيعمل كل شيئ كما هو متوقع.
المشكلة تحديدًا في البروكسي في السطر (*)
.
-
عند قراءة
admin.name
فإن الأوبجكتadmin
ليس لديه خاصية كهذه فيذهب البحث إلي الـprototype المتصل به. -
الprototype هو
userProxy
. -
عند قراءة الخاصية
name
من البروكسي، فإن الtrapget
يُشغل ويُرجع قيمتها من الأوبجكت ألأصلي كماtarget[prop]
في السطر(*)
.استدعاء
target[prop]
، عندما تكونprop
جالبة، تقوم بتشغيل الكود في سياقthis=target
. لذلك تكون النتيجةthis._name
من الكائن الأصليtarget
والذي هوuser
.
لإصلاح ذلك، نحتاج إلي receiver
، المتغير الثالث للtrap get
. هي تحافظ علي القيمة الصحيحة لـ this
وتمريرها لجالب. في حالتنا هو admin
.
كيف تمرر سياق لجالب؟ في الدوال العادية يمكننا استخدام call/apply
ولكن هذا جالب ولا يتم استدعاؤه، فقط الوصول إليه إليه.
تستطيع Reflect.get
أن تفعل ذلك. كل شيئ يمكنه أن يعمل بشكل صحيح إذا استخدمناه.
هنا الكود المصحح:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
والآن فإن receiver
الذي يحافظ علي القيمة الصحيحه this
، يتم تمريره للجالب باستخدام Reflect.get
في السطر (*)
.
يمكننا كتابة الtrap بشكل أقصر:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
استدعاءات Reflect
مسماة بنفس أسماء الtraps وتستقبل نفس المتغيرات. تم إنشائهم بهذه الطريقة.
لذلك فإن return Reflect...
يعطينا طريقة آمنو لتمرير العمليات دون أن نقلق إن كنا نسينا شيئًا.
حدود البروكسي
إن البروكسي هو طريقة فريدة لتعديل سلوك الكائنات الموجودة علي أدني مستوي. ومع ذلك هو ليس أفضل شيئ. هناك حدود.
الأوبجكتس الموجود بالفعل: Internal slots
الكثير من الكائنات الموجودة بالفعل مثل Map
, Set
, Date
, Promise
وغيرهم يستخدمون مايسمي “internal slots”.
هي عبارة عن خصائص، ولكن محفوظة ويتم استخدامها ضمنيًا فقط. علي سبيل المثال، يخزن الـ Map
العناصر في فتحة داخلية (internal slot) تسمي [[MapData]]
. الدوال الموجودة في اللغة تصل إليهم مباشرة وليس عن طريق [[Get]]/[[Set]]
. ولذلك فإن البروكسي لا يستطيع اعتراضهم.
لماذا نهتم؟ إنهم أشياء مضمنة علي كل الأحوال!
حسنًا، هنا المشكله، بعد أن يتم إحاطة أوبجكت كهذا ببروكسي فإن البروكسي لا يملك هذه الـ internal slots ولذلك فإن الدوال الضمنية ستفشل.
علي سبيل المثال:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // خطأ
ضمنيًا، يخزن الـ Map
كل البيانات في [[MapData]]
. والبروكسي ليس لديه فتحة (slot) كهذه. والدالة Map.prototype.set
تحاول أن تصل إلي الخاصية الداخلية this.[[MapData]]
ولكن بما أن this=proxy
فإنها لا تجدها بداخل البروكسي وتفشل.
لحسن الحظ، هناك طريقة لإصلاح ذلك:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
والآن هي تعمل جيدًا، لأن الtrap get
يربط خصائص الدالة ، مثل map.set
، بالأوبجكت المستهدف.
علي عكس المثال السابق، فإن قيمة this
بداخل proxy.set(...)
لن تكون بروكسي ولكن فقط الmap
الأصلي. لذلك عندما تحاول الدالة set
أن تصل إلي this.[[MapData]]
فإنها تنجح.
Array
لا تحتوي علي internal slotsاستثناء ملحوظ: الـ Array
لا تستخدم الـ internal slots. وهذا لأسباب متأصلة وتاريخية، لأنها ظهرت منذ زمن طويل.
لذلك لا توجد مشكلة كهذه عند إحاطة المصفوفة ببروكسي.
الخصائص الخاصة Private fields
الشيئ المشابه يحدث مع الخصائص الخاصة بالكلاس.
علي سبيل المثال، الدالة getName()
تصل إلي الخاصية الخاصة #name
وتقف بعد الإحاطة:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // خطأ
السبب في ذلك أن الخصائص الداخلية يتم إنشاؤها بالـ internal slots. ولا تستخدم الجافاسكريبت [[Get]]/[[Set]]
عند الوصول إليهم.
عند استدعاء getName()
فإن قيمة this
يتم إحاطتها بالأوبجكت user
، وهي لا تملك فتحة (slot) مع الخصائص الخاصة.
مرة أخري، فإن الحل بالربط يجعلها تعمل:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == "function" ? value.bind(target) : value;
},
});
alert(user.getName()); // Guest
كما قيل، فإن الحل له عيوب، كما تم التوضيح سابقًا: فإنه يقوم بتعريض الكائن الأصلي للدالة ويسمح بتمريره وإنهاء الكائنات بروكسي الأخري.
البروكسي ليس هو الأوبجكت المستهدف
إن البروكسي والكائن الأصلي مختلفان. هذا طبيعي، صحيح؟
لذلك إذا استخدمنا الأوبدجكت الأصلي كخاصية، ثم إحاطته ببروكسي، فإن البروكسي لا يمكن إيجاده:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
كما نري، فإنه بعد الإحاطة لا نستطيع أن نجد user
في المجموعه allUsers
، لأن البروكسي هو كائن مختلف.
===
البروكسي يمكنه اعتراض الكثير من العمليات، مثل new
باستخدام construct
و كذلك in
باستخدام has
وهكذا.
ولكن ليست هناك طريقة لاعتراض اختبار المساواة ===
للأوبجكتس. فإن الأوبجكت مساوٍ تماما لنفسه فقط وليس أي قيمة أخري.
لذلك فإن كل العمليات والكلاسز المبنيو في اللغة والتي تقارن الكائنات للمساواة ستقوم بالتفريق بين الأوبجكت والبروكسي. لا يوجد استبدال هنا.
البروكسي القابل للإلغاء
دعنا نقول أن لدينا مصدر، ونريد أن نمنع الوصول إليه في أى وقت.
مانستطيع فعله هو أن نحيطه ببروكسي قابل للإلغاء، بدون أي trap. هذا البروكسي سيقوم بتمرير العمليات إلي الأوبجكت ويمكننا أن نمنع الوصول إليه في أى وقت.
الشكل:
let {proxy, revoke} = Proxy.revocable(target, handler)
الإستدعاء يقوم بإرجاع أوبجكت يحتوي علي proxy
ودالة revoke
لإبطاله.
هاك مثالًا:
let object = {
data: "Valuable data",
};
let { proxy, revoke } = Proxy.revocable(object, {});
// تمرير اليروكسي إلى مكان آخر بلًا من الأوبجكت...
alert(proxy.data); // بيانات قيمة
// فيما بعد
revoke();
// لا يعمل البروكسي الآن (تم الإلغاء)
alert(proxy.data); // خطأ
استدعاء revoke()
يمسح كل المراجع الداخلية للأوبجكت المستهدف من البروكسي، ولذلك فإنهما ليسا متصلان بعد الآن. الأوبجكت المستهدف يمكن أن يتم تنظيفه بعد ذلك.
يمكننا أيضًا أن نخزن الدالة revoke
في WeakMap
، لنكون قادرين علي إيجاده بواسطة بروكسي:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..later in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // خطأ (تم إلغاؤه)
الفائدة من نهج كهذا هو أننا لسنا مضطرين لأن نحمل الدالة revoke
. يمكننا الحصول عليها من الmap بواسطة proxy
عند الحاجه.
نستخدم WeakMap
بدلًل من Map
هنا لأننا لا نريد أن نمنع عملية التنظيف (garbage collection). إذا أصبح الأوبجكت “لا يمكن الوصول إليه” (علي سبيل المثال لا توجد أي متغيرات تصل إليها)، تسمح له الـ WeakMap
أن يتم مسحه من الذاكرة مع دالة revoke
الخاصة به فليس هناك حاجة لها بعد الآن.
المراجع
الملخص
إن الـ Proxy
هو غلاف حول الأوبجكت، والذي يقوم بتمرير العمليات إلي الأوبجكت، ويقوم باعتراض بعضهم بشكل اختياري.
يمكنه أن يحيط أي أوبجكت، بما فيه الكلاس والدالة.
الشكل:
let proxy = new Proxy(target, {
/* traps */
});
…بعد ذلك يجب أن نستخدم proxy
في كل مكان بدلًا من target
. إن البروكسي لا يحتوي علي خصائص أو دوال خاصة به. هو يقوم باعتراض العمليه إذا وجد trap وإلا فإنه يمرر العمليه إلي الأوبجكت المستهدف.
يمكننا أن نعترض:
- قراءة (
get
), تعديل (set
), حذف (deleteProperty
) خاصية (حتي إذا لم تكن موجودة). - استدعاء دالة (
apply
). - المعامل
new
(construct
trap). - وغيره الكثير من العمليات (القامة الكاملة موجودة في بداية المقال وفي المصدر).
هذا يسمح لنا أن ننشئ خواص ودوال افتراضية واسترجاع قيم افتراضية وأوبجكت ملحوظ والكثير.
يمكننا أيضًا أن نعترض أوبجكت مرات عدة ببروكسي مختلف، وتعليمها بطرق مختلفة.
الكائن Reflect تم إنشاؤه ليكمل الـ Proxy. لكل trap في Proxy
, يوجد استدعاء لـ Reflect
بنفس المتغيرات. يجب أن نستخدمها لتمرير القيم إلي الأوبجكت المستهدف.
البروكسي له حدود:
- الأوبجكت المبنية بالفعل تمتلك “internal slots”، والوصول إليها لا مككن إحاطته ببروكسي. أنظر إلي الحل أعلاه.
- ومثله أيضًا الخصائص الخاصة في الكلاس، حيث أنهم يتم إنشاؤهم داخليا باستخدام فتحات (slots). ولذلك فإن الدوال المغلفة يجب أن تحتوي علي الأوبجكت المستهدف كقيمة لـ
this
للوصول إليهم بنجاح. - اختبار التساوي
===
لا يمكن اعتراضه. - السرعة: هذا يعتمد علي الإنجن ولكن بشكل عام فإن الوصول إلي خاصية ببروطسي بسيط يستغرق وقتًا أطول.
مهمه
عادة، عند محاولة قراءة خاصية غير موجودة فإنها تُرجع undefined
.
قم بإنشاء بروكسي يقوم بإظهار خطأ عند محاولة قراءة خاصية غير موجودة بدلًا من ذلك.
هذا يساعد علي استكشاف الأخطاء البرمجية بشكل أسرع.
قم بإنشاء دالة wrap(target)
والتي تستقبل كائن target
وتقوم بإرجاع بروكسي يضيف هذه الوظيفة.
هذا كيفية فعله:
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* your code */
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist "age"
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
} else {
throw new ReferenceError(`Property doesn't exist: "${prop}"`)
}
}
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist "age"
في بعض لغات البرمجة، يمكن الوصول إلي عناصر المصفوفات برقم سالب حيث تقوم بالحسبة من النهاية.
كهذا:
let array = [1, 2, 3];
array[-1]; // 3, the last element
array[-2]; // 2, one step from the end
array[-3]; // 1, two steps from the end
بطريقة أخري، فإن array[-N]
هو نفسه array[array.length - N]
.
قم بإنشاء بروكسي لتنفيذ هذا السلوك.
هنا كيف يجب أن تعمل:
let array = [1, 2, 3];
array = new Proxy(array, {
/* your code */
});
alert( array[-1] ); // 3
alert( array[-2] ); // 2
// السلوك الطبيعي للمصفوفات الأخري يجب أن يظل كما هو
let array = [1, 2, 3];
array = new Proxy(array, {
get(target, prop, receiver) {
if (prop < 0) {
// even if we access it like arr[1]
// prop هو نص ولذلك نريد أن نحوله
prop = +prop + target.length;
}
return Reflect.get(target, prop, receiver);
}
});
alert(array[-1]); // 3
alert(array[-2]); // 2
قم بإنشاء الدالة makeObservable(target)
والتي تقوم بجعل الأوبجكت observable عن طريق إرجاع بروكسي.
هنا كيف تعمل:
function makeObservable(target) {
/* your code */
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John"; // alerts: SET name=John
بعبارات أخري، الأوبجكت الذي يتم استرجاعه بواسطة makeObservable
هو مثل الأوبجكت الأصلي ولكن يحتوي أيضًا علي الدالة observe(handler)
والتي تنشئ handler
يتم استدعاؤه في كل تغير لخاصية.
عند تغير أي خاصية، يتم استدعاء handler(key, value)
بإسم وقيمة الخاصية.
ملاحظه: في هذا التكليف، اهتم فقط بعملية التعديل علي الخاصية. العمليات الأخري يمكن تطبيقها بنفس الطريقة.
يتكون الحل من جزئين:
- عند استدعاء
.observe(handler)
في أى وقت، نحتاج إلي أن نتذكر الـ handler فس أي مكان، لنكون قادرين علي استدعاءه لاحقًا. يمكن تخزين الـhandler في الأوبجكت باستخدام الرمز كاسم للخاصية. - نحتاج إلي بروكسي يحتوي علي الtrap
set
لاستدعاء الhandlers في حالة التغيير.
let handlers = Symbol('handlers');
function makeObservable(target) {
// 1. Initialize handlers store
target[handlers] = [];
// Store the handler function in array for future calls
target.observe = function(handler) {
this[handlers].push(handler);
};
// 2. Create a proxy to handle changes
return new Proxy(target, {
set(target, property, value, receiver) {
let success = Reflect.set(...arguments); // forward the operation to object
if (success) { // if there were no error while setting the property
// call all handlers
target[handlers].forEach(handler => handler(property, value));
}
return success;
}
});
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John";
الدالّة "Eval" لتنفيذ الشيفرة البرمجية
تنفذ الدالّة Eval
المضمّنة في اللغة الشيفرات البرمجية المُمرّرة لها كسلسلة نصية string
.
وصياغتها هكذا:
let result = eval(code);
فمثلًا:
let code = 'alert("Hello")';
eval(code); // Hello
يمكن أن تكون الشيفرة المُمررة للدالّة كبيرة وتحتوي على فواصل أسطر وتعريف دوالّ ومتغيّرات، وما إلى ذلك.
ولكن نتيجة الدالّة Eval
هي نتيجة آخر عبارة منفذة في الشيفرة.
وإليك المثال التالي:
let value = eval('1+1');
alert(value); // 2
let value = eval('let i = 0; ++i');
alert(value); // 1
تُنفذّ الشيفرة في البيئة الحالية للدالّة، ولذا فيمكنها رؤية المتغيرات الخارجية:
let a = 1;
function f() {
let a = 2;
eval('alert(a)'); // 2
}
f();
كما يمكنها تعديل المتغيّرات الخارجية أيضًا:
let x = 5;
eval("x = 10");
alert(x); // النتيجة: 10، تعدلت القيمة بنجاح
في الوضع الصارم، تملك الدالّة Eval
بيئة متغيّرات خاصة بها. لذا فلن تظهر الدوالّ والمتغيرات، المعرفة -داخل الدالة- للخارج وإنما ستبقى بداخلها:
// تذكر أن في الوضع الصارم يُشغّلُ تلقائيًا في الأمثلة الحيّة
eval("let x = 5; function f() {}");
alert(typeof x); // undefined (المتحول غير مرئي هنا)
// الدالّة f غير مرئية هنا أيضًا
بدون تفعيل “الوضع صارم”، لن يكون للدالّة Eval
بيئة متغيرات خاصة بها، ولذلك سنرى المتغيّر x
والدالّة f
من خارج الدالّة.
استخدامات الدالّة “Eval”
في طرق البرمجة الحديثة، نادرًا ما تستخدم الدالّة Eval
. وغالبًا ما يقال عنها أنها أصل الشرور.
والسبب بسيط: إذ كانت لغة جافاسكربت منذ زمن بعيد أضعف بكثير من الآن، ولم يكُ بالإمكان فعل إيّ شيء إلا باستخدام الدالّة Eval
. ولكن ذلك الوقت مضى عليه عقد من الزمن.
حاليًا، لا يوجد سبب وجيه لاستخدامها. ولو أن شخصًا يستخدمها الآن فلديه الإمكانية لاستبدالها بالبنية الحديثة للغة أو بالوحدات.
لاحظ أن إمكانية وصول الدالة eval
للمتغيرات الخارجية لها عواقب سيئة.
إن عملية تصغير الشيفرة (هي الأدوات تستخدم لتصغير شيفرة الجافاسكربت قبل نشرها وذلك لتصغير حجمها أكثر من ذي قبل) تعيد تسمية المتغيّرات المحلية لأسماء أقصر (مثل a
وb
وما إلى ذلك) لتصغير الشيفرة. وعادةً ما تكون هذه العلمية آمنة، ولكن ليس في حال استخدام الدالّة Eval
، إذ يمكننا الوصول للمتغيّرات المحلية من الشيفرة المُمررة للدالّة. لذا، لن تصغّر المتغيرات التي يحتمل أن تكون مرئية من الدالة Eval
. مما سيُؤثر سلبًا على نسبة ضغط الشيفرة.
يُعدّ استخدام المتغيّرات المحلية في الشيفرة بداخل الدالّة Eval
من الممارسات البرمجية السيئة، لأنه يزيد صعوبة صيانة الشيفرة.
هناك طريقتان لضمان الأمان الكامل عند مصادفتك مثل هذه المشاكل.
إذا لم تستخدم الشيفرة الممررة للدالّة المتغيرات الخارجية، فمن الأفضل استدعاء الدالّة هكذا: window.eval(...)
بهذه الطريقة ستُنفذّ الشيفرة في النطاق العام:
let x = 1;
{
let x = 5;
window.eval('alert(x)'); // 1 (global variable)
}
إن احتاجت الشيفرة الممررة للدالة Eval
لمتغيّرات خارجية، فغيّر Eval
لتصبح new Function
ومرّر المتغير كوسيط. هكذا:
let f = new Function('a', 'alert(a)');
f(5); // 5
شرحنا في مقالٍ سابق تعلمنا كيفية استخدام صياغة «الدالة الجديدة» new Function. إذ باستخدام هذه الصياغة ستُنشأ دالة جديدة من السلسلة (String)، في النطاق العام. لذا لن تتمكن من رؤية المتغيرات المحلية. ولكن من الواضح أن تمريرها المتغيرات صراحة كوسطاء سيحلّ المشكلة، كما رأينا في المثال أعلاه.
خلاصة
سيُشغّل استدعاء الدالّة eval(code)
الشيفرة البرمجية المُمرّرة ويعيد نتيجة العبارة الأخيرة.
- نادرًا ما تستخدم هذه الدالّة في الإصدارات الحديثة للغة، إذ لا توجد حاجة ماسّة لها.
- يمكننا الوصول دائمًا للمتغيّرات الخارجية في الدالّة
eval
. ولكن يعدّ ذلك من الممارسات السيئة. - بدلًا من ذلك يمكننا استخدام الدالة
eval
في النطاق العام، هكذاwindow.eval(code)
. - أو، إذا كانت الشيفرة الخاصة بك تحتاج لبعض البيانات من النطاق الخارجي، فاستخدم صياغة
الدالّة الجديدة
ومرّر لها المتغيرات كوسطاء.
التمارين
آلة حاسبة باستخدام الدالة Eval
الأهمية: 4
أنشئ آلة حاسبة تطالب بتعبير رياضي وتُعيد نتيجته.
لا داعي للتحقق من صحة التعبير في هذا التمرين. فقط قيّم التعبير وأعد نتيجته.
الحل
لنستخدم الدالة eval
لحساب التعبير الرياضي:
let expr = prompt("Type an arithmetic expression?", '2*3+2');
alert( eval(expr) );
يستطيع المستخدم أيضًا إدخال أي نص أو شيفرة.
لجعل الشيفرة آمنة، وحصرها للعمليات الرياضية فحسب، سنتحقق من expr
باستخدام التعابير النمطية، لكي لا تحتوي إلا على الأرقام والمعاملات رياضية.
ترجمة -وبتصرف- للفصل Eval: run a code string من كتاب The JavaScript language
مهمه
Create a calculator that prompts for an arithmetic expression and returns its result.
There’s no need to check the expression for correctness in this task. Just evaluate and return the result.
Let’s use eval
to calculate the maths expression:
let expr = prompt("Type an arithmetic expression?", '2*3+2');
alert( eval(expr) );
The user can input any text or code though.
To make things safe, and limit it to arithmetics only, we can check the expr
using a regular expression, so that it only may contain digits and operators.
تقنية Currying
مفهوم Currying هو تقنية متقدمة للعمل مع الدوالّ. يستخدم في العديد من اللغات البرمجية الآخرى من بينهم جافاسكربت.
Currying عبارة عن طريقة لتحويل الدوالّ التي تقيم الدالّة ذات الاستدعاء-أكثر من وسيط- f (a، b، c)
لتصبح قابلة للاستدعاء -بوسيط واحد- هكذا f(a)(b)(c)
.
تحول تقنية Currying الدالّة فقط ولا تستدعها.
لنرى في البداية مثالًا، لفهم ما نتحدث عنه فهمًا أفضل، وبعدها ننتقل للتطبيقات العملية.
سننشئ دالة مساعدة curry (f)
والّتي ستُنفذّ تقينة Currying على الدالّة f
التي تقبل وسيطين. بتعبير آخر، تحول الدالة curry(f)
الدالّة f(a, b)
ذات الوسيطين إلى دالة تعمل كوسيط واحدf(a)(b)
:
function curry(f) { // الدالةcurry(f) هي من ستُنفذّ تحويل currying
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// طريقة الاستخدام
function sum(a, b) {
return a + b;
}
let curriedSum = curry(sum);
alert( curriedSum(1)(2) ); // 3
كما نرى، فإن التنفيذ بسيط: إنه مجرد مغلفين للوسطاء.
- نتيجة
curry(func)
هي دالة مغلّفةfunction(a)
. - عندما تسمى هكذا
curriedSum(1)
، تُحفظ الوسطاء في البيئة اللغوية للجافاسكربت (وهي نوع مواصفات اللغة تستخدم لتعريف ارتباط المعرّفات بالمتغيّرات والدوالّ المحددة وذلك بناءً على بنية الترابط اللغوية في شيفرة ECMAScript)، وتعيد غلاف جديدfunction(b)
. - ثمّ يُسمى هذا المغلّف باسم
2
نسبةً لوسطائه، ويُمرّر الاستدعاء إلى الدالّةsum(a,b)
الأصليّة.
من الأمثلة المتقدمة باستخدام تقنية currying هو _curry من مكتبة Lodash، والتي تُعيد غِلافًا الّذي يسمح باستدعاء الدالّة طبيعيًا وجزئيًا:
function sum(a, b) {
return a + b;
}
let curriedSum = _.curry(sum); // استخدام _.curry من مكتبة lodash
alert( curriedSum(1, 2) ); // النتيجة: 3، لايزال بإمكاننا استدعاؤه طبيعيًا
alert( curriedSum(1)(2) ); // النتيجة: 3، الاستدعاء الجزئي
لماذا نحتاج لتقنية currying؟
لابد لنا من مثالٍ واقعي لفهم فوائد هذه التقنية.
مثلًا، ليكن لدينا دالّة التسجيل log(date, importance, message)
والّتي ستُنسّق المعلومات وتعرضها. مثل هذه الدوالّ مفيدة جدًا في المشاريع الحقيقة مثل: إرسال السجلات عبر الشبكة، في مثالنا سنستخدم فقط alert
:
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
لنُنفذّ تقنية currying عليها!
log = _.curry(log);
بعد ذلك ستعمل دالّة log
وفق المطلوب:
log(new Date(), "DEBUG", "some debug"); // log(a, b, c)
… ولكنها تعمل أيضًا بعد تحويلها بتقنية currying:
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
الآن يمكننا بسهولة إنشاء دالّة مناسبة للسجلات الحالية:
// logNow سيكون دالة جزئية من log مع وسيط أول ثابت
let logNow = log(new Date());
// استخدامه
logNow("INFO", "message"); // [HH:mm] INFO message
الآن logNow
هو نفس الدالة log
بوسيط أول ثابت، بمعنى آخر “دالّة مطبقة جزئيًا” أو “جزئية” للاختصار.
يمكننا المضي قدمًا وإنشاء دالّة مناسبة لسجلات تصحيح الأخطاء الحالية:
let debugNow = logNow("DEBUG");
debugNow("message"); // [HH:mm] DEBUG message
إذا:
- لم نفقد أي شيء بعد التحويل بتقنية currying: ولا يزال يمكننا أيضًا استدعاء الدالّة
log
طبيعيًا. - يمكننا بسهولة إنشاء دوالّ جزئية مثل: سجلات اليوم.
الاستخدام المتقدم لتقنية currying
في حالة رغبتك في الدخول في التفاصيل، إليك طريقة الاستخدام المتقدمة لتقنية currying للدوالّ ذات الوسطاء المتعددة والّتي يمكننا استخدامها أعلاه.
وهي مختصرة جدًا:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
إليك مثالًا لطريقة استخدامه:
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying
تبدو تقنية currying للوهلة الأولى معقدةً، ولكنها في الحقيقة سهلة الفهم جدًا.
نتيجة استدعاء curry(func)
هي دالّة مُغلّفة curried
والّتي تبدو هكذا:
// func is the function to transform
function curried(...args) {
if (args.length >= func.length) { // (1)
return func.apply(this, args);
} else {
return function pass(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
عند تشغيله، هناك فرعين للتنفيذ من الجملة الشرطية if
:
- سيكون الاستدعاء الآن هكذا: إن كان عدد الوسطاء
args
المُمرّرة هو نفس العدد الدالة الأصليّة المعرّفة لدينا (func.length
) أو أكثر، عندها نمرّر الاستدعاء له فقط. - وإلا سيكون الاستدعاء جزئيًا: لم تُستدعى الدالّة
func
بعد. وإنما أعيد بدلًا منها دالّة المغلِّفة أخرىpass
، والتي ستُعيد تطبيق الدالةcurried
مع تقديم الوسطاء السابقين مع الوسطاء الجدد. وثمّ في استدعاء الجديد سنحصل إما على دالة جزئية جديدة (إن لم يكُ عدد الوسطاء كافيا) أو النتيجة النهائية.
لنرى مثلًا ما يحدث في حال الاستدعاء الدالة هكذا sum(a, b, c)
. أي بثلاث وسطاء، وبذلك يكون sum.length = 3
.
عند استدعاء curried(1)(2)(3)
:
- الاستدعاء الأول
curried (1)
تحفظ1
في بيئته اللغوية، ويُعيد دالّة المغلفpass
. - يُستدعى المغلّف
pass
مع الوسيط المُمرّر(2)
: إذ يأخذ الوسطاء السابقين (1
)، ويدمجهم مع الوسيط الذي حصل عليه وهو(2)
ويستدعي الدالّةcurried(1, 2)
مع استخدام جميع ما حصل عليه من وسطاء. وبما أن عدد الوسطاء لا يزال أقل من 3 ، فإن الدالّةcurry
ستُعيد الدالّةpass
. - يُستدعى المغلّف
pass
مرة أخرى مع الوسيط المُمرّر(3)
: ومن أجل الاستدعاء التاليpass (3)
سيأخذ الوسطاء السابقين (1
,2
) ويضيف لهم الوسيط3
، ليكون الاستدعاء هكذاcurried(1, 2, 3)
- أخيرًا لدينا ثلاث وسطاء، والّذين سيمرّرون للدالّة الأصلية. إذا لم تتوضح الفكرة حتى الآن، فما عليك إلا تتبع تسلسل الاستدعاءات في عقلك أو على الورقة وستتوضح الأمور أكثر.
ملاحظة: تعمل مع الدوالّ ثابتة الطول فقط
يجب أن يكون للدالّة عدد ثابت من الوسطاء لتطبيق تقنية currying.
إن استخدمت دالّةّ ما معاملات البقية، مثل: f(...args)
، فلا يمكن معالجتها بهذه التقنية.
ملاحظة: أكثر بقليل من مجرد تقنية تحويل
انطلاقًا من التعريف، يجب على تقنية currying تحويل الدالّة sum(a, b, c)
إلى sum(a)(b)(c)
.
لكن غالبية تطبيقات هذه التقنية في جافاسكربت متقدمة، وكما وضحنا سابقًا: فهي تحافظ على الدالّة قابلة للاستدعاء بعدة تنويعات للوسطاء المُمرّرة.
خلاصة
تقنية Currying هو عملية تحويل تجعل f(a,b,c)
قابلة للاستدعاء كـ f(a)(b)(c)
. عادةً ما تحافظ تطبيقات الجافاسكربت على الدوالّ بحيث تكون قابلة للاستدعاء بالشكل الطبيعي أو الجزئي إن كان عدد الوسطاء غير كافٍ.
كما تسمح لنا هذه التقنية أيضًا بالحصول على دوالّ جزئية بسهولة. كما رأينا في مثال التسجيل، بعد تنفيذ هذه التقنية على الدالّة العالمية ذات الثلاث وسطاء log(date, importance, message)
فإن ذلك سيمنحنا دوالّ جزئية عند استدعاؤها باستخدام وسيط واحد (هكذا log(date)
) أو وسيطين (هكذا log(date, importance)
).
ترجمة -وبتصرف- للفصل Currying من كتاب The JavaScript language
النوع المرجعي
هذه المقالة تقوم بتغطية موضوع متقدم, لفهم بعض الحالات بشكل أفضل.
إنها ليست مهمة. يعيش العديد من المطورين ذوي الخبرة بشكل جيد دون معرفة ذلك. تابع القراءة إذا كنت تريد معرفة كيفية عمل الأشياء خلف الكواليس.
قد تفقد استدعاء تابع تم تقييمه بشكل ديناميكي this
.
علي سبيل المثال:
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
user.hi(); // تعمل
// الآن دعونا نقوم بتشغيل user.hi أو user.bye بناءً علي الإسم
(user.name == "John" ? user.hi : user.bye)(); // Error!
على السطر الأخير يوجد عامل شرطي يختار إما user.hi
أو user.bye
. فى هذه الحالة تكون النتيجة user.hi
.
ثم يتم استدعاء التابع على الفور بين قوسين ()
. و لكنه لم يعمل بشكل صحيح!
كما ترى, تشغيل التابع أحدث خطأ, بسبب أن نتيجة "this"
داخل تشغيل التابع أنتج undefined
.
هذا يعمل (كائن نقطة تابع):
user.hi();
هذا لا يعمل:
(user.name == "John" ? user.hi : user.bye)(); // خطأ!
لماذ ؟ إذا كنا نريد ان نفهم لماذا يحدث هذا, دعونا نري خلف الكواليس كيف يعمل ()obj.method
.
تفسير النوع المرجعي
عند التدقيق, ربما نلاحظ وجود عمليتين علي عبارة ()obj.method
:
- اولاً, النقطة
'.'
تجب الخاصيةobj.method
. - ثانياً الأقواس
()
تقوم بتشغيلها.
لذا, كيف يمكن للمعلومات الخاصة بـ this
ان تمر من الجزء الأول الى الجزء الثاني?
إذا وضعنا هذه العمليات على خطوط منفصلة, اذا this
سوف نقوم بفقدها بالتأكيد:
let user = {
name: "John",
hi() { alert(this.name); }
}
// تقسيم الحصول على واستدعاء التابع في سطرين
let hi = user.hi;
hi(); // خطأ, لأن this غير معرفة
هنا hi = user.hi
يضع التابع في المتغير, ثم في السطر الأخير يكون مستقلاً تماماً, و في هذه الحالة لا يوجد this
.
لجعل ()user.hi
تعمل, جافاسكريبت تستخدم خدعة – النقطة '.'
لا تيعد تابع, و لكن قيمه من المميز Reference Type.
النوع المرجعي هو “نوع المواصفات”. لا يمكننا استخدامها صراحة, و لكن يتم استخدامها داخلياً بواسطة اللغه.
قيمة النوع المرجعي هي مزيج من ثلاث قيم (base, name, strict)
, حيث:
base
الكائن.name
إسم الخاصية.strict
تكون صحيحة اذاuse strict
تعمل.
النتيجة من إستخدام user.hi
لا يكون تابع, و لكن قيمة من النوع المرجعي. user.hi
في الوضع الصارم تكون:
// قيمة النوع المرجعي
(user, "hi", true)
حيث الأقواس ()
تسمى النوع المرجعي, يتلقون المعلومات الكاملة حول الكائن و توابعه, و يمكن وضع القيمه الصحيحة لـ this
(=user
في هذه الحالة).
النوع المرجعي هو نوع داخلي خاص “وسيط”, بغرض تمرير المعلومات من النقطة .
الي طلب الأقواس ()
.
اى عملية اخري مثل hi = user.hi
تتجاهل النوع المرجعي بالكامل, تأخذ القيمة من user.hi
(التابع) و تقوم بتمريره. اذا اى من العمليات المستقبلية “تفقد” this
.
لذا, قيمة this
يتم تمريرها بالطريقة الصحيحة فقط إذا تم استدعاء التابع مباشرةً باستخدام نقطة obj.method()
أو الاقواس المربعة obj['method']()
(يقومون بنفس الوظيفه هنا). لاحقًا في هذا البرنامج التعليمي ، سنتعلم طرقًا مختلفة لحل هذه المشكلة مثل func.bind().
الملخص
النوع المرجعي هو نوع داخلي من اللغة.
قراءة خاصية ، كما هو الحال مع النقطة .
في obj.method()
لا يعيد قيمة الخاصية بالضبط, و لكن “النوع المرجعي” كلاً من قيمة الخاصية والكائن الذي تم أخذها منه.
هذا لاستدعاء الطريقة اللاحقة ()
للوصل الى الكائن و وضع قيمة this
بها.
بالنسبة لجميع العمليات الأخرى ، يصبح النوع المرجعي تلقائيًا قيمة الخاصية (تابع في حالتنا).
جميع آليات العمل مختفيه. لا يهم إلا في الحالات الدقيقة, مثل عندما يتم الحصول على طريقة ديناميكيًا من الكائن ، باستخدام تعبير.
نتيجة النقطة .
ليست في الواقع طريقة ، ولكنها قيمة `` يحتاج إلى طريقة لتمرير المعلومات حول obj
مهمه
ما هي نتيجة هذه الشيفرة?
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)()
ملاحظة. هناك مأزق :)
خطأ!
حاول هذا:
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)() // خطأ!
لا تعطينا رسالة الخطأ في معظم المتصفحات الكثير من الأدلة حول الخطأ الذي حدث.
يظهر الخطأ لأن فاصلة منقوطة مفقودة بعد user = {...}
.
لا تقوم جافاسكريبت بإدراج فاصلة منقوطة تلقائيًا قبل قوس ()(user.go)
, إنها تقوم بقراءة الشيفرة هكذا:
let user = { go:... }(user.go)()
ثم يمكننا أن نرى أيضًا أن هذا التعبير المشترك هو عبارة عن استدعاء للكائن { go: ... }
كتابع مع متغير (user.go)
. وهذا يحدث أيضًا على نفس السطر مع let user
, لذلك user
لم يتم حتى الآن تعريف الكائن ، ومن هنا كان الخطأ.
إذا أدخلنا الفاصلة المنقوطة ، فكل شيء على ما يرام:
let user = {
name: "John",
go: function() { alert(this.name) }
};
(user.go)() // John
يرجى ملاحظة أن الأقواس حول (user.go)
لا تفعل شيئ هنا.عادة ما يقومون بإعداد ترتيب العمليات ، ولكن هنا النقطة
.
تعمل أولاً على أي حال, لذلك ليس هناك تأثير. فقط الشيء الفاصلة المنقوطة هو المهم.
في الشيفرة بالأسفل نريد تنفيذ obj.go()
4 مرات.
لكن تنفيذ (1)
و (2)
يكون مختلف عن تنفيذ (3)
4 (4)
. لماذا?
let obj, method;
obj = {
go: function() { alert(this); }
};
obj.go(); // (1) [object Object]
(obj.go)(); // (2) [object Object]
(method = obj.go)(); // (3) undefined
(obj.go || obj.stop)(); // (4) undefined
هنا يكون التفسير.
-
هذا هو استدعاء طريقة الكائن المعتاد.
-
نفس الشيء ، الأقواس لا تغير ترتيب العمليات هنا ، النقطة أولاً على أي حال.
-
هنا لدينا تنفيذ اكثر تعقيداً
(expression).method()
. التنفيذ يعمل كما لو كان مقسوم الى سطرين:f = obj.go; // حساب المصطلح f(); // تنفيذ ما لدينا
هنا
f()
يتم تنفيذها كـ تابع, بدونthis
. -
نفس الشيئ في
(3)
, ايسر النقطة.
لدينا مصطلح.
لتفسير سلوك (3)
و (4)
نريد إعادة تنفيذ مدخلات الخاصية (نقطة او اقواس مربعة) تعيد قيمة النوع المرجعي.
اى عملية عليها عدا تنفيذ التابع (مثل =
or ||
) يحولها إلى قيمة عادية ، لا تحمل المعلومات التي تسمح بتعيينها this
.
BigInt
BigInt
هو نوع رقمي خاص يوفر الدعم للأعداد الصحيحة ذات الطول التعسفي.
لإنشاء قيمة من النوع BigInt
، يجب عليك إضافة n
إلى نهاية الحرف الرقمي أو استدعاء الدالة BigInt
، والتي ستنشئ عددًا من النوع BigInt
من الوسيطة التي تم تمريرها. يمكن أن تكون الوسيطة رقمًا أو سلسلة ، إلخ.
const bigint = 1234567890123456789012345678901234567890n;
const sameBigint = BigInt("1234567890123456789012345678901234567890");
const bigintFromNumber = BigInt(10); // 10n مثلها
عوامل الرياضيات
BigInt
يمكن استخدامها في الغالب مثل رقم عادي ، على سبيل المثال:
alert(1n + 2n); // 3
alert(5n / 2n); // 2
يرجى ملاحظة : أن القسمة 5/2
ترجع النتيجة مقربة إلى الصفر بدون الجزء العشري. جميع العمليات على BigInts
ترجع BigInts
.
لا يمكننا مزج الأرقام الكبيرة والأرقام العادية:
alert(1n + 2); // Error: Cannot mix BigInt and other types
يجب علينا تحويلها بشكل صريح إذا لزم الأمر: باستخدام BigInt()
أو Number()
، مثل هذا :
let bigint = 1n;
let number = 2;
// bigint تحويل number إلى
alert(bigint + BigInt(number)); // 3
// number تحويل bigint إلى
alert(Number(bigint) + number); // 3
يحدث تحويل bigint
إلى رقم دائمًا بشكل ضمني وبدون توليد أخطاء ، ولكن إذا كانت قيمة bigint
كبيرة جدًا ولا تتناسب مع نوع الرقم ، فسيتم تجاهل bits
الإضافية ، لذلك يجب توخي الحذر في مثل هذه التحويلات.
bigints
يعد عامل تشغيل الزائد الأحادي +value
طريقة معروفة لتحويل value
إلى رقم.
لا يتم دعم عامل التشغيل هذا عند العمل مع أرقام BigInt
:
let bigint = 1n;
alert( +bigint ); // error
لذلك يجب أن نستخدم Number()
لتحويل bigint
إلى رقم.
المقارنات
تعمل المقارنات ، مثل <،> مع الأحرف الكبيرة والأرقام كالمعتاد:
alert( 2n > 1n ); // true
alert( 2n > 1 ); // true
يرجى ملاحظة أن الأرقام المنتظمة والأرقام الكبيرة تنتمي إلى أنواع مختلفة ، يمكن أن تكون متساوية فقط مع مقارنة ضيقة ==
,ولكنها ليست متساوية تمامًا ===
:
alert( 1 == 1n ); // true
alert( 1 === 1n ); // false
العمليات المنطقية
عندما تكون داخل if
أو العمليات المنطقية الأخرى ، يتصرفbigints
مثل الأرقام.
على سبيل المثال ، في if
، تكون قيمة bigint0n
خاطئة ، والقيم الأخرى صحيحة:
if (0n) {
// لا ينفذ أبداً
}
تعمل عوامل التشغيل المنطقية ، مثل ||
و & &
وغيرها مع bigint
المشابهة للأرقام
:
alert( 1n || 2 ); // 1 (1n ستكون `true`)
alert( 0n || 2 ); // 2 (0n ستكون `false`)
تعدد الأشكال
تعدد أشكال bigints
صعب. والسبب هو أن العديد من عوامل تشغيل JavaScript
، مثل+
،-
وما إلى ذلك تتصرف بشكل مختلف باستخدام bigints
مقارنة بالأرقام العادية.
على سبيل المثال ، يؤدي قسمة رقم bigint
إلى إرجاع bigint
(يتم تقريبه إذا لزم الأمر).
لمحاكاة مثل هذا السلوك ، سيحتاج الملء المتعدد إلى تحليل الشفرة واستبدال جميع هذه العوامل بوظائفها. لكن القيام بذلك أمر مرهق وسيكلف الكثير من الأداء.
لذا ، لا يوجد “تعدد أشكال” جيد معروف.
على الرغم من ذلك هناك حل عكسي اقترحه مطورو مكتبة JSBI .
تقوم هذه المكتبة بتنفيذ أعداد كبيرة باستخدام أساليبها الخاصة. يمكننا استخدامها بدلاً من “bigints” الأصلية :
العملية | الأصلية BigInt |
JSBI |
---|---|---|
إنشاء من رقم | a = BigInt(789) |
a = JSBI.BigInt(789) |
الإضافة | c = a + b |
c = JSBI.add(a, b) |
الطرح | c = a - b |
c = JSBI.subtract(a, b) |
… | … | … |
… ثم استخدم “تعدد أشكال” (مكوّن Babel الإضافي) لتحويل مكالمات JSBI إلى bigint
الأصلية لتلك المتصفحات التي تدعمها
.
بعبارة أخرى ، يقترح هذا النهج استخدام JSBI بدلاً من BigInt
المدمج. تعمل JSBI داخليًا مع الأرقام كما هو الحال مع BigInt
، تحاكيها وفقًا لجميع متطلبات المواصفات. وبالتالي ، يمكننا تنفيذ كود JSBI في المترجمين الذين لا يدعمون Bigint
، وبالنسبة لأولئك الذين يدعمون ، يقوم تعدد الأشكال بتحويل المكالمات إلى Bigint
العادية…
يمكننا استخدام رمز JSBI هذا “كما هو” للمحركات التي لا تدعم bigint
وتلك التي تدعم – سيقوم تعدد الأشكال بتحويل المكالمات إلى bigint
الأصلية
.