معرف أو غير محدد؟ الفروق الدقيقة في إنشاء المصفوفات في JavaScript

صورة

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

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

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

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

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

عند إنشاء المصفوفات ، هناك خصوصية واحدة: لا يهم إذا استخدمنا المصفوفة (...) أو المصفوفة الجديدة (...) - مواصفات ECMAScript لا تميز بينهما ، علاوة على ذلك ، تعتبرها متكافئة.

22.1.1 The Array Constructor The Array constructor is the %Array% intrinsic object and the initial value of the Array property of the global object. When called as a constructor it creates and initializes a new exotic Array object. When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments. 

لذلك ، لن أفلسف بشكل سيء ، وفي الأمثلة سأستخدم بنية Array (...) الجديدة فقط ، حتى لا أربك أي شخص.

لنبدأ.

إنشاء مجموعة:

 let arr = new Array(5); 

ماذا حصلنا عليه؟

 console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined 

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

 let arr = new Array(5).map(function() { return new Array(5); }); console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined console.log(arr[0][0]); // TypeError: arr[0] is undefined 

كيف أنه ، بعد كل شيء ، كان علينا الحصول على مصفوفة ، وفي كل خلية ، وفقًا لذلك ، يجب أن يكون هناك مجموعة من 5 عناصر ...

دعنا ننتقل مرة أخرى إلى وثائق ECMAScript ونرى ما هو مكتوب فيها فيما يتعلق بطريقة إنشاء المصفوفات بحجة واحدة:

 22.1.1.2 Array (len) This description applies if and only if the Array constructor is called with exactly one argument. 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs = 1. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(0, proto). 7. If Type(len) is not Number, then 1. Let defineStatus be CreateDataProperty(array, "0", len). 2. Assert: defineStatus is true. 3. Let intLen be 1. 8. Else, 1. Let intLen be ToUint32(len). 2. If intLen ≠ len, throw a RangeError exception. 9. Let setStatus be Set(array, "length", intLen, true). 10. Assert: setStatus is not an abrupt completion. 11. Return array. 

وما نراه ، اتضح أنه تم إنشاء الكائن ، ويتم إنشاء خاصية الطول في إجراء ArrayCreate (النقطة 6) ، ويتم تعيين القيمة في خاصية الطول (النقطة 9) ، وماذا عن الخلايا؟ بصرف النظر عن الحالة الخاصة عندما تكون الوسيطة التي تم تمريرها ليست رقمًا ، ويتم إنشاء صفيف بخلية مفردة "0" بالقيمة المقابلة (النقطة 7) ، لا توجد كلمة عنها ... وهذا يعني ، = = 5 طول ، لكن لا توجد خمس خلايا. نعم ، إن المترجم يربكنا عندما نحاول الوصول إلى خلية واحدة ، فهو يفسر أن قيمتها غير محددة ، في حين أنها ليست كذلك.

هنا ، للمقارنة ، طريقة إنشاء المصفوفات بعدة وسيطات يتم إرسالها إلى المُنشئ:

 22.1.1.3 Array (...items ) This description applies if and only if the Array constructor is called with at least two arguments. When the Array function is called the following steps are taken: 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs ≥ 2. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(numberOfArgs, proto). 7. ReturnIfAbrupt(array). 8. Let k be 0. 9. Let items be a zero-origined List containing the argument items in order. 10. Repeat, while k < numberOfArgs 1. Let Pk be ToString(k). 2. Let itemK be items[k]. 3. Let defineStatus be CreateDataProperty(array, Pk, itemK). 4. Assert: defineStatus is true. 5. Increase k by 1. 11. Assert: the value of array's length property is numberOfArgs. 12. Return array. 

هنا ، من فضلك - 10 نقطة ، وإنشاء تلك الخلايا نفسها.

