ذكرنا في الفصل الأول من هذا القسم أن هناك طرقًا حديثة لإعداد النموذج الأولي.
يعتبر إعداد النموذج الأولي أو قراءته باستخدام obj.__proto__
قديمًا ومهملًا إلى حد ما (تم نقله إلى ما يسمى "الملحق ب" من معيار JavaScript، المخصص للمتصفحات فقط).
الطرق الحديثة للحصول على/تعيين نموذج أولي هي:
Object.getPrototypeOf(obj) – يُرجع [[Prototype]]
لـ obj
.
Object.setPrototypeOf(obj, proto) - يضبط [[Prototype]]
من obj
على proto
.
الاستخدام الوحيد لـ __proto__
، الذي لا يثير الاستياء، هو كخاصية عند إنشاء كائن جديد: { __proto__: ... }
.
على الرغم من أن هناك طريقة خاصة لذلك أيضًا:
Object.create(proto[, descriptors]) – ينشئ كائنًا فارغًا باستخدام proto
محدد كـ [[Prototype]]
وواصفات خاصية اختيارية.
على سبيل المثال:
دع الحيوان = { يأكل: صحيح }; // أنشئ كائنًا جديدًا باستخدام الحيوان كنموذج أولي دع الأرنب = Object.create(animal); // نفس {__proto__: الحيوان} تنبيه (أرنب. يأكل)؛ // حقيقي تنبيه(Object.getPrototypeOf(rabbit) === حيوان); // حقيقي Object.setPrototypeOf(rabbit, {}); // قم بتغيير النموذج الأولي للأرنب إلى {}
يعد الأسلوب Object.create
أقوى قليلاً، لأنه يحتوي على وسيطة ثانية اختيارية: واصفات الخصائص.
يمكننا توفير خصائص إضافية للكائن الجديد هناك، مثل هذا:
دع الحيوان = { يأكل: صحيح }; دع الأرنب = Object.create(animal, { القفزات: { القيمة: صحيح } }); تنبيه (rabbit.jumps)؛ // حقيقي
تكون الواصفات بنفس التنسيق الموضح في الفصل علامات الخصائص وواصفاتها.
يمكننا استخدام Object.create
لإجراء استنساخ كائن أقوى من نسخ الخصائص في for..in
:
السماح بالاستنساخ = Object.create( Object.getPrototypeOf(obj)، Object.getOwnPropertyDescriptors(obj) );
يقوم هذا الاستدعاء بإنشاء نسخة طبق الأصل من obj
، بما في ذلك جميع الخصائص: enumerable وغير enumerable، وخصائص البيانات والمحددات/الحروف - كل شيء، ومع [[Prototype]]
الصحيح.
هناك العديد من الطرق لإدارة [[Prototype]]
. كيف حدث ذلك؟ لماذا؟
وذلك لأسباب تاريخية.
كان الميراث النموذجي موجودًا في اللغة منذ فجرها، لكن طرق إدارته تطورت مع مرور الوقت.
لقد عملت خاصية prototype
لوظيفة المنشئ منذ العصور القديمة جدًا. إنها أقدم طريقة لإنشاء كائنات باستخدام نموذج أولي معين.
وفي وقت لاحق، في عام 2012، ظهر Object.create
في المعيار. لقد أعطى القدرة على إنشاء كائنات باستخدام نموذج أولي معين، لكنه لم يوفر القدرة على الحصول عليه/ضبطه. طبقت بعض المتصفحات ملحق __proto__
غير القياسي الذي سمح للمستخدم بالحصول على/تعيين نموذج أولي في أي وقت، لمنح المزيد من المرونة للمطورين.
لاحقًا، في عام 2015، تمت إضافة Object.setPrototypeOf
و Object.getPrototypeOf
إلى المعيار، لأداء نفس وظيفة __proto__
. نظرًا لأنه تم تنفيذ __proto__
فعليًا في كل مكان، فقد تم إهماله نوعًا ما وشق طريقه إلى الملحق ب من المعيار، وهو: اختياري للبيئات غير المستعرضة.
لاحقًا، في عام 2022، سُمح رسميًا باستخدام __proto__
في الكائنات الحرفية {...}
(تم نقله من الملحق ب)، ولكن ليس ككائن getter/setter obj.__proto__
(لا يزال في الملحق B).
لماذا تم استبدال __proto__
بالوظائف getPrototypeOf/setPrototypeOf
؟
لماذا تمت إعادة تأهيل __proto__
جزئيًا وتم السماح باستخدامه في {...}
، ولكن ليس كـ getter/setter؟
هذا سؤال مثير للاهتمام، ويتطلب منا أن نفهم سبب كون __proto__
سيئًا.
وقريبا سنحصل على الجواب.
لا تغير [[Prototype]]
على الكائنات الموجودة إذا كانت السرعة مهمة
من الناحية الفنية، يمكننا الحصول على/تعيين [[Prototype]]
في أي وقت. لكن عادةً ما نقوم بتعيينه مرة واحدة فقط في وقت إنشاء الكائن ولا نقوم بتعديله بعد الآن: يرث rabbit
من animal
، وهذا لن يتغير.
وقد تم تحسين محركات JavaScript بشكل كبير لهذا الغرض. يعد تغيير النموذج الأولي "أثناء التنقل" باستخدام Object.setPrototypeOf
أو obj.__proto__=
عملية بطيئة جدًا لأنها تؤدي إلى انقطاع التحسينات الداخلية لعمليات الوصول إلى خاصية الكائن. لذا تجنب ذلك إلا إذا كنت تعرف ما تفعله، أو أن سرعة JavaScript لا تهمك تمامًا.
كما نعلم، يمكن استخدام الكائنات كمصفوفات ترابطية لتخزين أزواج المفاتيح/القيمة.
…ولكن إذا حاولنا تخزين المفاتيح التي قدمها المستخدم فيه (على سبيل المثال، قاموس أدخله المستخدم)، يمكننا أن نرى خللًا مثيرًا للاهتمام: جميع المفاتيح تعمل بشكل جيد باستثناء "__proto__"
.
تحقق من المثال:
دع obj = {}; Let key = موجه("ما هو المفتاح؟", "__proto__"); obj[key] = "بعض القيمة"; تنبيه(obj[مفتاح]); // [كائن كائن]، وليس "قيمة ما"!
هنا، إذا كتب المستخدم __proto__
، فسيتم تجاهل المهمة في السطر 4!
من المؤكد أن هذا قد يكون مفاجئًا بالنسبة لغير المطورين، ولكنه مفهوم جدًا بالنسبة لنا. الخاصية __proto__
خاصة: يجب أن تكون إما كائنًا أو null
. لا يمكن أن تصبح السلسلة نموذجًا أوليًا. لهذا السبب يتم تجاهل إسناد سلسلة إلى __proto__
.
لكننا لم نكن ننوي تنفيذ مثل هذا السلوك، أليس كذلك؟ نريد تخزين أزواج المفاتيح/القيمة، ولم يتم حفظ المفتاح المسمى "__proto__"
بشكل صحيح. لذلك هذا خطأ!
هنا العواقب ليست فظيعة. لكن في حالات أخرى قد نقوم بتخزين كائنات بدلاً من السلاسل في obj
، ومن ثم سيتم تغيير النموذج الأولي بالفعل. ونتيجة لذلك، سوف تسوء عملية التنفيذ بطرق غير متوقعة على الإطلاق.
والأمر الأسوأ من ذلك هو أن المطورين عادة لا يفكرون في مثل هذا الاحتمال على الإطلاق. وهذا يجعل من الصعب ملاحظة مثل هذه الأخطاء وحتى تحويلها إلى نقاط ضعف، خاصة عند استخدام JavaScript على جانب الخادم.
قد تحدث أشياء غير متوقعة أيضًا عند إسناد الكائن إلى obj.toString
، نظرًا لأنه تابع كائن مدمج.
كيف يمكننا تجنب هذه المشكلة؟
أولاً، يمكننا فقط التبديل إلى استخدام Map
للتخزين بدلاً من الكائنات العادية، وبعد ذلك يصبح كل شيء على ما يرام:
دع الخريطة = خريطة جديدة ()؛ Let key = موجه("ما هو المفتاح؟", "__proto__"); Map.set(key, "بعض القيمة"); تنبيه(map.get(مفتاح)); // "بعض القيمة" (كما هو مقصود)
…لكن بناء جملة Object
غالبًا ما يكون أكثر جاذبية، لأنه أكثر إيجازًا.
ولحسن الحظ، يمكننا استخدام الأشياء، لأن مبدعي اللغة فكروا في هذه المشكلة منذ فترة طويلة.
كما نعلم، __proto__
ليست خاصية لكائن ما، ولكنها خاصية وصول للكائن Object.prototype
:
لذلك، إذا تمت قراءة أو تعيين obj.__proto__
، فسيتم استدعاء getter/setter المقابل من النموذج الأولي الخاص به، وسيحصل على/sets [[Prototype]]
.
كما قيل في بداية هذا القسم التعليمي: __proto__
هي طريقة للوصول إلى [[Prototype]]
، وليست [[Prototype]]
نفسها.
الآن، إذا أردنا استخدام كائن كمصفوفة ترابطية والتحرر من مثل هذه المشكلات، فيمكننا القيام بذلك بخدعة صغيرة:
Let obj = Object.create(null); // أو: obj = { __proto__: null } Let key = موجه("ما هو المفتاح؟", "__proto__"); obj[key] = "بعض القيمة"; تنبيه(obj[مفتاح]); // "بعض القيمة"
ينشئ Object.create(null)
كائنًا فارغًا بدون نموذج أولي ( [[Prototype]]
null
):
لذا، لا يوجد حرف/أداة ضبط موروثة لـ __proto__
. والآن تتم معالجتها كخاصية بيانات عادية، وبالتالي فإن المثال أعلاه يعمل بشكل صحيح.
يمكننا أن نطلق على مثل هذه الكائنات كائنات "عادية جدًا" أو كائنات "قاموس خالص"، لأنها أبسط من الكائن العادي العادي {...}
.
الجانب السلبي هو أن هذه الكائنات تفتقر إلى أي أساليب كائن مضمنة، على سبيل المثال toString
:
Let obj = Object.create(null); تنبيه (كائن) ؛ // خطأ (لا توجد سلسلة)
…ولكن هذا عادةً ما يكون جيدًا بالنسبة للمصفوفات الترابطية.
لاحظ أن معظم التوابع المرتبطة بالكائنات هي Object.something(...)
، مثل Object.keys(obj)
- فهي ليست موجودة في النموذج الأولي، لذا ستستمر في العمل على مثل هذه الكائنات:
Let chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见"; تنبيه(Object.keys(chineseDictionary)); // مرحبا، وداعا
لإنشاء كائن باستخدام النموذج الأولي المحدد، استخدم:
يوفر Object.create
طريقة سهلة لنسخ كائن بكل الواصفات:
Let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
بناء الجملة الحرفي: { __proto__: ... }
، يسمح بتحديد خصائص متعددة
أو Object.create(proto[, descriptors])، يسمح بتحديد واصفات الخاصية.
الطرق الحديثة للحصول على/تعيين النموذج الأولي هي:
Object.getPrototypeOf(obj) – يُرجع [[Prototype]]
لـ obj
(مثل __proto__
getter).
Object.setPrototypeOf(obj, proto) - يضبط [[Prototype]]
من obj
إلى proto
(مثل __proto__
setter).
لا يُنصح بالحصول على/إعداد النموذج الأولي باستخدام __proto__
getter/setter، فهو موجود الآن في الملحق B من المواصفات.
لقد قمنا أيضًا بتغطية الكائنات التي لا تحتوي على نماذج أولية، والتي تم إنشاؤها باستخدام Object.create(null)
أو {__proto__: null}
.
تُستخدم هذه الكائنات كقواميس لتخزين أي مفاتيح (ربما أنشأها المستخدم).
عادةً، ترث الكائنات الأساليب المضمنة و__ __proto__
getter/setter من Object.prototype
، مما يجعل المفاتيح المقابلة "مشغولة" ويحتمل أن تسبب آثارًا جانبية. مع النموذج الأولي null
، تكون الكائنات فارغة حقًا.
الأهمية: 5
يوجد dictionary
كائن، تم إنشاؤه كـ Object.create(null)
لتخزين أي أزواج key/value
.
قم بإضافة طريقة dictionary.toString()
إليها، والتي يجب أن تُرجع قائمة مفاتيح مفصولة بفواصل. يجب ألا تظهر toString
الخاصة بك في for..in
فوق الكائن.
وإليك كيف ينبغي أن تعمل:
دع القاموس = Object.create(null); // الكود الخاص بك لإضافة أسلوب Dictionary.toString // أضف بعض البيانات Dictionary.apple = "Apple"; Dictionary.__proto__ = "اختبار"; // __proto__ هو مفتاح خاصية عادي هنا // فقط apple و __proto__ موجودان في الحلقة ل(دع المفتاح في القاموس) { تنبيه (مفتاح)؛ // "تفاحة"، ثم "__proto__" } // toString الخاص بك قيد التنفيذ تنبيه (قاموس)؛ // "تفاحة، __ بروتو __"
يمكن أن تأخذ الطريقة جميع المفاتيح القابلة للتعداد باستخدام Object.keys
وتخرج قائمتها.
لجعل toString
غير قابلة للتعداد، دعنا نعرّفها باستخدام واصف الخاصية. يسمح لنا بناء جملة Object.create
بتوفير كائن بواصفات الخصائص كوسيطة ثانية.
دع القاموس = Object.create(null, { toString: { // تعريف خاصية toString value() { // القيمة هي دالة إرجاع Object.keys(this).join(); } } }); Dictionary.apple = "Apple"; Dictionary.__proto__ = "اختبار"; // apple و __proto__ موجودان في الحلقة ل(دع المفتاح في القاموس) { تنبيه (مفتاح)؛ // "تفاحة"، ثم "__proto__" } // قائمة خصائص مفصولة بفواصل بواسطة toString تنبيه (قاموس)؛ // "تفاحة، __ بروتو __"
عندما نقوم بإنشاء خاصية باستخدام واصف، تكون علاماتها false
بشكل افتراضي. لذلك في الكود أعلاه، dictionary.toString
غير قابل للتعداد.
راجع الفصل علامات الخصائص وأوصافها للمراجعة.
الأهمية: 5
لنقم بإنشاء كائن rabbit
جديد:
وظيفة الأرنب (الاسم) { this.name = name; } Rabbit.prototype.sayHi = وظيفة () { تنبيه(هذا.اسم); }; دع الأرنب = أرنب جديد("أرنب");
هذه المكالمات تفعل نفس الشيء أم لا؟
Rabbit.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(rabbit).sayHi(); أرنب.__proto__.sayHi();
الاستدعاء الأول يحتوي this == rabbit
، أما الاستدعاءات الأخرى this
مساوية لـ Rabbit.prototype
، لأنه في الواقع الكائن الموجود قبل النقطة.
لذلك تظهر المكالمة الأولى فقط Rabbit
، بينما تظهر المكالمات الأخرى undefined
:
وظيفة الأرنب (الاسم) { this.name = name; } Rabbit.prototype.sayHi = وظيفة() { تنبيه(هذا.اسم); } دع الأرنب = أرنب جديد("أرنب"); Rabbit.sayHi(); // أرنب Rabbit.prototype.sayHi(); // غير محدد Object.getPrototypeOf(rabbit).sayHi(); // غير محدد أرنب.__proto__.sayHi(); // غير محدد