قصة مشكلة: أقصر مذكرة جافا سكريبت

الصورة


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


كتابة مفكرة - وظيفة الديكور التي تحفظ نتائج تنفيذ دالة ملفوفة لمنع تكرار العمليات الحسابية. لديك 50 حرفًا فقط.

اللغة ، بالطبع ، هي JavaScript . المهمة نفسها هي مهمة كلاسيكية ، لكن الحد الأقصى المكون من 50 حرفًا أصبح تحديًا حقيقيًا.


في فواصل اليوم الأول من المؤتمر ، ناقشنا الخيارات لتحقيق الهدف ، وتقليل الاستجابة تدريجياً. توجت كل هذه الضجة بفكرة مشاركة المهمة مع جميع المشاركين في المؤتمر ، وفي اليوم الثاني تصورنا المهمة (انظر الملحق) وبدأنا في توزيع النماذج على أولئك الذين أرادوا. ونتيجة لذلك ، حصلنا على حوالي 40 حلًا وأصبحنا مقتنعين مرة أخرى بالمجتمع الاستثنائي لمطوري js ، لكن سجل Dmitry Kataev (SEMrush) المكون من 53 حرفًا بقي. دعونا نكتشف ذلك!


التنفيذ المعتاد


function memoize(f) { let cache = {}; return function ret() { let key = JSON.stringify(arguments); if (!cache.hasOwnProperty(key)) { cache[key] = f.apply(this, arguments); } return cache[key]; } } 

النتيجة: 190 حرفًا تقريبًا


  • Memoize - Memoizer لدينا
  • و - وظيفة مزخرفة ومغلفة
  • دالة ناتجة

للحصول على الإجابة - حجم الوظيفة - نستخدم:


 memoize.toString().replace(/\s+/g, ' ').length 

عند تقييم حجم دالة ، نولي اهتماما لجسمها وقائمة المعلمات. إذا كانت الوظيفة مجهولة ، فلن يؤخذ الإعلان في الاعتبار.


اختبارات بسيطة لاختبار الصحة بعد الإساءة:


 const log = memoize(console.log); const inc = memoize(o => ox + 1); 

لا.استدعاء دالةنتيجة التنفيذ في وحدة التحكم
1.log(false)> خطأ
2.log('2', {x:1})> "2" ، {x: 1}
3.log(false)لا شيء ، حيث تم تنفيذ الوظيفة بالفعل لهذه القيم.
4.log('2', {x:1})لا شيء ، حيث تم تنفيذ الوظيفة بالفعل لهذه القيم.
5.inc({x:1})2
6.inc({x:2})3

بعد ذلك ، سيتم تمييز نتيجة كل تنفيذ بنتيجة الاختبار.


التنفيذ الصافي


بادئ ذي بدء ، أريد التخلص من إعلان الوظيفة لصالح وظيفة السهم ، نظرًا لأننا لسنا مهتمين بهذا السياق ، فنحن لا نناشد الحجج ، وبصفتنا مُنشئ لا ننوي الاتصال به من خلال جديد . في الوقت نفسه ، سنقوم بتخفيض أسماء المتغيرات المحلية المستخدمة:


 const memoize = f => { let c = {}; return function() { let k = JSON.stringify(arguments); if (!c.hasOwnProperty(k)) { c[k] = f.apply(this, arguments); } return c[k]; } } 

النتيجة: 154 ، اجتياز الاختبارات


ثم يمكننا تنفيذ عملية مماثلة مع الدالة الناتجة ، ولكننا بحاجة إلى الحجج . هنا يأتي عامل الانتشار إلى الإنقاذ ، مما يسمح لنا باستبدال الكائن القابل للتكرار للحجج بمتغير الصفيف a . بالإضافة إلى ذلك ، لن نقوم بتمرير هذا السياق إلى الوظيفة التي يتم تزيينها: إذا لزم الأمر ، Function.prototype.bind أو بوليفيل سيساعدنا.


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); if (!c.hasOwnProperty(k)) { c[k] = f(...a); } return c[k]; } } 

النتيجة: 127 ، اجتازت الاختبارات


ننتقل الآن إلى نص الدالة الناتجة. من الواضح أن العثور على المفتاح في ذاكرة التخزين المؤقت وإرجاع القيمة أمر مرهق. لنحاول تقليل الطريقة:


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return c[k] || (c[k] = f(...a)); } } 

النتيجة: 101 ، انخفض الاختباران 3 و 4


هنا نتخلى عن طريقة hasOwnProperty . يمكننا تحمل ذلك ، لأن نتيجة إجراء تسلسل لمجموعة من الحجج عبر JSON.stringify ستكون دائمًا "[...]" ومن غير المحتمل أن تظهر هذه الخاصية في ذاكرة التخزين المؤقت للنموذج الأولي ( Object ).


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


