اقتراح "حقول الحقول" أو "الخطأ الذي حدث في ارتكاب tc39"

كلنا منذ فترة طويلة نريد تغليفًا عاديًا في JS ، والذي يمكن استخدامه بدون إيماءات غير ضرورية. نريد أيضًا إنشاءات مناسبة للإعلان عن خصائص الطبقة. وأخيرًا ، نريد أن تظهر كل هذه الميزات في اللغة بطريقة لا تؤدي إلى كسر التطبيقات الحالية.


يبدو أن الأمر هنا هو السعادة: اقتراح فئة الصفوف ، والذي بعد سنوات عديدة من عذاب لجنة tc39 لا يزال يتعين عليه الوصول إلى stage 3 وحتى الحصول على التنفيذ في الكروم .


بصراحة ، أود حقًا كتابة مقال حول سبب استخدام ميزة اللغة الجديدة وكيفية القيام بذلك ، ولكن للأسف ، لن تكون المقالة حول ذلك على الإطلاق.


وصف التيار المفقود


لن أكرر الوصف الأصلي والأسئلة الشائعة والتغييرات في المواصفات هنا ، ولكن فقط بإيجاز النقاط الرئيسية.


حقول الصف


تحديد الحقول واستخدامها داخل الفصل:


 class A { x = 1; method() { console.log(this.x); } } 

الوصول إلى الحقول خارج الفصل:


 const a = new A(); console.log(ax); 

بدا كل شيء واضحًا ولسنوات عديدة كنا نستخدم هذه البنية باستخدام Babel و TypeScript .


فقط هناك فارق بسيط. يستخدم هذا التركيب الجديد [[Define]] ، وليس [[Set]] الدلالات التي عشنا معها طوال هذا الوقت.


من الناحية العملية ، هذا يعني أن الرمز أعلاه لا يساوي هذا:


 class A { constructor() { this.x = 1; } method() { console.log(this.x); } } 

لكن في الواقع هذا يعادل :


 class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } } 

وعلى الرغم من أنه في المثال أعلاه ، فإن كلا النهجين يفعلان نفس الشيء بشكل أساسي ، وهذا اختلاف خطير للغاية ، وإليك السبب:


لنفترض أن لدينا فئة أبوين مثل هذا:


 class A { x = 1; method() { console.log(this.x); } } 

بناءً على ذلك ، أنشأنا آخر:


 class B extends A { x = 2; } 

وقد استخدموه:


 const b = new B(); b.method(); //   2   

