
تساءل الجميع الذين يستخدمون جافا سكريبت رائعة لأي غرض: لماذا يكون نوع الكائن " فارغًا " ؟ typeof من دالة تُرجع "function" ، لكن من Array - "object" ؟ وأين getClass في فصولك المتبجح؟ وعلى الرغم من أن معظم المواصفات والحقائق التاريخية تجيب بسهولة وطبيعية ، أود أن أرسم خطًا قليلاً ... المزيد لنفسي.
إذا كان القارئ ونوعك ومثالك غير كافيين بالنسبة لك في مهامك ، وتريد نوعًا من التحديد ، بدلاً من "الكائنات" ، فقد يكون مفيدًا لاحقًا. أوه نعم ، عن البط: سيكونون أيضا ، مجرد خطأ بسيط.
خلفية موجزة
لطالما كان الحصول على نوع متغير في JavaScript بشكل موثوق به مهمة غير تافهة ، بالتأكيد ليس للمبتدئين. في معظم الحالات ، ليس مطلوبًا بالطبع ، فقط:
if (typeof value === 'object' && value !== null) {
والآن لا يمكنك التقاط Cannot read property of null
- وهي نظير محلي لـ NPE. هل هذا مألوف؟
ثم بدأنا في استخدام الوظائف كمنشدين أكثر فأكثر ، وأحيانًا يكون من المفيد فحص نوع الكائن الذي تم إنشاؤه بهذه الطريقة. ولكن مجرد استخدام typeof من المثيل لن يعمل ، لأننا نحصل على "الكائن" بشكل صحيح.
ثم كان من الطبيعي استخدام نموذج OOP أولي في JavaScript ، أتذكر؟ لدينا بعض الأشياء ، ومن خلال الرابط إلى نموذجها الأولي ، يمكننا العثور على خاصية المُنشئ التي تشير إلى الوظيفة التي تم إنشاء الكائن بها. ثم القليل من السحر مع toString من الوظيفة والتعبيرات العادية ، وهنا النتيجة:
f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1]
في بعض الأحيان كانوا يسألون مثل هذه الأسئلة في المقابلات ، ولكن لماذا؟
نعم ، يمكننا فقط حفظ تمثيل سلسلة من النوع كخاصية خاصة والحصول عليه من الكائن من خلال سلسلة النموذج الأولي:
function Type() {}; Type.prototype.typeName = 'Type'; var o = new Type; o.typeName; < "Type"
مرتين فقط عليك كتابة "النوع" : في بيان الوظيفة وفي الخاصية.
بالنسبة للكائنات المدمجة (مثل Array أو Date ) ، كان لدينا خاصية سرية [[Class]] ، والتي يمكن ربطها عبر toString من الكائن القياسي:
Object.prototype.toString.call(new Array); < "[object Array]"
الآن لدينا فئات ، ويتم إصلاح الأنواع المخصصة أخيرًا في اللغة: هذا ليس LiveScript بالنسبة لك ؛ نكتب الكود المدعوم بكميات كبيرة!
في نفس الوقت تقريبًا ، ظهرت Symbol.toStringTag و Function.name ، والتي يمكننا من خلالها أخذ نوعنا بطريقة جديدة.
بشكل عام ، قبل أن نمضي قدمًا ، أود أن أشير إلى أن المشكلة قيد النظر تتطور على StackOverflow جنبًا إلى جنب مع اللغة وترتفع من التحرير إلى هيئة التحرير: قبل 9 سنوات ، قبل 7 سنوات ، وليس منذ وقت طويل ، أو هذا وذاك .
الوضع الحالي
في وقت سابق ، درسنا بالفعل بالتفصيل الكافي Symbol.toStringTag و Function.name . باختصار ، إن شخصية toStringTag الداخلية حديثة [[Class]] ، فقط يمكننا إعادة تعريفها لكائناتنا. وخاصية Function.name مصدق عليها في جميع المتصفحات تقريبًا نفس النوع typeName من المثال: إرجاع اسم الوظيفة.
بدون تردد ، يمكنك تحديد هذه الوظيفة:
function getTag(any) { if (typeof any === 'object' && any !== null) { if (typeof any[Symbol.toStringTag] === 'string') { return any[Symbol.toStringTag]; } if (typeof any.constructor === 'function' && typeof any.constructor.name === 'string') { return any.constructor.name; } } return Object.prototype.toString.call(any).match(/\[object\s(\w+)]/)[1]; }
- إذا كان المتغير كائنًا ، فعندئذٍ:
1.1. إذا تم تجاوز الكائن إلى StringTag ، ثم إعادته ؛
1.2. إذا كانت دالة المُنشئ معروفة للكائن وكانت الوظيفة لها خاصية اسم ، فأعدها ؛ - أخيرًا ، استخدم طريقة toString للكائن الكائن ، والتي ستقوم بكل العمل المتعدد الأشكال بالنسبة لنا لأي متغير آخر على الإطلاق.
الكائن مع toStringTag :
let kitten = { [Symbol.toStringTag]: 'Kitten' }; getTag(kitten); < "Kitten"
الفصل الدراسي مع toStringTag :
class Cat { get [Symbol.toStringTag]() { return 'Kitten'; } } getTag(new Cat); < "Kitten"
باستخدام اسم المنشئ :
class Dog {} getTag(new Dog); < "Dog"
→ يمكن العثور على مزيد من الأمثلة في هذا المستودع.
وبالتالي ، من السهل الآن تحديد نوع أي متغير في جافا سكريبت. تتيح لك هذه الوظيفة التحقق من المتغيرات بشكل منتظم من حيث النوع واستخدام تعبير مفتاح بسيط بدلاً من عمليات فحص البط في وظائف متعددة الأشكال. بشكل عام ، لم يعجبني أبدًا النهج القائم على كتابة البط ، يقولون إذا كان هناك شيء ما له خاصية لصق ، هل هو صفيف أم شيء؟
بعض البط الخاطئ
إن فهم نوع المتغير من خلال وجود طرق أو خصائص معينة هو بالطبع عمل الجميع ويعتمد على الموقف. لكنني سآخذ هذا getTag وأتفقد بعض الأشياء اللغوية.
الطبقات؟
"بطتي" المفضلة في جافا سكريبت هي الفصول. الرجال الذين بدأوا في كتابة جافا سكريبت مع ES-2015 ، لا يشكون في بعض الأحيان ما هي هذه الفئات. والحقيقة هي:
class Person { constructor(name) { this.name = name; } hello() { return this.name; } } let user = new Person('John'); user.hello(); < "John"
لدينا كلمة أساسية للفئة ، ومنشئ ، وبعض الطرق ، تمتد حتى. نقوم أيضًا بإنشاء مثيل من هذه الفئة من خلال جديد . يبدو الفصل في معناه المعتاد - يعني فئة!
ومع ذلك ، عندما تبدأ في إضافة طرق جديدة في الوقت الفعلي إلى "الفصل الدراسي" ، وفي نفس الوقت تصبح متاحة على الفور للحالات التي تم إنشاؤها بالفعل ، يتم فقد بعضها:
Person.prototype.hello = function() { return `Is not ${this.name}`; } user.hello(); < "Is not John"
لا تفعل هذا!
ولا يعرف البعض بشكل موثوق أن هذا مجرد سكر نحوي على نموذج النموذج الأولي ، لأنه لم يتغير شيء من الناحية النظرية في اللغة. إذا اتصلنا بـ getTag من الشخص ، فإننا نحصل على "الوظيفة" ، وليس "الفئة" المختلقة ، وهذا يستحق التذكر.
وظائف أخرى
هناك عدة طرق لتعريف دالة في JavaScript: FunctionDeclaration و FunctionExpression و ArrowFunction مؤخرًا. نعلم جميعًا متى وماذا نستخدم: تختلف الأمور تمامًا. علاوة على ذلك ، إذا قمنا باستدعاء getTag من الوظيفة المعلنة من قبل أي من الخيارات المقترحة ، فسوف نحصل على "Function" .
في الواقع ، هناك العديد من الطرق لتعريف وظيفة في اللغة. نضيف إلى القائمة على الأقل إعلان ClassDeclaration ، ثم الوظائف والمولدات غير المتزامنة ، وما إلى ذلك: AsyncFunctionDeclaration و AsyncFunctionExpression و AsyncArrowFunction و GeneratorDeclaration و GeneratorExpression و ClassExpression و MethodDefinition (القائمة ليست كاملة). ويبدو أنهم يقولون ذلك ماذا؟ كل ما سبق يتصرف مثل دالة - مما يعني أن getTag سيعيد أيضًا "دالة" . ولكن هناك ميزة واحدة: كل الخيارات هي وظائف بالتأكيد ، ولكن ليس كلها مباشرة - الوظيفة .
هناك أنواع فرعية مضمنة من الوظيفة :
تم تصميم مُنشئ الوظيفة بحيث يكون من الفئات الفرعية.
لا توجد وسائل نحوية لإنشاء مثيلات الفئات الفرعية للوظيفة باستثناء الفئات الفرعية GeneratorFunction و AsyncFunction المضمنة.
لدينا وظيفة وداخل GeneratorFunction و AsyncFunction مع منشئيها "الموروثة" منه. وهذا يؤكد أن الأحواض والمولدات لها طبيعتها الفريدة. ونتيجة لذلك:
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } getTag(sleep); < "AsyncFunction"
في الوقت نفسه ، لا يمكننا إنشاء مثل هذه الوظيفة من خلال عامل التشغيل الجديد ، وتعيد دعوتنا وعدنا :
getTag(sleep(100)); < "Promise"
مثال على وظيفة المولد:
function* incg(i) { while(1) yield i += 1; } getTag(incg); < "GeneratorFunction"
يؤدي استدعاء مثل هذه الوظيفة إلى إرجاع مثيل - كائن المولد :
let inc = incg(0); getTag(inc); < "Generator"
تم إعادة تعريف رمز toStringTag إلى حد ما بالنسبة لروابط ومولدات. ولكن typeof لأي دالة ستظهر "الوظيفة" .
كائنات مضمنة
لدينا أشياء مثل Set أو Map أو Date أو Error . سيؤدي تطبيق getTag عليها إلى إرجاع "دالة" ، لأن هذه هي الوظائف - منشئو المجموعات والتواريخ والأخطاء القابلة للتكرار. من الحالات التي نحصل عليها على التوالي - "Set" و "Map" و "Date" و " Error ".
لكن احترس! لا تزال هناك أشياء مثل JSON أو Math . إذا كنت على عجل ، يمكنك افتراض موقف مماثل. لكن لا! هذا مختلف تمامًا - كائنات مفردة مضمنة. أنها ليست فورية ( is not a constructor
). استدعاء نوع سيعيد "كائن" (الذي يشك في ذلك). ولكن سيتحول getTag إلى StringTag وسيحصل على "JSON" و "Math" . هذه آخر ملاحظة أريد مشاركتها.
دعنا نذهب دون تعصب
سألت أعمق قليلاً من المعتاد عن نوع المتغير في جافا سكريبت مؤخرًا عندما قررت كتابة مفتش الكائنات البسيط لتحليل التعليمات البرمجية الديناميكية (اللعب). لا يتم نشر المادة فقط في البرمجة غير الطبيعية ، نظرًا لأنك لا تحتاج إليها في الإنتاج: هناك نوع من ، مثل ، Array.isArray ، isNaN وكل شيء آخر يستحق التذكر عند إجراء التحقق اللازم. تحتوي معظم المشاريع على TypeScript أو Dart أو Flow. أنا أحب جافا سكريبت !