موضوعات متنوعة وهامة في الجافا سكربت |JavaScript misc

الوسيط Proxy و الـReflect

الكائن Proxy يُحيط كائن آخر و يتوسط العمليات التى تُجري على هذا الكائن، مثل القراءة أو التعديل على الخصائص وغيرها والتعامل معهم بشكل اختياري أو تجعل الكائن الأساسي يتعامل معهم دون تدخل.

إن الـProxies مُستخدمة فى كثير من المكتبات وبعض أطر العمل. سنرى الكثير من التطبيقات العملية فى هذا المقال.

الوسيط Proxy

الشكل:

let proxy = new Proxy(target, handler);
  • target – هو الكائن الذي يتم إحاطته ويمكن أن يكون أى شيئ حتي الدوال (functions).
  • handler – إعدادت الـproxy: هو كائن يحتوي علي “traps” والتي هي عبارة عن دوال تعمل فى العمليات. مثلًا الـ get trap لقراءة خاصية من الـobject target وكذلك set trap لتعديل\إضافة خاصية للـobject target وهكذا.

بالنسبة للعمليات فى الـ 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.

  1. عملية التعديل proxy.test= تُعدل القيمة فى الـtarget.
  2. عملية القراءة proxy.test تقوم بإرجاع القيمة من target.
  3. التكرار على الـ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 – شبيه بالJtrap get، ولكن مفيد فقط للخصائص التي يتم التعديل عليها.

يجب أن يقوم ال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”`!

ماذا حدث؟ من الممكن أننا فعلنا شيئًا خاطئًا مع الوراثة؟

ولكن إذا قمنا بإزالة البروكسي، سيعمل كل شيئ كما هو متوقع.

المشكلة تحديدًا في البروكسي في السطر (*).

  1. عند قراءة admin.name فإن الأوبجكت admin ليس لديه خاصية كهذه فيذهب البحث إلي الـprototype المتصل به.

  2. الprototype هو userProxy.

  3. عند قراءة الخاصية name من البروكسي، فإن الtrap get يُشغل ويُرجع قيمتها من الأوبجكت ألأصلي كما 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) بإسم وقيمة الخاصية.

ملاحظه: في هذا التكليف، اهتم فقط بعملية التعديل علي الخاصية. العمليات الأخري يمكن تطبيقها بنفس الطريقة.

يتكون الحل من جزئين:

  1. عند استدعاء .observe(handler) في أى وقت، نحتاج إلي أن نتذكر الـ handler فس أي مكان، لنكون قادرين علي استدعاءه لاحقًا. يمكن تخزين الـhandler في الأوبجكت باستخدام الرمز كاسم للخاصية.
  2. نحتاج إلي بروكسي يحتوي علي ال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

مهمه

الأهمية: 4

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

إذا:

  1. لم نفقد أي شيء بعد التحويل بتقنية currying: ولا يزال يمكننا أيضًا استدعاء الدالّة log طبيعيًا.
  2. يمكننا بسهولة إنشاء دوالّ جزئية مثل: سجلات اليوم.

الاستخدام المتقدم لتقنية 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:

  1. سيكون الاستدعاء الآن هكذا: إن كان عدد الوسطاء args المُمرّرة هو نفس العدد الدالة الأصليّة المعرّفة لدينا (func.length) أو أكثر، عندها نمرّر الاستدعاء له فقط.
  2. وإلا سيكون الاستدعاء جزئيًا: لم تُستدعى الدالّة func بعد. وإنما أعيد بدلًا منها دالّة المغلِّفة أخرى pass، والتي ستُعيد تطبيق الدالة curried مع تقديم الوسطاء السابقين مع الوسطاء الجدد. وثمّ في استدعاء الجديد سنحصل إما على دالة جزئية جديدة (إن لم يكُ عدد الوسطاء كافيا) أو النتيجة النهائية.

لنرى مثلًا ما يحدث في حال الاستدعاء الدالة هكذا sum(a, b, c)‎. أي بثلاث وسطاء، وبذلك يكون sum.length = 3.

عند استدعاء curried(1)(2)(3)‎:

  1. الاستدعاء الأول curried (1)‎ تحفظ 1 في بيئته اللغوية، ويُعيد دالّة المغلف pass.
  2. يُستدعى المغلّف pass مع الوسيط المُمرّر (2): إذ يأخذ الوسطاء السابقين (1)، ويدمجهم مع الوسيط الذي حصل عليه وهو (2) ويستدعي الدالّة curried(1, 2)‎ مع استخدام جميع ما حصل عليه من وسطاء. وبما أن عدد الوسطاء لا يزال أقل من 3 ، فإن الدالّة curry ستُعيد الدالّة pass.
  3. يُستدعى المغلّف 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:

  1. اولاً, النقطة '.' تجب الخاصية obj.method.
  2. ثانياً الأقواس () تقوم بتشغيلها.

لذا, كيف يمكن للمعلومات الخاصة بـ 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

مهمه

الأهمية: 2

ما هي نتيجة هذه الشيفرة?

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) لا تفعل شيئ هنا.عادة ما يقومون بإعداد ترتيب العمليات ، ولكن هنا النقطة . تعمل أولاً على أي حال, لذلك ليس هناك تأثير. فقط الشيء الفاصلة المنقوطة هو المهم.

الأهمية: 3

في الشيفرة بالأسفل نريد تنفيذ 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

هنا يكون التفسير.

  1. هذا هو استدعاء طريقة الكائن المعتاد.

  2. نفس الشيء ، الأقواس لا تغير ترتيب العمليات هنا ، النقطة أولاً على أي حال.

  3. هنا لدينا تنفيذ اكثر تعقيداً (expression).method(). التنفيذ يعمل كما لو كان مقسوم الى سطرين:

    f = obj.go; // حساب المصطلح
                                  f();        // تنفيذ ما لدينا

    هنا f() يتم تنفيذها كـ تابع, بدون this.

  4. نفس الشيئ في (3), ايسر النقطة . لدينا مصطلح.

لتفسير سلوك (3) و (4) نريد إعادة تنفيذ مدخلات الخاصية (نقطة او اقواس مربعة) تعيد قيمة النوع المرجعي.

اى عملية عليها عدا تنفيذ التابع (مثل = or ||) يحولها إلى قيمة عادية ، لا تحمل المعلومات التي تسمح بتعيينها this.

-------

BigInt

إضافة حديثة
هذه إضافة حديثة إلى اللغه. يمكنك العثور على الحالة الحالية للدعم في https://caniuse.com/#feat=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 الأصلية .

المراجع