تمت كتابة الكثير من المقالات الرائعة حول نموذج الكائن في JavaScript. وحول الطرق المختلفة لإنشاء أعضاء صف خاص على الإنترنت مليء بالأوصاف الجديرة بالاهتمام. ولكن حول الأساليب المحمية - هناك القليل من البيانات. أود أن أسد هذه الفجوة وأخبر كيف يمكنك إنشاء طرق محمية بدون مكتبات في JavaScript ECMAScript 5 فقط.
في هذه المقالة:
رابط إلى مستودع git-hub مع شفرة المصدر والاختبارات. لماذا هناك حاجة لأعضاء الفئة المحمية
باختصار ، إذن
- من الأسهل فهم عمل الفصل والعثور على الأخطاء فيه. (يمكنك أن ترى على الفور في الحالات التي يتم فيها استخدام أعضاء الفصل. إذا كانت خاصة ، فأنت بحاجة إلى تحليل هذه الفئة فقط ، جيدًا ، وإذا كانت محمية ، فعندئذ فقط هذه الفئات والفئات المشتقة منها.)
- أسهل لإدارة التغيير. (على سبيل المثال ، يمكنك إزالة أعضاء من القطاع الخاص دون خوف من كسر شيء ما خارج الفصل القابل للتحرير.)
- يتم تقليل عدد التطبيقات في تعقب الأخطاء ، لأن يمكن لمستخدمي المكتبة أو التحكم "خياطة" الأعضاء "الخاصين" ، والذي قررنا إزالته في الإصدار الجديد من الفصل ، أو تغيير منطق عملهم.
- وبشكل عام ، يعد أعضاء الفئة المحمية أداة تصميم. من الجيد أن يكون في متناول اليد واختبار جيد.
دعني أذكرك بأن الفكرة الرئيسية للأعضاء المحميين هي إخفاء الأساليب والخصائص عن مستخدمي مثيل الفصل ، ولكن في نفس الوقت السماح للفصول المشتقة بالوصول إليها.
لن يسمح استخدام TypeScript باستدعاء الأساليب المحمية ، ومع ذلك ، بعد التجميع في JavaScript ، يصبح جميع الأعضاء الخاصين والمحميين عامين. على سبيل المثال ، نقوم بتطوير عنصر تحكم أو مكتبة يقوم المستخدمون بتثبيتها على مواقعهم أو تطبيقاتهم. سيكون هؤلاء المستخدمون قادرين على فعل ما يريدون مع الأعضاء المحميين ، منتهكين سلامة الفصل. نتيجة لذلك ، تعقّب أداة تتبع الأخطاء لدينا شكاوى من أن مكتبتنا أو عنصر التحكم لدينا لا يعمل بشكل صحيح. نحن نقضي الوقت والجهد لفرزها -
"هل هذه هي الطريقة التي كان بها الكائن في حالة العميل ، مما أدى إلى حدوث خطأ؟!" . لذلك ، من أجل تسهيل الحياة على الجميع ، هناك حاجة إلى هذه الحماية التي لن تجعل من الممكن تغيير معنى أفراد الطبقة الخاصة والمحمية.
ما تحتاجه لفهم الطريقة المعنية
لفهم طريقة إعلان أعضاء الصف المحميين ، تحتاج إلى معرفة قوية:
- فئات الجهاز والكائنات في JavaScript.
- طرق لإنشاء أعضاء الصف الخاص (على الأقل من خلال الإغلاق).
- الأساليب Object.defineProperty و Object.getOwnPropertyDescriptor
حول طراز الجهاز في JavaScript ، يمكنني أن أوصي ، على سبيل المثال ، بمقالة ممتازة كتبها Andrey Akinshin (
DreamWalker )
"فهم OOP في JS [الجزء رقم 1]" .
فيما يتعلق بالممتلكات الخاصة ، هناك وصف جيد ، وفي رأيي ، كامل إلى حد ما
لما يصل إلى 4 طرق مختلفة لإنشاء أعضاء فئة خاصة على موقع MDN.
بالنسبة لأسلوب Object.defineProperty ، سيسمح لنا بإخفاء الخصائص والأساليب من الحلقات for-in ، ونتيجة لذلك ، من خوارزميات التسلسل:
function MyClass(){ Object.defineProperty(MyClass.prototype, 'protectedNumber', { value: 12, enumerable: false }); this.publicNumber = 25; }; var obj1 = new MyClass(); for(var prop in obj1){ console.log('property:' prop);
يجب أن يتم هذا الإخفاء ، ولكن هذا ، بالطبع ، لا يكفي لأنه لا تزال هناك إمكانية لاستدعاء الطريقة / الخاصية مباشرة:
console.log(obj1.protectedNumber);
مساعد فئة المحميين خطأ
أولاً ، نحتاج إلى فئة ProtectedError ، التي ترث من الخطأ ، والتي سيتم طرحها إذا لم يكن هناك وصول إلى الطريقة أو الملكية المحمية.
function ProtectedError(){ this.message = "Encapsulation error, the object member you are trying to address is protected."; } ProtectedError.prototype = new Error(); ProtectedError.prototype.constructor = ProtectedError;
تنفيذ أعضاء الفئة المحمية في ES5
الآن بعد أن أصبح لدينا فئة ProtectedError ونفهم ما يفعله Object.defineProperty بالقيمة التي يمكن عدها: خطأ ، دعنا نحلل إنشاء فئة أساسية تريد مشاركة طريقة المحمية مع جميع فئاتها المشتقة ، ولكن نخفيها عن أي شخص آخر:
function BaseClass(){ if (!(this instanceof BaseClass)) return new BaseClass(); var _self = this;
وصف مُنشئ فئة BaseClass
ربما ستشوشك الشيكات:
if (!(this instanceof BaseClass)) return new BaseClass();
هذا الاختبار هو "هواة". يمكنك إزالته ؛ لا علاقة له بالطرق المحمية. ومع ذلك ، أنا شخصيا أتركه في رمز بلدي ، لأنه هناك حاجة لتلك الحالات عندما لا يتم إنشاء مثيل الفصل بشكل صحيح ، أي بدون الكلمة جديدة. على سبيل المثال ، مثل هذا:
var obj1 = BaseClass();
في مثل هذه الحالات ، افعل ما يحلو لك. يمكنك ، على سبيل المثال ، إنشاء خطأ:
if (!(this instanceof BaseClass)) throw new Error('Wrong instance creation. Maybe operator "new" was forgotten');
أو يمكنك ببساطة إنشاء مثيل بشكل صحيح ، كما هو الحال في BaseClass.
بعد ذلك ، نقوم بحفظ المثال الجديد في المتغير _self (لماذا أحتاج إلى شرح ذلك لاحقًا).
وصف الممتلكات العامة المسماة المحمية
عند إدخال الطريقة ، نسمي فحص السياق الذي تم الاتصال به. من الأفضل أن تقوم بإيداع طريقة منفصلة ، على سبيل المثال ، checkAccess ، لأن ستكون هناك حاجة إلى نفس الفحص في جميع الأساليب والخصائص المحمية للفئات. لذا ، بادئ ذي بدء ، تحقق من نوع سياق الاستدعاء لهذا. إذا كان هذا يحتوي على نوع آخر بخلاف BaseClass ، فلن يكون النوع هو BaseClass نفسه ولا أي من مشتقاته. نحن نحظر مثل هذه المكالمات.
if(!(this instanceof BaseClass)) throw new ProtectedError();
كيف يحدث هذا؟ على سبيل المثال ، مثل هذا:
var b = new BaseClass(); var someObject = {}; b.protectedMethod.call(someObject);
في حالة الفئات المشتقة ، سيكون تعبير هذا المثيل لـ BaseClass صحيحًا. ولكن بالنسبة لمثيلات BaseClass ، سيكون هذا المثال من تعبير BaseClass صحيحًا. لذلك ، لتمييز مثيلات فئة BaseClass عن مثيلات الفئات المشتقة ، نتحقق من المُنشئ. إذا كان المُنشئ يطابق BaseClass ، فإن الطريقة المحمية لدينا تسمى مثيل BaseClass ، تمامًا مثل الطريقة العامة العادية:
var b = new BaseClass(); b.protectedMethod();
نحن نحظر مثل هذه المكالمات:
if(this.constructor === BaseClass) throw new ProtectedError();
يأتي بعد ذلك استدعاء طريقة المحمية المغلقة ، والتي ، في الواقع ، هي الطريقة التي نحميها. داخل الطريقة ، إذا كنت بحاجة إلى الرجوع إلى أعضاء فئة BaseClass ، يمكنك القيام بذلك باستخدام المثيل المخزن لـ _self. هذا هو بالضبط ما تم إنشاؤه _ نفسي للوصول إلى أعضاء الفصل من جميع الأساليب الخاصة / الخاصة. لذلك ، إذا كنت لا تحتاج إلى الوصول إلى أعضاء الفئة في طريقتك أو ملكيتك المحمية ، فلا يمكنك إنشاء المتغير _self.
استدعاء طريقة محمية داخل فئة BaseClass
داخل فئة BaseClass ، يجب الوصول إلى ProtectMethod بالاسم فقط ، وليس من خلال ذلك. خلاف ذلك ، داخل الطريقة المحمية لا يمكننا التمييز بين ما إذا كنا قد تم استدعاؤنا كطريقة عامة أو من داخل فئة. في هذه الحالة ، ينقذنا الإغلاق - الطريقة المحمية التي تتصرف مثل طريقة خاصة عادية ، مغلقة داخل الفصل ولا تكون مرئية إلا في نطاق وظيفة BaseClass.
وصف فئة DerivedClass المشتقة
الآن دعونا نلقي نظرة على فئة مشتقة وكيفية جعلها في متناول طريقة محمية لفئة أساسية.
function DerivedClass(){ var _base = { protectedMethod: this.protectedMethod.bind(this) }; function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); }
وصف مُنشئ فئة مشتقة
في الفئة المشتقة ، نقوم بإنشاء كائن _base نضع فيه مرجعًا لطريقة المحمية من الفئة الأساسية ، مغلقًا لسياق الفئة المشتقة من خلال طريقة الربط القياسية. هذا يعني أن استدعاء _base.protectedMethod ()؛ هذا داخل كائن محمي ليس كائن _base ، بل مثيل لفئة DerivedClass.
وصف طريقة ProtectedMethod داخل DerivedClass
في فئة DerivedClass ، من الضروري التصريح عن الطريقة العامة المحمية في الطريقة نفسها كما فعلنا في الفئة الأساسية عبر Object.defineProperty والتحقق من الوصول إليها عن طريق استدعاء طريقة checkAccess أو عن طريق التحقق مباشرة في الطريقة:
Object.defineProperty(DerivedClass.prototype, 'protectedMethod', { enumerable: false, configurable: false, value: function(){ if(this.constructor === DerivedClass) throw new ProtectedError() return _base.protectedMethod(); } });
نتحقق -
"ولكن هل تم استدعاؤنا كوسيلة عامة بسيطة؟" بالنسبة لحالات فئة DerivedClass ، سيكون المُنشئ مساوياً لـ DerivedClass. إذا كان الأمر كذلك ، فقم بإنشاء خطأ. خلاف ذلك ، نرسلها إلى الفئة الأساسية وستقوم بالفعل بإجراء جميع الفحوصات الأخرى.
لذلك ، في الفئة المشتقة ، لدينا وظيفتان. تم تعريف واحد عبر Object.defineProperty وهو مطلوب للفئات المشتقة من DerivedClass. إنه عام وبالتالي لديه شيك يحظر المكالمات العامة. توجد الطريقة الثانية في كائن _base ، الذي يتم إغلاقه داخل فئة DerivedClass وبالتالي فهو غير مرئي لأي شخص من الخارج ويتم استخدامه للوصول إلى الطريقة المحمية من جميع أساليب DerivedClass.
حماية الملكية
مع الخصائص ، يحدث العمل بشكل مختلف قليلاً. يتم تعريف الخصائص في BaseClass كالمعتاد من خلال Object.defineProperty ، فقط في المراسلات والمستوطنين تحتاج أولاً إلى إضافة الشيك الخاص بنا ، أي تحقق من المكالمة
function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return _protectedProperty; }, set: function (value) { checkAccess.call(this); _protectedProperty = value; }, enumerable: false, configurable: false }); }
داخل فئة BaseClass ، لا يتم الوصول إلى الخاصية المحمية من خلال هذا ، ولكن إلى المتغير المغلق _protectedProperty. إذا كان من المهم بالنسبة لنا أن يعمل كل من getter و setter عند استخدام الخاصية داخل فئة BaseClass ، فإننا نحتاج إلى إنشاء طرق خاصة getProtectedPropety و setProtectedProperty ، حيث لن تكون هناك عمليات تحقق ، ويجب أن يتم استدعاؤهم بالفعل.
function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return getProtectedProperty(); }, set: function (value) { checkAccess.call(this); setProtectedProperty(value); }, enumerable: false, configurable: false }); function getProtectedProperty(){
في الفئات المشتقة ، يكون العمل مع الخصائص أكثر تعقيدًا ، لأن لا يمكن استبدال الخاصية بالسياق. لذلك ، سوف نستخدم طريقة Object.getOwnPropertyDescriptor القياسية للحصول على getter و setter من خاصية الفئة الأساسية كوظائف يمكن استخدامها بالفعل لتغيير سياق المكالمة:
function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty');
وصف الميراث
وآخر شيء أود التعليق عليه هو ميراث DerivedClass من BaseClass. كما تعلم ، DerivedClass.prototype = new BaseClass ()؛ لا يخلق نموذجًا أوليًا فحسب ، بل يعيد أيضًا كتابة خاصية المُنشئ. وبسبب هذا ، لكل مثيل من DerivedClass ، تصبح خاصية المُنشئ مساوية لـ BaseClass. لإصلاح ذلك ، عادةً بعد إنشاء نموذج أولي ، أعد كتابة خاصية المُنشئ:
DerivedClass.prototype = new BaseClass(); DerivedClass.prototype.constructor = DerivedClass;
ومع ذلك ، حتى لا يعيد أحد كتابة هذه الخاصية بعدنا ، فإننا نستخدم نفس Object.defineProperty. الخاصية القابلة للتكوين: خطأ خاصية يمنع تجاوز الخاصية مرة أخرى:
DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false });