بصفتي مطور JS ، هذا ما يبقيني مستيقظًا في الليل
نشرت: 2022-03-11JavaScript هو غريب الأطوار للغة. على الرغم من أنها مستوحاة من Smalltalk ، إلا أنها تستخدم بناء جملة يشبه الحرف C. فهو يجمع بين جوانب نماذج البرمجة الإجرائية والوظيفية والموجهة للكائنات (OOP). لديها العديد من الأساليب ، وغالبًا ما تكون زائدة عن الحاجة ، لحل أي مشكلة برمجة يمكن تصورها تقريبًا ولا يتم إبداء رأي قوي بشأنها. إنه مكتوب بشكل ضعيف وديناميكي ، مع نهج متاهل لكتابة الإكراه الذي يتعثر حتى المطورين ذوي الخبرة.
تحتوي JavaScript أيضًا على الثآليل والفخاخ والميزات المشكوك فيها. يعاني المبرمجون الجدد من بعض مفاهيمه الأكثر صعوبة - التفكير في عدم التزامن ، والإغلاق ، والرفع. المبرمجون ذوو الخبرة في لغات أخرى يفترضون بشكل معقول أن الأشياء ذات الأسماء والمظاهر المتشابهة ستعمل بنفس الطريقة في JavaScript وغالبًا ما تكون خاطئة. المصفوفات ليست مصفوفات في الحقيقة. ما هي الصفقة مع this
، ما هو النموذج الأولي ، وماذا يفعل new
في الواقع؟
مشكلة فصول ES6
أسوأ مذنب حتى الآن هو أحدث إصدار من JavaScript ، ECMAScript 6 ( ES6 ): class. بعض الحديث حول الفصول الدراسية ينذر بالخطر بصراحة ويكشف عن سوء فهم عميق الجذور لكيفية عمل اللغة في الواقع:
"تعد JavaScript أخيرًا لغة موجهة للكائنات الآن بعد أن أصبحت بها فئات!"
أو:
"الطبقات تحررنا من التفكير في نموذج الميراث المكسور لجافا سكريبت."
او حتى:
"الفئات هي طريقة أكثر أمانًا وأسهل لإنشاء أنواع في JavaScript."
هذه العبارات لا تزعجني لأنها توحي بوجود خطأ ما في الوراثة النموذجية ؛ دعونا نضع هذه الحجج جانبا. تزعجني هذه العبارات لأن أيا منها ليس صحيحًا ، وهي توضح عواقب نهج جافا سكريبت "كل شيء للجميع" في تصميم اللغة: إنه يشل فهم المبرمج للغة أكثر مما يسمح به. قبل أن أذهب إلى أبعد من ذلك ، دعنا نوضح.
اختبار JavaScript Pop Quiz # 1: ما هو الفرق الأساسي بين هذه الكتل البرمجية؟
function PrototypicalGreeting(greeting = "Hello", name = "World") { this.greeting = greeting this.name = name } PrototypicalGreeting.prototype.greet = function() { return `${this.greeting}, ${this.name}!` } const greetProto = new PrototypicalGreeting("Hey", "folks") console.log(greetProto.greet())
class ClassicalGreeting { constructor(greeting = "Hello", name = "World") { this.greeting = greeting this.name = name } greet() { return `${this.greeting}, ${this.name}!` } } const classyGreeting = new ClassicalGreeting("Hey", "folks") console.log(classyGreeting.greet())
الجواب هنا ليس هناك واحد . هذه تفعل الشيء نفسه بشكل فعال ، إنها فقط مسألة ما إذا كان قد تم استخدام بناء جملة فئة ES6.
صحيح ، المثال الثاني أكثر تعبيرا. لهذا السبب وحده ، قد تجادل بأن class
هو إضافة لطيفة للغة. لسوء الحظ ، فإن المشكلة أكثر دقة قليلاً.
اختبار JavaScript Pop Quiz # 2: ماذا تفعل الكود التالي؟
function Proto() { this.name = 'Proto' return this; } Proto.prototype.getName = function() { return this.name } class MyClass extends Proto { constructor() { super() this.name = 'MyClass' } } const instance = new MyClass() console.log(instance.getName()) Proto.prototype.getName = function() { return 'Overridden in Proto' } console.log(instance.getName()) MyClass.prototype.getName = function() { return 'Overridden in MyClass' } console.log(instance.getName()) instance.getName = function() { return 'Overridden in instance' } console.log(instance.getName())
الإجابة الصحيحة هي أنها تطبع لوحدة التحكم:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
إذا أجبت بشكل غير صحيح ، فأنت لا تفهم ما هو class
في الواقع. هذا ليس خطأك. مثل Array
، class
ليست ميزة لغوية ، إنها الظلامية النحوية . يحاول إخفاء نموذج الوراثة النموذجي والتعابير الاصطلاحية الخرقاء المصاحبة له ، ويشير ضمنيًا إلى أن JavaScript يفعل شيئًا ليس كذلك.
ربما تم إخبارك أن class
قد تم تقديمه إلى JavaScript لجعل مطوري OOP الكلاسيكيين القادمين من لغات مثل Java أكثر راحة مع نموذج وراثة فئة ES6. إذا كنت أحد هؤلاء المطورين ، فمن المحتمل أن هذا المثال قد أصابك بالرعب. أنه ينبغي. إنه يوضح أن الكلمة الأساسية class
JavaScript لا تأتي مع أي من الضمانات التي من المفترض أن توفرها الفئة. كما يوضح أيضًا أحد الاختلافات الرئيسية في نموذج وراثة النموذج الأولي: النماذج الأولية هي حالات كائن وليست أنواعًا .
النماذج الأولية مقابل الطبقات
يتمثل الاختلاف الأكثر أهمية بين الوراثة المستندة إلى الفئة والنموذج الأولي في أن الفئة تحدد نوعًا يمكن إنشاء مثيل له في وقت التشغيل ، في حين أن النموذج الأولي هو في حد ذاته مثيل كائن.
الطفل من فئة ES6 هو تعريف نوع آخر يمد الأصل بخصائص وطرق جديدة ، والتي بدورها يمكن إنشاء مثيل لها في وقت التشغيل. الطفل من النموذج الأولي هو مثيل كائن آخر يفوض إلى الأصل أي خصائص لم يتم تنفيذها على الطفل.
ملاحظة جانبية: قد تتساءل لماذا ذكرت طرق الفصل ، ولكن ليس طرق النماذج الأولية. هذا لأن JavaScript ليس لديها مفهوم الأساليب. الدالات هي من الدرجة الأولى في JavaScript ، ويمكن أن يكون لها خصائص أو أن تكون خصائص لكائنات أخرى.
ينشئ مُنشئ الفئة مثيلاً للفئة. المُنشئ في JavaScript هو مجرد وظيفة قديمة بسيطة تُرجع كائنًا. الشيء الوحيد الذي يميز مُنشئ JavaScript هو أنه عند استدعائه بالكلمة الأساسية new
، فإنه يعين النموذج الأولي الخاص به كنموذج أولي للكائن المرتجع. إذا كان هذا يبدو مربكًا بعض الشيء بالنسبة لك ، فأنت لست وحدك - إنه كذلك ، وهو جزء كبير من سبب سوء فهم النماذج الأولية.
لوضع نقطة جيدة حقًا في ذلك ، فإن طفل النموذج الأولي ليس نسخة من نموذجه الأولي ، ولا هو كائن بنفس شكل النموذج الأولي الخاص به. يمتلك الطفل مرجعًا حيًا للنموذج الأولي ، وأي خاصية نموذج أولي غير موجودة في الطفل هي مرجع أحادي الاتجاه لخاصية تحمل الاسم نفسه في النموذج الأولي.
ضع في اعتبارك ما يلي:
let parent = { foo: 'foo' } let child = { } Object.setPrototypeOf(child, parent) console.log(child.foo) // 'foo' child.foo = 'bar' console.log(child.foo) // 'bar' console.log(parent.foo) // 'foo' delete child.foo console.log(child.foo) // 'foo' parent.foo = 'baz' console.log(child.foo) // 'baz'
في المثال السابق ، بينما لم يتم undefined
child.foo
، فإنه يشير إلى parent.foo
. بمجرد أن حددنا foo
على child
، child.foo
لديه القيمة 'bar'
، لكن احتفظ parent.foo
بقيمته الأصلية. بمجرد delete child.foo
فإنه يشير مرة أخرى إلى parent.foo
، مما يعني أنه عندما نغير قيمة الوالد ، يشير child.foo
إلى القيمة الجديدة.
دعونا نلقي نظرة على ما حدث للتو (لغرض التوضيح بشكل أوضح ، سوف نتظاهر بأن هذه عبارة عن Strings
وليست سلاسل حرفية ، ولا يهم الاختلاف هنا):
الطريقة التي يعمل بها هذا الأمر تحت الغطاء ، وخاصة خصائص new
this
، موضوع ليوم آخر ، لكن موزيلا لديها مقال شامل حول سلسلة وراثة النموذج الأولي لجافا سكريبت إذا كنت ترغب في قراءة المزيد.
الفكرة الأساسية هي أن النماذج الأولية لا تحدد type
؛ هم أنفسهم instances
ويمكن تغييرها في وقت التشغيل ، مع كل ما يستتبعه وضمنيًا.
مازلت معي؟ دعنا نعود إلى تشريح فئات JavaScript.
اختبار JavaScript Pop Quiz # 3: كيف تطبق الخصوصية في الفصول الدراسية؟
النموذج الأولي وخصائص الفئة أعلاه ليست "مغلفة" بقدر "معلقة بشكل غير مستقر خارج النافذة". يجب أن نصلح ذلك ، لكن كيف؟
لا توجد أمثلة رمز هنا. الجواب هو أنك لا تستطيع.
لا تحتوي JavaScript على أي مفهوم للخصوصية ، ولكن لديها قيود:
function SecretiveProto() { const secret = "The Class is a lie!" this.spillTheBeans = function() { console.log(secret) } } const blabbermouth = new SecretiveProto() try { console.log(blabbermouth.secret) } catch(e) { // TypeError: SecretiveClass.secret is not defined } blabbermouth.spillTheBeans() // "The Class is a lie!"
هل تفهم ما حدث للتو؟ إذا لم يكن الأمر كذلك ، فأنت لا تفهم عمليات الإغلاق. هذا جيد ، حقًا - إنهم ليسوا مخيفين كما تم تصوره ، إنهم مفيدون للغاية ، ويجب أن تأخذ بعض الوقت للتعرف عليهم.
اختبار JavaScript Pop Quiz # 4: ما هو المكافئ لما سبق باستخدام الكلمة الرئيسية class
؟
آسف ، هذا سؤال مخادع آخر. يمكنك فعل الشيء نفسه بشكل أساسي ، لكن يبدو الأمر كما يلي:
class SecretiveClass { constructor() { const secret = "I am a lie!" this.spillTheBeans = function() { console.log(secret) } } looseLips() { console.log(secret) } } const liar = new SecretiveClass() try { console.log(liar.secret) } catch(e) { console.log(e) // TypeError: SecretiveClass.secret is not defined } liar.spillTheBeans() // "I am a lie!"
اسمحوا لي أن أعرف ما إذا كان ذلك يبدو أسهل أو أوضح من SecretiveProto
. من وجهة نظري الشخصية ، الأمر أسوأ إلى حد ما - فهو يكسر الاستخدام class
لإعلانات الفئات في JavaScript ولا يعمل كثيرًا كما تتوقع ، على سبيل المثال ، من Java. سيتم توضيح ذلك من خلال ما يلي:

اختبار JavaScript Pop Quiz # 5: ماذا تفعل SecretiveClass::looseLips()
؟
هيا نكتشف:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }
حسنًا ... كان ذلك محرجًا.
اختبار JavaScript Pop Quiz # 6: ما الذي يفضله مطورو JavaScript المتمرسون - النماذج الأولية أو الفئات؟
لقد خمنت أن هذا سؤال خادع آخر - يميل مطورو JavaScript المتمرسون إلى تجنب كليهما عندما يستطيعون ذلك. إليك طريقة رائعة للقيام بما ورد أعلاه باستخدام JavaScript اصطلاحي:
function secretFactory() { const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!" const spillTheBeans = () => console.log(secret) return { spillTheBeans } } const leaker = secretFactory() leaker.spillTheBeans()
لا يقتصر الأمر على تجنب القبح المتأصل في الوراثة أو فرض التغليف. فكر فيما قد تفعله أيضًا مع secretFactory
leaker
والذي لا يمكنك فعله بسهولة باستخدام نموذج أولي أو فصل دراسي.
لسبب واحد ، يمكنك تدميره لأنه لا داعي للقلق بشأن سياق this
:
const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)
هذا جميل جدا. إلى جانب تجنب التلاعب new
this
، فإنه يسمح لنا باستخدام كائناتنا بالتبادل مع الوحدتين CommonJS و ES6. كما أنه يجعل التركيب أسهل قليلاً:
function spyFactory(infiltrationTarget) { return { exfiltrate: infiltrationTarget.spillTheBeans } } const blackHat = spyFactory(leaker) blackHat.exfiltrate() // Favor composition over inheritance, (...) console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)
عملاء blackHat
لا داعي للقلق بشأن مصدر التسرب ، ولا يضطر exfiltrate
إلى spyFactory
Function::bind
السياق أو الخصائص المتداخلة بعمق. ضع في اعتبارك ، لا داعي للقلق كثيرًا بشأن this
في الكود الإجرائي المتزامن البسيط ، ولكنه يسبب جميع أنواع المشكلات في التعليمات البرمجية غير المتزامنة التي من الأفضل تجنبها.
بقليل من التفكير ، يمكن تطوير spyFactory
إلى أداة تجسس متطورة للغاية يمكنها التعامل مع جميع أنواع أهداف التسلل - أو بعبارة أخرى ، واجهة.
بالطبع يمكنك القيام بذلك مع فئة أيضًا ، أو بالأحرى مجموعة متنوعة من الفئات ، وكلها موروثة من abstract class
أو interface
مجردة ... باستثناء أن JavaScript لا يحتوي على أي مفهوم للملخصات أو الواجهات.
دعنا نعود إلى مثال الترحيب لنرى كيف سنقوم بتنفيذه مع المصنع:
function greeterFactory(greeting = "Hello", name = "World") { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!
ربما لاحظت أن هذه المصانع تزداد إيجازًا مع تقدمنا في العمل ، لكن لا تقلق - فهم يفعلون نفس الشيء. عجلات التدريب تنطلق يا رفاق!
هذا بالفعل أقل نموذجية من النموذج الأولي أو إصدار الفصل من نفس الكود. ثانيًا ، يحقق تغليف خصائصه بشكل أكثر فعالية. أيضًا ، يحتوي على ذاكرة وأداء أقل في بعض الحالات (قد لا يبدو الأمر كذلك للوهلة الأولى ، لكن مترجم JIT يعمل بهدوء خلف الكواليس لتقليل التكرار واستنتاج الأنواع).
لذا فهي أكثر أمانًا ، وغالبًا ما تكون أسرع ، ومن الأسهل كتابة كود مثل هذا. لماذا نحتاج الفصول مرة أخرى؟ أوه ، بالطبع ، إعادة الاستخدام. ماذا يحدث إذا أردنا متغيرات ترحيب غير سعيدة ومتحمسة؟ حسنًا ، إذا كنا نستخدم فئة ClassicalGreeting
، فمن المحتمل أن ننتقل مباشرةً إلى الحلم بالتسلسل الهرمي للفصل. نعلم أننا سنحتاج إلى تحديد معلمات علامات الترقيم ، لذلك سنفعل القليل من إعادة البناء وإضافة بعض العناصر الفرعية:
// Greeting class class ClassicalGreeting { constructor(greeting = "Hello", name = "World", punctuation = "!") { this.greeting = greeting this.name = name this.punctuation = punctuation } greet() { return `${this.greeting}, ${this.name}${this.punctuation}` } } // An unhappy greeting class UnhappyGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, " :(") } } const classyUnhappyGreeting = new UnhappyGreeting("Hello", "everyone") console.log(classyUnhappyGreeting.greet()) // Hello, everyone :( // An enthusiastic greeting class EnthusiasticGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, "!!") } greet() { return super.greet().toUpperCase() } } const greetingWithEnthusiasm = new EnthusiasticGreeting() console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!
إنها طريقة جيدة ، حتى يأتي شخص ما ويطلب ميزة لا تتناسب تمامًا مع التسلسل الهرمي ويتوقف الأمر برمته عن أي معنى. ضع دبوسًا في هذه الفكرة بينما نحاول كتابة نفس الوظيفة مع المصانع:
const greeterFactory = (greeting = "Hello", name = "World", punctuation = "!") => ({ greet: () => `${greeting}, ${name}${punctuation}` }) // Makes a greeter unhappy const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ":(") console.log(unhappy(greeterFactory)("Hello", "everyone").greet()) // Hello, everyone :( // Makes a greeter enthusiastic const enthusiastic = (greeter) => (greeting, name) => ({ greet: () => greeter(greeting, name, "!!").greet().toUpperCase() }) console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!
ليس من الواضح أن هذا الرمز أفضل ، على الرغم من أنه أقصر قليلاً. في الواقع ، يمكنك أن تجادل بأنه من الصعب القراءة ، وربما يكون هذا نهجًا منفردًا. ألا يمكن أن يكون لدينا مصنع unhappyGreeterFactory
enthusiasticGreeterFactory
؟
ثم يأتي عميلك ويقول ، "أنا بحاجة إلى مرحب جديد غير سعيد ويريد من الغرفة بأكملها أن يعرف ذلك!"
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
إذا احتجنا إلى استخدام هذا المستقبلي غير السعيد المتحمسين أكثر من مرة ، فيمكننا أن نجعل الأمر أسهل على أنفسنا:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory("You're late", "Jim").greet())
هناك طرق لهذا النمط من التكوين تعمل مع النماذج الأولية أو الفئات. على سبيل المثال ، يمكنك إعادة التفكير UnhappyGreeting
and EnthusiasticGreeting
كديكور. لا يزال الأمر يتطلب المزيد من البيانات المعيارية أكثر من أسلوب النمط الوظيفي المستخدم أعلاه ، ولكن هذا هو الثمن الذي تدفعه مقابل سلامة وتغليف الطبقات الحقيقية .
الشيء ، في JavaScript ، أنك لا تحصل على هذا الأمان التلقائي. تقوم أطر عمل JavaScript التي تؤكد على استخدام class
بالكثير من "السحر" للتغلب على هذه الأنواع من المشاكل وإجبار الفئات على التصرف. ألق نظرة على كود مصدر Polymer's ElementMixin
في وقت ما ، أتحداك. إنها مستويات ساحرة لجافا سكريبت أركانا ، وأعني ذلك بدون سخرية أو سخرية.
بالطبع ، يمكننا إصلاح بعض المشكلات التي تمت مناقشتها أعلاه باستخدام Object.freeze
أو Object.defineProperties
أكبر أو أقل. ولكن لماذا تقليد النموذج بدون الوظيفة ، بينما تجاهل الأدوات التي توفرها JavaScript لنا في الأصل والتي قد لا نجدها في لغات مثل Java؟ هل ستستخدم مطرقة تسمى "مفك البراغي" لقيادة المسمار ، في حين أن صندوق الأدوات الخاص بك يحتوي على مفك براغي حقيقي يجلس بجواره مباشرةً؟
البحث عن الأجزاء الجيدة
غالبًا ما يؤكد مطورو JavaScript على الأجزاء الجيدة للغة ، بالعامية والإشارة إلى الكتاب الذي يحمل نفس الاسم. نحاول تجنب الفخاخ التي حددتها خيارات التصميم اللغوية الأكثر تساؤلًا ونلتزم بالأجزاء التي تتيح لنا كتابة تعليمات برمجية نظيفة وقابلة للقراءة وتقليل الأخطاء وقابلة لإعادة الاستخدام.
هناك حجج معقولة حول أي أجزاء من JavaScript مؤهلة ، لكني آمل أن أقنعك أن class
ليس واحدًا منها. إذا تعذر ذلك ، آمل أن تفهم أن الوراثة في JavaScript يمكن أن تكون فوضى مربكة وأن هذه class
لا تصلحها ولا توفر عليك فهم النماذج الأولية. رصيد إضافي إذا التقطت تلميحات تفيد بأن أنماط التصميم الموجهة للكائنات تعمل بشكل جيد بدون الفئات أو وراثة ES6.
أنا لا أخبرك أن تتجنب class
تمامًا. في بعض الأحيان تحتاج إلى الميراث ، وتوفر class
بناء جملة أنظف للقيام بذلك. على وجه الخصوص ، فإن class X extends Y
هي أجمل بكثير من نهج النموذج الأولي القديم. بالإضافة إلى ذلك ، تشجع العديد من أطر العمل الأمامية الشائعة استخدامه ، وربما يجب عليك تجنب كتابة كود غريب غير قياسي من حيث المبدأ وحده. أنا فقط لا أحب إلى أين يذهب هذا.
في كوابيسي ، تمت كتابة جيل كامل من مكتبات JavaScript باستخدام class
، مع توقع أنها ستتصرف بشكل مشابه للغات الشائعة الأخرى. تم اكتشاف فئات جديدة كاملة من الحشرات (يقصد التورية). يتم إحياء العناصر القديمة التي كان من الممكن أن تُترك بسهولة في مقبرة جافا سكريبت المشوهة إذا لم نسقط بلا مبالاة في فخ class
. يعاني مطورو جافا سكريبت ذوي الخبرة من هذه الوحوش ، لأن ما هو مشهور لا يكون دائمًا جيدًا.
في نهاية المطاف ، نتخلى جميعًا عن الإحباط ونبدأ في إعادة اختراع العجلات في Rust أو Go أو Haskell أو من يعرف ماذا أيضًا ، ثم التجميع إلى Wasm للويب ، وتتكاثر أطر عمل الويب والمكتبات الجديدة إلى ما لا نهاية متعدد اللغات.
انها حقا تجعلني مستيقظا في الليل.