وهنا سقطنا الاختبارين 3 و 4. حدث هذا لأن وحدة التحكم بالوظيفة المزخرفة لا تعرض قيمة: ستكون النتيجة غير محددة . نضع هذا في ذاكرة التخزين المؤقت ، وعندما نحاول التحقق من ميزة الانفصال عندما نطلق عليها مرة أخرى ، يتم عرض خطأ ضمنيًا في المعامل الأول ، وبالتالي نصل إلى الثانية ، مما يؤدي إلى استدعاء الوظيفة. سيحدث هذا التأثير لجميع النتائج التي تم اختزالها إلى false : 0 ، "" ، null ، NaN ، إلخ.


بدلاً من OR وإذا كانت العبارة ، يمكننا استخدام عامل ثلاثي مشروط:


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return c.hasOwnProperty(k) ?c[k] :c[k] = f(...a); } } 

النتيجة: 118 اجتازت الاختبارات


تخفيض طفيف جدا. ولكن ماذا لو كنت تستخدم الخريطة كمخزن بدلاً من كائن بسيط؟ هناك أيضا طريقة قصيرة:


 const memoize = f => { let c = new Map; return (...a) => { let k = JSON.stringify(a); return (c.has(k) ?c :c.set(k, f(...a))).get(k); } } 

النتيجة: 121 اجتازت الاختبارات


فشل الحد تماما. لكن تجاهل الخريطة على الفور لا يستحق ذلك. يسمح لك هذا التطبيق بتخزين القيمة الأساسية باستخدام الكائنات كمفتاح. وهذا يعني ، هل يجب أن نتخلى عن JSON.stringify على الإطلاق؟


 const memoize = f => { let c = new Map; return (...a) => (c.has(a) ?c :c.set(a, f(...a))).get(a); } 

النتيجة: 83 ، انخفض الاختباران 3 و 4


تبدو واعدة جدا! ومع ذلك ، بدأ الاختباران 3 و 4 في الانخفاض مرة أخرى ، وذلك لأن مقارنة المفاتيح في كائن الخريطة يتم تنفيذها باستخدام خوارزمية SameValueZero . إذا حذفت التفاصيل باستخدام NaN و -0 و 0 ، فستعمل بالمثل مع عامل المقارنة الصارم ( === ). ولدينا مجموعة جديدة من الحجج (وبالتالي كائن) لكل استدعاء للدالة الملتفة ، حتى بنفس القيم. تتم المقارنة وفقًا لمرجع الكائن ، وبالتالي لن تجد طريقة Map.prototype.has أي شيء.


وبالتالي ، فإن استخدام الخريطة لم يقلل من قدرتنا على الملكية أو JSON.stringify .


في عامل يأتي الإنقاذ ، الذي يتحقق من وجود خاصية في كائن أو في سلسلة من نماذجها الأولية. تم شرح سبب عدم الخوف من البحث في النماذج الأولية أعلاه.


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return k in c ?c[k] :c[k] = f(...a); } } 

النتيجة: 105 اجتازت الاختبارات


يتكون نص كل من المذكرة والوظيفة الناتجة من تعبيرين مع الحاجة إلى تعريف المتغير المحلي وتهيئته قبل المنطق في بيان الإرجاع . هل من الممكن اختزال نص دالة السهم إلى تعبير واحد هنا؟ بالطبع ، باستخدام نمط IIFE ( التعبير عن الوظيفة التي يتم استدعاؤها فورًا ):


 const memoize = f => (c => (...a) => (k => k in c ?c[k] : c[k] = f(...a))(JSON.stringify(a)) )({}); 

النتيجة: 82 اجتازت الاختبارات


حان الوقت للتخلص من المساحات الإضافية:


 f=>(c=>(...a)=>(k=>k in c?c[k]:c[k]=f(...a))(JSON.stringify(a)))({}); 

النتيجة: 68 اجتياز الاختبارات


من الواضح أن عنق الزجاجة الآن هو طريقة JSON.stringify الطويلة ، التي تقوم بتسلسل الكائن بشكل متكرر إلى سلسلة JSON ، والتي نستخدمها كمفتاح. في الواقع ، نحن لا نحتاج إلى وظيفة تسلسل ، ولكن وظيفة تجزئة يمكننا من خلالها التحقق من المساواة بين الكائنات ، لأنها تعمل بلغات أخرى. ولكن ، لسوء الحظ ، لا يوجد حل أصلي في JavaScript ، ومن الواضح أن polyphile المكتوب ذاتيًا على hashCode في النموذج الأولي للكائن خارج النطاق.