والآن ، ماذا يفعل Array.prototype.map () بعد ذلك؟

 22.1.3.15 Array.prototype.map ( callbackfn [ , thisArg ] ) 1. Let O be ToObject(this value). 2. ReturnIfAbrupt(O). 3. Let len be ToLength(Get(O, "length")). 4. ReturnIfAbrupt(len). 5. If IsCallable(callbackfn) is false, throw a TypeError exception. 6. If thisArg was supplied, let T be thisArg; else let T be undefined. 7. Let A be ArraySpeciesCreate(O, len). 8. ReturnIfAbrupt(A). 9. Let k be 0. 10. Repeat, while k < len 1. Let Pk be ToString(k). 2. Let kPresent be HasProperty(O, Pk). 3. ReturnIfAbrupt(kPresent). 4. If kPresent is true, then 1. Let kValue be Get(O, Pk). 2. ReturnIfAbrupt(kValue). 3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»). 4. ReturnIfAbrupt(mappedValue). 5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue). 6. ReturnIfAbrupt(status). 5. Increase k by 1. 11. Return A. 

الفقرة 7 - يتم إنشاء نسخة من المصفوفة الأصلية ، في الفقرة 10 يتم تنفيذ التكرار على عناصرها ، وعلى وجه الخصوص ، يتحقق البند 10.2 لمعرفة ما إذا كانت هناك خلية محددة في المصفوفة المصدر ، بحيث ، إذا نجحت ، خريطة (10.4) و إنشاء الخلية المناسبة في النسخة - 10.4.5. بما أن الرقم 10.2 يعطي خطأ لكل من التمريرات الخمسة ، فلن يتم إنشاء خلية واحدة في النسخة التي تم إرجاعها للصفيف أيضًا.

لذلك ، كيف نجح مُنشئ الصفيف وأسلوب Array.prototype.map () في العمل ، لكننا اكتشفنا ذلك ، لكن المهمة ظلت كما كانت من قبل ، لأنه لم يتم بناء المصفوفة. Function.prototype.apply () سيأتي لإنقاذ!
دعونا التحقق من ذلك في العمل على الفور:

 let arr = Array.apply(null, new Array(5)); console.log(arr); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

يا هلا ، لوحظ بوضوح أن جميع الخلايا الخمس هنا ، وكذلك الخلية الأولى ، التي تحتوي على رقم "0" تحتوي على واصف.

في هذه الحالة ، عمل البرنامج على النحو التالي:

  1. استدعينا طريقة Function.prototype.apply () وقمنا بتمرير السياق الفارغ إليها ، وكصفيف صفيف جديد (5) .
  2. إنشاء صفيف جديد (5) مجموعة بدون خلايا ، ولكن بطول 5 .
  3. استخدم Function.prototype.apply () الطريقة الداخلية لتقسيم الصفيف إلى وسيطات منفصلة ، ونتيجة لذلك ، قام بتمرير خمس وسيطات ذات قيم غير محددة إلى مُنشئ Array .
  4. تلقى صفيف 5 وسيطات مع قيم غير محددة ، إضافتها إلى الخلايا المقابلة.

يبدو أن كل شيء واضح ، باستثناء ما هي هذه الطريقة الداخلية لـ Function.prototype.apply () ، والتي تجعل 5 وسيطات من لا شيء - أقترح مرة أخرى أن ننظر إلى وثائق ECMAScript :

 19.2.3.1 Function.prototype.apply 1. If IsCallable(func) is false, throw a TypeError exception. 2. If argArray is null or undefined, then Return Call(func, thisArg). 3. Let argList be CreateListFromArrayLike(argArray). 7.3.17 CreateListFromArrayLike (obj [, elementTypes] ) 1. ReturnIfAbrupt(obj). 2. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object). 3. If Type(obj) is not Object, throw a TypeError exception. 4. Let len be ToLength(Get(obj, "length")). 5. ReturnIfAbrupt(len). 6. Let list be an empty List. 7. Let index be 0. 8. Repeat while index < len a. Let indexName be ToString(index). b. Let next be Get(obj, indexName). c. ReturnIfAbrupt(next). d. If Type(next) is not an element of elementTypes, throw a TypeError exception. e. Append next as the last element of list. f. Set index to index + 1. 9. Return list. 