ثم ، لسبب ما ، تم تغيير الفئة A بطريقة متوافقة مع السابقة:


 class A { _x = 1; //  ,   ,        get x() { return this._x; }; set x(val) { return this._x = val; }; method() { console.log(this._x); } } 

وبالنسبة إلى دلالات [[Set]] ، يعد هذا بالفعل تغييرًا متوافقًا للخلف ، ولكن ليس لـ [[Define]] . الآن سيتم إخراج المكالمة إلى b.method() إلى وحدة التحكم 1 بدلاً من 2 . وسيحدث هذا لأن Object.defineProperty يعيد تعريف واصف الخاصية ، وبالتالي ، لن يتم استدعاء getter / setter من الفئة A في الواقع ، في الصف التابع ، قمنا بإخفاء خاصية x الخاصة بالأب ، على غرار كيف يمكننا القيام بذلك في النطاق المعجمي:


 const x = 1; { const x = 2; } 

صحيح ، في هذه الحالة ، سيوفر لنا no-shadow بقواعده no-shadowed-variable / no-shadow ، ولكن احتمالية أن يقوم شخص ما بعمل حقل no-shadowed-class-field يميل إلى الصفر.


بالمناسبة ، سأكون ممتنًا للمصطلح الروسي الأكثر نجاحًا في shadowed .

على الرغم من كل ما سبق ، فأنا لست معارضا غير مقبول للدلالات الجديدة (على الرغم من أنني أفضل الآخر) ، لأن لها جوانبها الإيجابية الخاصة. ولكن ، للأسف ، لا تفوق هذه المزايا أهم ناقص - لقد استخدمنا دلالات [[Set]] لسنوات عديدة ، لأنه يتم استخدامها في babel6 و TypeScript افتراضيًا.


صحيح ، تجدر الإشارة إلى أنه في babel7 تم تغيير القيمة الافتراضية .

يمكن قراءة المزيد من المناقشات الأصلية حول هذا الموضوع هنا وهنا .


الحقول الخاصة


والآن سننتقل إلى الجزء الأكثر إثارة للجدل في هذا الجزء. مثير للجدل لدرجة:


  1. على الرغم من حقيقة أنه تم تنفيذه بالفعل في Chrome Canary وتم تمكين الحقول العامة بالفعل بشكل افتراضي ، إلا أن الحقول الخاصة لا تزال خلف العلامة ؛
  2. على الرغم من حقيقة أن الدمج الأولي للحقول الخاصة تم دمجه مع الحقل الحالي ، إلا أنه لا يزال يتم إنشاء الطلبات لفصل هاتين الميزتين (على سبيل المثال ، واحد ، اثنان ، ثلاثة وأربعة ) ؛
  3. حتى بعض أعضاء اللجنة (مثل Allen Wirfs-Brock و Kevin Smith ) يتحدثون ويقدمون بدائل ، على الرغم من المرحلة 3 ؛
  4. لقد فوت هذا الرقم القياسي لعدد المشكلات - 129 في المستودع الحالي + 96 في الأصل ، مقابل 126 لـ BigInt ، ولدى صاحب التسجيل تعليقات سلبية في الغالب ؛
  5. اضطررت إلى إنشاء موضوع منفصل لمحاولة تلخيص جميع المطالبات ضده بطريقة أو بأخرى ؛
  6. كان علي أن أكتب أسئلة متكررة منفصلة تغطي هذا الجزء
    ومع ذلك ، بسبب حجة ضعيفة إلى حد ما ، ظهرت مثل هذه المناقشات ( واحد ، اثنان )
  7. أنا شخصياً قضيت كل وقت فراغي (وأحيانًا أعمل) لفترة طويلة من الوقت لمعرفة كل شيء وحتى العثور على تفسير لماذا كان هكذا أو تقديم بديل مناسب ؛
  8. في النهاية ، قررت كتابة مقال المراجعة هذا.

يتم إعلان الحقول الخاصة على النحو التالي:


 class A { #priv; } 

والوصول إليها كما يلي:


 class A { #priv = 1; method() { console.log(this.#priv); } } 

لن أثير حتى الموضوع القائل بأن النموذج العقلي وراء هذا ليس بديهيًا للغاية ( this.#priv !== this['#priv'] ) ، لا يستخدم الكلمات private / protected المحجوزة بالفعل (والتي ستتسبب بالضرورة في ألم إضافي لمطوري TypeScript) ، ليس من الواضح كيفية توسيعه لمعدلات الوصول الأخرى ، والصيغة نفسها ليست جميلة جدًا. على الرغم من أن كل هذا كان السبب الأصلي الذي دفعني إلى دراسة أعمق والمشاركة في المناقشات.


كل هذا يتعلق ببناء الجملة ، حيث تكون التفضيلات الجمالية الذاتية قوية جدًا. ويمكن للمرء العيش معه والتعود عليه بمرور الوقت. إن لم يكن لشيء واحد: هناك مشكلة كبيرة للغاية في علم الدلالة ...


دلالات WeakMap


دعونا نلقي نظرة على ما وراء الاقتراح الحالي. يمكننا إعادة كتابة المثال أعلاه بالتغليف وبدون استخدام بناء الجملة الجديد ، ولكن مع الحفاظ على دلالات الصيغة الحالية:


 const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } } 

بالمناسبة ، على أساس هذا الدلالات ، قام أحد أعضاء اللجنة ببناء مكتبة صغيرة للمرافق تسمح لك باستخدام الدولة الخاصة الآن ، من أجل إظهار أن هذه الوظيفة مبالغ فيها للغاية من قبل اللجنة. يأخذ الرمز المنسق 27 سطرا فقط.

بشكل عام ، كل شيء جيد جدًا ، نحصل على hard-private ، والتي لا يمكن الحصول عليها / اعتراضها / تتبعها بأي شكل من التعليمات البرمجية الخارجية ، وفي الوقت نفسه يمكننا الوصول إلى الحقول الخاصة لمثيل آخر من نفس الفئة ، على سبيل المثال مثل هذا:


 isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; } 

حسنًا ، هذا مريح للغاية ، باستثناء حقيقة أن هذه الدلالات ، بالإضافة إلى التغليف نفسه ، تتضمن أيضًا brand-checking (لا يمكنك البحث عن Google - فمن غير المحتمل أن تجد معلومات ذات صلة).
brand-checking هو عكس duck-typing ، بمعنى أنه لا يتحقق من الواجهة العامة للكائن ، ولكن حقيقة أن الكائن تم إنشاؤه باستخدام رمز موثوق به.
في الواقع ، هذا الاختيار له نطاق معين - فهو مرتبط بشكل أساسي بأمان استدعاء التعليمات البرمجية غير الموثوق بها في مساحة عنوان واحدة مع عنوان موثوق به والقدرة على تبادل الكائنات مباشرة بدون تسلسل.


على الرغم من أن بعض المهندسين يعتبرون هذا جزءًا ضروريًا من التغليف المناسب.

على الرغم من حقيقة أن هذه فرصة غريبة إلى حد ما ، والتي ترتبط ارتباطًا وثيقًا بنمط (وصف قصير وأطول ) ، Realms الدعاية والعمل العلمي في مجال علوم الكمبيوتر ، والتي يشارك فيها مارك صموئيل ميللر (وهو أيضًا عضو في اللجنة) ، في تجربتي ، في ممارسة معظم المطورين ، لا يحدث هذا تقريبًا.


بالمناسبة ، ما زلت صادفت غشاء (على الرغم من أنني لم أكن أعرف ما كان عليه آنذاك) عندما أعيدت كتابة vm2 لتناسب احتياجاتي.

مشكلة brand-checking


كما ذكرنا سابقًا ، brand-checking هو عكس duck-typing . من الناحية العملية ، هذا يعني أن وجود هذا الرمز:


 const brands = new WeakMap(); class A { constructor() { brands.set(this, {}); } method() { return 1; } brandCheckedMethod() { if (!brands.has(this)) throw 'Brand-check failed'; console.log(this.method()); } } 

لا يمكن استدعاء brandCheckedMethod إلا بمثيل من الفئة A وحتى إذا كان الهدف هو كائن يخزن الثوابت من هذه الفئة ، فإن هذه الطريقة ستؤدي إلى استثناء:


 const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method(); //        1 duckTypedObj.brandCheckedMethod(); //      

من الواضح أن هذا المثال اصطناعي تمامًا واستخدام duckTypedObj مثل هذا duckTypedObj مشكوك فيه ، حتى نفكر في Proxy .
واحدة من سيناريوهات استخدام الوكيل الهامة للغاية هي metaprogramming. من أجل قيام الوكيل بجميع الأعمال المفيدة اللازمة ، يجب تنفيذ طرق الكائنات التي يتم لفها باستخدام وكيل في سياق الوكيل ، وليس في سياق الهدف ، أي:


 const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); doSomethingUseful('get', retval, target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } }); 

