toString: عظيم و رهيب

الصورة


من المحتمل أن تكون وظيفة toString في JavaScript هي الأكثر "ضمنية" التي تمت مناقشتها بين مطوري js أنفسهم وبين المراقبين الخارجيين. هي سبب العديد من النكات والمذكرات حول العديد من العمليات الحسابية المشبوهة ، والتحولات التي تدخل في ذهول [كائن الكائن] . قد يتم التنازل عنها ، ربما ، فقط للمفاجأة عند العمل مع float64.


لقد دفعتني الحالات المثيرة للاهتمام التي كان عليّ ملاحظتها أو استخدامها أو التغلب عليها إلى كتابة استخلاص حقيقي. سنقوم بالركض فوق مواصفات اللغة واستخدام الأمثلة لتحليل الميزات غير الواضحة لـ toString .


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


كل ما تريد معرفته


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


JavaScript هي لغة ذات نظام ضعيف: مما يعني أنها تسمح لنا بخلط أنواع مختلفة ، وتنفيذ العديد من العمليات بشكل ضمني. في التحويلات ، يتم إقران toString مع valueOf لتقليل الكائن إلى البدائي المطلوب للعملية. على سبيل المثال ، يتحول عامل الجمع إلى سلسلة إذا كان هناك سطر واحد على الأقل بين عوامل التشغيل. تؤدي بعض الوظائف المعيارية للغة قبل عملها إلى مناقشة السلسلة: parseInt و decodeURI و JSON.parse و btoa وما إلى ذلك.


لقد قيل الكثير وسخر من الصب الضمني. سننظر في تنفيذ toString لكائنات نموذج اللغة الرئيسية.


Object.prototype.toString


إذا انتقلنا إلى القسم المقابل من المواصفات ، نجد أن المهمة الرئيسية لـ toString الافتراضية هي الحصول على ما يسمى بالعلامة لتسلسل إلى السلسلة الناتجة:


"[object " + tag + "]" 

للقيام بذلك:


  1. يحدث استدعاء لرمز toStringTag الداخلي (أو الخاصية الزائفة [[الفئة]] في الإصدار القديم): يحتوي على العديد من كائنات النموذج الأولي ( الخريطة والرياضيات و JSON وغيرها).
  2. إذا كان أحدهم مفقودًا أو ليس سلسلة ، فسيتم تعداد عدد من الخصائص والأساليب الزائفة الداخلية الأخرى التي تشير إلى نوع الكائن: [[Call]] للوظيفة ، [[DateValue]] للتاريخ ، وما إلى ذلك.
  3. حسنًا ، إذا لم يكن هناك شيء على الإطلاق ، فإن العلامة هي "كائن" .

أولئك الذين يتأثرون بالانعكاس سوف يلاحظون على الفور إمكانية الحصول على نوع كائن بعملية بسيطة (غير موصى بها من قبل المواصفات ، ولكن ممكن):


 const getObjT = obj => Object.prototype.toString.call(obj).match(/\[object\s(\w+)]/)[1]; 

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


 [Infinity, null, x => 1, new Date, function*(){}].map(getObjT); > ["Number", "Null", "Function", "Date", "GeneratorFunction"] 

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


هذا النهج له عيب رئيسي: أنواع المستخدم. ليس من الصعب تخمين أنه في حالاتهم نحصل فقط على "كائن" .


Symbol.toStringTag و Function.name مخصصة


يعتمد OOP في JavaScript على النماذج الأولية ، وليس على الفئات (مثل Java) ، وليس لدينا طريقة getClass () جاهزة. سيساعد التعريف الصريح لحرف toStringTag لنوع المستخدم على حل المشكلة:


 class Cat { get [Symbol.toStringTag]() { return 'Cat'; } } 

أو في نمط النموذج:


 function Dog(){} Dog.prototype[Symbol.toStringTag] = 'Dog'; 

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


 class Cat {} (new Cat).constructor.name < 'Cat' 

أو في نمط النموذج:


 function Dog() {} (new Dog).constructor.name < 'Dog' 

بالطبع ، لا يعمل هذا الحل مع الكائنات التي تم إنشاؤها باستخدام دالة مجهولة ( "مجهول" ) أو Object.create (خالية) ، أو بالنسبة للأوليات التي لا تحتوي على كائن مجمّع ( خالية ، غير محددة ).


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


Function.prototype.toString


لقد كنا مشتتين قليلاً ، ولكن نتيجة لذلك ، حصلنا على وظائف لها اهتمامها الخاص بـ String . أولاً ، ألق نظرة على الكود التالي:


 (function() { console.log('(' + arguments.callee.toString() + ')()'); })() 

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