نحن ننظر إلى النقاط الأكثر إثارة للاهتمام:

19.2.3.1 - الفقرة 3: إنشاء قائمة بالوسيطات من كائن مشابه للصفيف (كما نذكر ، يجب أن يكون لهذه الكائنات خاصية طول).

7.3.17 - طريقة إنشاء قائمة نفسها. يتحقق ما إذا كان الكائن هو أو لا ، وإذا كان الأمر كذلك ، طلب لحقل الطول (الفقرة 4). ثم يتم إنشاء فهرس يساوي "0" (الفقرة 7). يتم إنشاء حلقة مع زيادة الفهرس إلى القيمة مأخوذة من حقل الطول (الفقرة 8). في هذه الدورة ، نشير إلى قيم خلايا المصفوفة المرسلة مع الفهارس المقابلة (الفقرتان 8 أ و 8 ب). وكما نتذكر ، عند الوصول إلى قيمة خلية واحدة في صفيف لا توجد فيها خلايا بالفعل ، فإنها لا تزال تعطي قيمة - غير محددة . تتم إضافة القيمة الناتجة إلى نهاية قائمة الوسائط (الفقرة 8e).

حسنًا ، بعد أن أصبح كل شيء في مكانه الصحيح ، يمكنك بناء المصفوفة الفارغة بأمان.

 let arr = Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); }); console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ] console.log(arr[0]); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: (5) […], writable: true, enumerable: true, configurable: true } console.log(arr[0][0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr[0],"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

الآن ، كما ترون ، كل شيء يتلاقى ويبدو بسيطًا للغاية: نحن ، بالطريقة التي نعرفها بالفعل ، نقوم بإنشاء مجموعة Array.apply فارغة (صفرية ، جديدة صفيف (5)) ثم ننقلها إلى طريقة الخريطة ، والتي تنشئ نفس الصفيف في كل من الخلايا.

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

 let arr = new Array(...new Array(5)).map(() => new Array(...new Array(5))); 

أو سنبسطها تمامًا ، على الرغم من أنني وعدت سابقًا بعدم لمس ...

 let arr = Array(...Array(5)).map(() => Array(...Array(5))); 
ملاحظة: هنا استخدمنا أيضًا وظائف السهم ، حيث أننا ما زلنا نتعامل مع عامل فروق الذي ظهر في نفس المواصفات مثلهم.

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

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

وأخيرًا ، بالعودة إلى نفس السؤال حول تدفق stackoverflow - أذكر أن الشخص اعتبر خطأً أن الطريقة التي تلقاها أدت إلى الإجابة الصحيحة ، وأنه حصل على مصفوفة 5 × 5 ، لكن خطأً بسيطًا تسلل إلى هناك.

قاد في:

Array.apply(null, new Array(5)).map(function(){
return new Array(5);
});


ما رأيك النتيجة الفعلية ستكون هنا؟

الجواب
console.log (arr) ؛ // صفيف (5) [(5) [...] ، (5) [...] ، (5) [...] ، (5) [...] ، (5) [...]]
console.log (arr [0]) ؛ // Array (5) [<5 فتحات فارغة>]
console.log (Object.getOwnPropertyDescriptor (arr، "0"))؛ // كائن {value: (5) [...] ، قابل للكتابة: صواب ، قابل للتعداد: صواب ، قابل للتكوين: صواب}
console.log (arr [0] [0]) ؛ // غير محدد
console.log (Object.getOwnPropertyDescriptor (arr [0]، "0"))؛ // غير محدد

أليس كذلك ، هذا ليس هو ما يريده ...

المراجع:

ECMAScript 2015 لغة المواصفات
ما هو Array.apply القيام به في الواقع

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


All Articles