استدعاء proxy.method(); ستقوم بعمل مفيد معلن في الوكيل وإرجاع 1 ، أثناء استدعاء proxy.brandCheckedMethod(); بدلاً من القيام بعمل مفيد مرتين من الوكيل ، فإنه سيتم استثناء ، لأن a !== proxy ، مما يعني أن brand-check لم يمر.


نعم ، يمكننا تنفيذ الأساليب / الوظائف في سياق هدف حقيقي ، وليس وكيل ، وهذا كافٍ لبعض السيناريوهات (على سبيل المثال ، لتنفيذ نمط ) ، ولكن هذا لا يكفي لجميع الحالات (على سبيل المثال ، لتنفيذ خصائص تفاعلية: MobX 5 يستخدم بالفعل وكيلًا لهذا ، تقوم Vue.js و Aurelia بتجربة هذا النهج للإصدارات المستقبلية).


بشكل عام ، طالما أن brand-check يجب أن يتم بشكل صريح ، فهذه ليست مشكلة - يحتاج المطور فقط بوعي إلى تحديد المقايضة التي يقوم بها وما إذا كان يحتاج إليها ، علاوة على ذلك ، في حالة brand-check الصريح من brand-check يمكنك تنفيذه بطريقة لن يتم طرح الخطأ على الوكلاء الموثوق بهم.