يؤدي التطبيق المستخدم إلى toString للكائن النموذجي للدالة إلى إرجاع تمثيل سلسلة من التعليمات البرمجية المصدر للدالة ، مع الحفاظ على البنية المستخدمة في تعريفها: FunctionDeclaration و FunctionExpression و ClassDeclaration و ArrowFunction وما إلى ذلك.


على سبيل المثال ، لدينا وظيفة السهم:


 const bind = (f, ctx) => function() { return f.apply(ctx, arguments); } 

استدعاء bind.toString () سيعيد لنا تمثيل سلسلة من ArrowFunction :


 "(f, ctx) => function() { return f.apply(ctx, arguments); }" 

واستدعاء toString من دالة ملفوفة يمثل بالفعل سلسلة تمثيل دالة FunctionExpression :


 "function() { return f.apply(ctx, arguments); }" 

هذا المثال المرتبط ليس من قبيل الصدفة ، نظرًا لأن لدينا حلًا جاهزًا مع وظيفة ربط السياق .prototype.bind ، وفيما يتعلق بالوظائف المرتبطة الأصلية ، هناك ميزة من الدالة Function.prototype.toString للعمل معها. اعتمادًا على التنفيذ ، يمكن الحصول على تمثيل لكل من الوظيفة الملتفة نفسها والوظيفة المستهدفة . أحدث إصدارات V8 و SpiderMonkey من الكروم و ff:


 function getx() { return this.x; } getx.bind({ x: 1 }).toString() < "function () { [native code] }" 

لذلك ، يجب توخي الحذر مع الميزات المزخرفة أصلاً.


تدرب على استخدام f.toString


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


أبسط شيء يتبادر إلى الذهن هو تحديد طول الوظيفة :


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

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


يتبادر إلى الذهن على الفور تعريف معلمات الوظيفة ، وهو ما يمكن أن يكون مفيدًا للتفكير:


 f.toString().match(/^function(?:\s+\w+)?\s*\(([^\)]+)/m)[1].split(/\s*,\s*/) 

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


خيار خطير ومثير للاهتمام لتجاوز دالة من خلال التقييم :


 const sum = (a, b) => a + b; const prod = eval(sum.toString().replace(/\+(?=\s*(?:a|b))/gm, '*')); sum(5, 10) < 15 prod(5, 10) < 50 

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


الاستخدام العملي هو تجميع وتوزيع القوالب . تقوم العديد من تطبيقات محرك القالب بتجميع التعليمات البرمجية المصدر للقالب وتوفر وظيفة بيانات تشكل بالفعل HTML النهائي (أو غيره). فيما يلي مثال على دالة _template :


 const helloJst = "Hello, <%= user %>" _.template(helloJst)({ user: 'admin' }) < "Hello, admin" 

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


 const helloStr = _.template(helloJst).toString() helloStr < "function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { __p += 'Hello, ' + ((__t = ( user )) == null ? '' : __t); } return __p }" 

الآن نحن بحاجة إلى تنفيذ هذا الرمز على العميل قبل الاستخدام. أنه عند التجميع لم يكن هناك خطأ في بناء الجملة بسبب بناء جملة FunctionExpression :


 const helloFn = eval(helloStr.replace(/^function\(obj\)/, 'obj=>')); 

أو نحو ذلك:


 const helloFn = eval(`const f = ${helloStr};f`); 

أو كما تحب أكثر. على أي حال:


 helloFn({ user: 'admin' }) < "Hello, admin" 

قد لا يكون هذا أفضل ممارسة لتجميع القوالب من جانب الخادم وتوزيعها على العملاء بشكل أكبر. مجرد مثال باستخدام مجموعة من Function.prototype.toString والتقييم .


أخيرًا ، المهمة القديمة لتحديد اسم وظيفة (قبل ظهور خاصية Function.name ) عبر toString :


 f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1] 

بالطبع ، هذا يعمل بشكل جيد مع بناء جملة FunctionDeclaration . سيتطلب الحل الأكثر ذكاء تعبيرًا عاديًا أو مطابقة للأنماط.


الإنترنت مليء بالحلول المثيرة للاهتمام على أساس Function.prototype.toString ، اسأل فقط. شارك تجربتك في التعليقات: شيق جدًا.


صفيف. نموذج أولي إلى السلسلة


إن تنفيذ toString لكائن نموذج مصفوفة هو أمر عام ويمكن استدعاؤه لأي كائن. إذا كان الكائن يحتوي على طريقة ربط ، فإن نتيجة toString ستكون استدعائه ، وإلا ، Object.prototype.toString .


