مسلية JavaScript: بدون أقواس متعرجة

الصورة


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


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


المهمة السابقة:



الصياغة


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

عداد المكالمات مجرد عذر ، لأن هناك console.count () . خلاصة القول هي أن وظيفتنا تجمع بعض البيانات عند استدعاء وظيفة ملفوفة وتوفر واجهة معينة للوصول إليها. يمكن حفظ جميع نتائج المكالمة ، وجمع السجلات ، ونوع من الحفظ. مجرد مواجهة بدائية ومفهومة للجميع.


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


حل معتاد


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


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

هذا لا يناسبنا على الفور ، حيث:


  1. في JavaScript ، لا يمكنك تنفيذ خاصية خاصة بهذه الطريقة: يمكننا قراءة مكالمات المثيل (التي نحتاجها) وكتابة القيمة من الخارج (التي لا نحتاجها). بالطبع ، يمكننا استخدام الإغلاق في المنشئ ، ولكن ما معنى الصف؟ وسأظل أخشى استخدام حقول خاصة جديدة بدون بابل 7 .
  2. تدعم اللغة نموذجًا وظيفيًا ، ولا يبدو أن إنشاء مثيل من خلال جديد هو الحل الأفضل هنا. من الأفضل كتابة دالة تُرجع دالة أخرى. نعم فعلا!
  3. أخيرًا ، لن يسمح لنا بناء جملة ClassDeclaration و MethodDefinition بالتخلص من جميع الأقواس المتعرجة.

ولكن لدينا نمط وحدة رائع يطبق الخصوصية باستخدام الإغلاق:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

يمكنك العمل بالفعل مع هذا.


قرار ترفيهي


لماذا تستخدم الأقواس هنا؟ هذه 4 حالات مختلفة:


  1. تعريف نص دالة العد (إعلان الوظيفة )
  2. تهيئة الكائن المرتجع
  3. تعريف نص دالة الاستدعاء ( FunctionExpression ) مع تعبيرين
  4. تحديد نص دالة getCalls ( FunctionExpression ) بتعبير واحد

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


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

من الرائع العمل مع هذا:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

النقطة الرابعة واضحة: نحن فقط نستبدل FunctionExpression بـ ArrowFunction . سيزودنا غياب الأقواس المتعرجة بسجل قصير لوظيفة السهم في حالة تعبير واحد في جسمه:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

مع الثالث - كل شيء أكثر تعقيدًا. تذكر أن أول شيء فعلناه هو استبدال FunctionExpression بوظائف الاستدعاء بـ FunctionDeclaration df . لإعادة كتابة هذا الأمر إلى ArrowFunction ، يجب حل مشكلتين: عدم فقدان الوصول إلى الوسيطات (وهي الآن مجموعة زائفة من الحجج ) وتجنب النص الوظيفي لتعبرين.


سيتم مساعدة المشكلة الأولى من خلال معلمات args المحددة بواسطة الوظيفة مع عامل الانتشار . ولدمج تعبيرين في تعبير واحد ، يمكنك استخدام AND المنطقي . على عكس عامل الاقتران المنطقي الكلاسيكي الذي يُرجع Boolean ، فإنه يحسب المعاملات من اليسار إلى اليمين إلى "false" الأول ويعيدها ، وإذا كانت كلها "true" - فإن القيمة الأخيرة. ستعطينا الزيادة الأولى للعداد 1 ، مما يعني أن هذا التعبير الفرعي سيتم دائمًا إلغاؤه إلى الحقيقة. إن الاختزال إلى "الحقيقة" نتيجة استدعاء الوظيفة في التعبير الفرعي الثاني لا يهمنا: في أي حال ، تتوقف الآلة الحاسبة عندها. الآن يمكننا استخدام ArrowFunction :


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

يمكنك تزيين سجل قليلاً باستخدام زيادة البادئة:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

سيبدأ حل النقطة الأولى والأكثر صعوبة من خلال استبدال FunctionDeclaration بـ ArrowFunction . لكن لا يزال لدينا الجسم في أقواس متعرجة:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

إذا أردنا التخلص من الأقواس المتعرجة التي تأطير جسم الوظيفة ، فسيتعين علينا تجنب الإعلان عن المتغيرات وتهيئتها من خلال let . ولدينا متغيرين كاملين: مكالمات و df .


أولاً ، دعنا نتعامل مع العداد. يمكننا إنشاء متغير محلي من خلال تعريفه في قائمة معلمات الدوال ، ونقل القيمة الأولية عن طريق استدعاؤه باستخدام IIFE (التعبير عن وظيفة تم استدعاؤها فورًا):


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

يبقى لربط التعابير الثلاثة في واحد. نظرًا لأن لدينا جميع التعبيرات الثلاثة التي تمثل الوظائف التي يمكن اختزالها دائمًا إلى true ، يمكننا أيضًا استخدام المنطقي AND :


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

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


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

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


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

وهكذا يتحقق الهدف.


الاختلافات في السمة


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


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


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

ومع ذلك ، إذا كانت الحجة f ليست دالة ، فيجب علينا أن نلقي استثناءً بطريقة جيدة. ولا يمكن طرح استثناء الرمي في سياق التعبير. يمكنك الانتظار للحصول على تعبيرات (المرحلة 2) والمحاولة مرة أخرى. أو هل لدى شخص بالفعل أفكار الآن؟


أو فكر في فصل يصف إحداثيات نقطة:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

والتي يمكن تمثيلها بوظيفة:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

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


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


الخلاصة


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

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


All Articles