لسوء الحظ ، لقد حرمنا التيار الحالي من هذه المرونة:


 class A { #priv; method() { this.#priv; //    brand-check   } } 

ستقوم هذه method دائمًا بإلقاء استثناء إذا لم يتم استدعاؤها في سياق كائن تم إنشاؤه باستخدام المُنشئ A وأسوأ جزء هو أن brand-check ضمني هنا ومختلط مع وظيفة أخرى - التغليف.


في حين أن ضروري تقريبًا لأي رمز ، brand-check له نطاق ضيق إلى حد ما. سيؤدي دمجها في صيغة واحدة إلى حقيقة أن الكثير من brand-check غير المقصودة brand-check تظهر في رمز المستخدم ، عندما كان المطور يخطط فقط لإخفاء تفاصيل التنفيذ.
والشعار المستخدم للترويج لهذا كان # is the new _ يفاقم الوضع فقط.


يمكنك أيضًا قراءة مناقشة تفصيلية حول كيفية كسر prozal الحالي لبروكسي . تحدث أحد مطوري Aurelia والمؤلف Vue.js في المناقشة .

أيضًا ، تعليقي ، الذي يصف بمزيد من التفصيل الفرق بين سيناريوهات الوكيل المختلفة ، قد يبدو مثيرًا للاهتمام لشخص ما. ككل ، مناقشة كاملة لربط الحقول الخاصة والأغشية .

البدائل


كل هذه المناقشات لن تكون ذات معنى إذا لم تكن هناك بدائل. لسوء الحظ ، لم يكن هناك بديل واحد حتى في المرحلة 1 ، ونتيجة لذلك ، لم يكن لديه حتى فرصة للعمل بشكل كاف. ومع ذلك ، سأذكر هنا بدائل تحل بطريقة ما المشكلات الموضحة أعلاه.


  1. Symbol.private - بروزازيل بديل من أعضاء اللجنة.
    1. إنه يحل جميع المشاكل المذكورة أعلاه (على الرغم من أنه قد يكون له مشاكله الخاصة ، ولكن نظرًا لعدم وجود عمل نشط عليه ، فمن الصعب العثور عليها)
    2. تم إعادته مرة أخرى في الاجتماع الأخير للجنة بسبب عدم وجود brand-check متكامل brand-check ، ومشاكل في نمط الغشاء (على الرغم من أن هذا + يقدم حلاً مناسبًا) وعدم وجود بنية ملائمة
    3. يمكن بناء جملة مريحة على رأس الفعلية ، كما بينت هنا وهنا
  2. الفئات 1.1 - posozal في وقت سابق من نفس المؤلف
  3. استخدام الخاص ككائن

بدلا من الاستنتاج


بنبرة المقال ، ربما يبدو أنني أدين اللجنة - الأمر ليس كذلك. يبدو لي فقط أنه على مر السنين (اعتمادًا على نقطة البداية ، قد تكون عقودًا) عملت اللجنة على تغليف في شبيبة ، تغير الكثير في الصناعة ، ويمكن أن يصبح المظهر غير واضح ، مما أدى إلى تصنيف خاطئ للأولويات .


علاوة على ذلك ، نحن كمجتمع ، ندفع tc39 لإجبارهم على إطلاق الميزات بشكل أسرع ، مع إعطاء القليل من الملاحظات في المراحل المبكرة من prozos ، مما يقلل من سخطنا فقط في وقت لا يمكن فيه تغيير الكثير.


ويعتقد أنه في هذه الحالة ، فشلت العملية ببساطة.


بعد غمرها في رأسي والتحدث مع بعض الممثلين ، قررت أنني سأبذل قصارى جهدي لمنع تكرار مثل هذا الموقف - ولكن يمكنني أن أفعل القليل (اكتب مقال مراجعة ، اجعل تنفيذ stage1 في babel وكل شيء).


لكن الأهم هو ردود الفعل - لذا أطلب منك المشاركة في هذا الاستبيان الصغير. وسأحاول ، بدوره ، نقله إلى اللجنة.

Source: https://habr.com/ru/post/ar428119/


All Articles