الصفيف ، منطقيا ، لديه طريقة ربط تسلسل تمثيل السلسلة لجميع عناصره من خلال الفاصل الذي تم تمريره كمعلمة (الافتراضي هو فاصلة).


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


 function seria() { return Array.from(arguments).toString(); } 

أو نحو ذلك:


 const seria = (...a) => a.toString(); 

فقط تذكر أن السلسلة "10" والرقم 10 سيتم إجراء تسلسل لهما. في مشكلة أقصر المذكرة في مرحلة واحدة ، تم استخدام هذا الحل.


تعمل الصلة الأصلية لعناصر المصفوفة من خلال دورة حسابية من 0 إلى الطول ولا تتم تصفيتها للعناصر المفقودة ( فارغة وغير محددة ). بدلاً من ذلك ، يحدث التسلسل مع الفاصل . وهذا يؤدي إلى ما يلي:


 const ar = new Array(1000); ar.toString() < ",,,...,,," // 1000 times 

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


 const k = []; k[2**10] = 1; k[2**20] = 2; k[2**30] = 3; Object.values(k).toString() < "1,2,3" Object.keys(k).toString() < "1024,1048576,1073741824" 

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


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


 const ar = new Array(1000); JSON.stringify(ar); < "[null,null,null,...,null,null,null]" // 1000 times 

في ختام القسم ، أود أن أذكرك أنه يمكنك تحديد طريقة الانضمام الخاصة بك لنوع المستخدم والاتصال Array.prototype.toString.call كبديل بديل للسلسلة ، ولكن أشك في أن لها أي استخدام عملي.


Number.prototype.toString and parseInt


إحدى المهام المفضلة لدي في اختبارات js هي ما سيعيد المكالمة التالية parseInt ؟


 parseInt(10**30, 2) 

أول شيء يفعله parseInt هو ضم الوسيطة إلى سلسلة عن طريق استدعاء الدالة المجردة ToString ، والتي ، بناءً على نوع الوسيطة ، تنفذ فرع الإرسال المطلوب. بالنسبة لرقم النوع ، يتم ما يلي:


  1. إذا كانت القيمة NaN ، 0 ، أو Infinity ، فارجع السلسلة المقابلة.
  2. خلاف ذلك ، تقوم الخوارزمية بإرجاع السجل الأكثر ملاءمة للإنسان للرقم: في شكل عشري أو أسي.

لن أقوم بتكرار الخوارزمية لتحديد النموذج المفضل هنا ، سألاحظ فقط ما يلي: إذا تجاوز عدد الأرقام في علامة عشرية 21 ، فسيتم تحديد نموذج أسي. وهذا يعني أنه في حالتنا لا تعمل parseInt مع "100 ... 000" ولكن مع "1e30". لذلك ، فإن الجواب ليس متوقعًا على الإطلاق 2 ^ 30. من يدري طبيعة هذا السحر رقم 21 - اكتب!


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


إجراء عكسي:


 (2**30).toString(2) 

هذا هو المكان الذي يتم فيه استدعاء وظيفة toString من كائن النموذج الأولي Number ، والذي يستخدم نفس الخوارزمية لتحويل العدد إلى سلسلة. كما أن لديها معلمة الجذر الأساسية الاختيارية. فقط يلقي RangeError لقيمة غير صالحة (يجب أن يكون عددًا صحيحًا من 2 إلى 36 شاملًا) ، بينما يُرجع parseInt NaN .


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


مهمة صرف الانتباه للحظة:


 '3113'.split('').map(parseInt) 

ماذا سيعود وكيف يصلح؟


حرم من الاهتمام


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


دعوة للتقاعس


الأمثلة التي ذكرتها ليست بأي حال من الأحوال وصفات جاهزة - فقط غذاء للتفكير. بالإضافة إلى ذلك ، أجد أنه من العبث وقليل من الغباء مناقشة هذا في المقابلات الفنية: لهذا هناك مواضيع أبدية حول الإغلاق ، والانضمام ، وحلقة حدث ، وأنماط الوحدة / الواجهة / الوسيط ، وأسئلة "بالطبع" حول [الإطار المستخدم].


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


مكافأة


عند إعداد هذه المادة للنشر ، استخدمت ترجمة Google. وبالصدفة اكتشفت تأثيرًا ترفيهيًا. إذا حددت ترجمة من الروسية إلى الإنجليزية ، فأدخل "toString" وابدأ محوها باستخدام مفتاح Backspace ، فسنلاحظ:


مكافأة


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

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


All Articles