همم ، لماذا علينا أن نسلسل أنفسنا؟ عند إضافة عنصر إلى كائن بواسطة المفتاح ، سيتم استدعاء toString بشكل ضمني. نظرًا لأننا رفضنا استخدام كائن الوسيطات القابلة للتكرار لصالح الصفيف من خلال عامل الانتشار ، فلن يتم استدعاء toString من Object.prototype ، ولكن من Array.prototype ، حيث يتم إعادة تعريفه وفصل عناصره بفواصل. وهكذا ، للحصول على مجموعة مختلفة من الحجج نحصل على مفتاح مختلف.


 f=>(c=>(...a)=>a in c?c[a]:c[a]=f(...a))({}); 

النتيجة: 44 ، انخفض الاختبار 6


الاختبار 6 بدأ للتو في الانخفاض. يبدو أن القيمة المرتجعة هي نتيجة استدعاء دالة سابق في الاختبار 5. لماذا يحدث هذا؟ نعم ، تجاوزنا المكالمة إلى String لكائن الوسيطات ، لكننا لم نأخذ في الاعتبار أن أي وسيطة يمكن أن تكون أيضًا كائنًا معقدًا ، حيث ننادي toString التي نحصل منها على [كائن الكائن] المفضل لدى الجميع. هذا يعني أن الوسيطات {x: 1} و {x: 2} ستستخدم نفس المفتاح في التجزئة.


بدا btoa المستخدم للتحويل إلى base64 منافسًا جيدًا لوظيفة التسلسل. لكنه يقود أولاً إلى الخيط ، لذلك لا توجد فرصة. فكرنا في اتجاه إنشاء URI ، وتشكيل ArrayBuffer ، أي وظائف للحصول على تجزئة أو قيمة متسلسلة. لكنهم بقوا في مكانهم.


بالمناسبة ، JSON.stringify له خصائصه الخاصة: Infinity ، NaN ، غير محدد ، سيتم طرح الرمز إلى قيمة خالية . وينطبق الشيء نفسه على الوظائف. إذا أمكن ، يحدث استدعاء ضمني لـ JSON من الكائن ، وسيتم تمثيل الخريطة والتعيين بواسطة العناصر التي تم تعدادها ببساطة. إنه مفهوم ، بالنظر إلى التنسيق النهائي: JSON.


ماذا بعد ذلك؟


تعديل سامة


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


أولاً ، لماذا لا تبدأ ذاكرة التخزين المؤقت على النحو التالي:


 (f,c={})=>(...a)=>(k=>k in c?c[k]:c[k]=f(...a))(JSON.stringify(a)); 

النتيجة: 66 اجتياز الاختبارات


هنا نستخدم المعلمة الافتراضية في وظيفة السهم. بالطبع ، نعطي العميل الفرصة لضبط ذاكرة التخزين المؤقت ، فماذا؟ لكننا قللنا حرفين.


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


 f=>(...a)=>(k=>k in f?f[k]:f[k]=f(...a))(JSON.stringify(a)); 

النتيجة: 59 اجتازت الاختبارات


هنا سوف تحمينا JSON.stringify من التقاطع مع الخصائص والأساليب الأخرى للكائن (الوظيفة) ، مع التفاف الوسيطات في "[...]".


في هذه اللحظة بالذات ، لم يعد نموذج IIFE الذي تم تطبيقه سابقًا يبرر نفسه. ولكن هناك حاجة ماسة إلى الاحتفاظ بتعبير واحد لوظيفة السهم لتجنب عبارة الإرجاع :


 f=>(...a)=>(k=JSON.stringify(a),k in f?f[k]:f[k]=f(...a)); 

النتيجة: 57 اجتياز الاختبارات


نظرًا لأننا لا نستخدم عبارة block في دالة السهم ، فلا يمكننا الإعلان عن متغير ( var أو let ) ، ولكن يمكننا استخدام السياق العام - التأثير الجانبي! هنا الصراع لديه بالفعل بعض الفرص ليكون.


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


 f=>(...a)=>(k=JSON.stringify(a))in f?f[k]:f[k]=f(...a); 

النتيجة: 54 اجتياز الاختبارات


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


وأخيرًا:


 f=>(...a)=>f[k=JSON.stringify(a)]=k in f?f[k]:f(...a); 

النتيجة: 53 اجتازت الاختبارات


لماذا لا تحسب المفتاح عند الوصول إلى القيمة. وبعد ذلك - نفس العامل الثلاثي والمهمة. المجموع: 53 حرفا!


هل من الممكن إزالة الأحرف الثلاثة المتبقية؟


الفهم


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


  • تعبير دالة السهم
  • تحديد نطاق المعجم و IIFE
  • كائن الحجج الشبيهة بالصفيف
  • الانتشار أو الفاصلة أو عوامل التشغيل
  • عامل مقارنة صارم
  • JSON.stringify & toString
  • في المشغل و hasOwnProperty
  • عامل تجميع وبيان كتلة
  • كائن الخريطة
  • وشيء آخر

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


التطبيق


الصورة


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

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


All Articles