يقدم
هذا الفصل هو الفصل الثاني حول التنفيذ الموجهة للكائنات ECMASCRIPT. في الفصل الأول ، نناقش المقارنة بين المقدمة و Cemascript. إذا لم تكن قد قرأت الفصل الأول ، قبل المتابعة مع هذا الفصل ، أوصي بشدة بقراءة الفصل الأول أولاً ، لأن هذا الفصل طويل جدًا (صفحة 35).
النص الإنجليزي الأصلي: http://dmitrysoshnikov.com/ecmascript/chapter-7-2-ooop-ecmascript-implementation/
ملاحظة: نظرًا لأن المقالة طويلة جدًا ، فإن الأخطاء أمر لا مفر منه ويتم تصحيحها باستمرار.
في المقدمة ، نمتد إلى ecmascript. الآن ، عندما نعلم أنه هو تطبيق OOP ، دعنا نحدده بدقة:
نسخة الكود كما يلي:
ECMASCRIPT هي لغة برمجة موجهة نحو الكائنات التي تدعم ميراث التفويض بناءً على النماذج الأولية.
Ecmascript هي لغة موجهة نحو الكائنات تدعم الميراث المفوض القائم على النموذج الأولي.
سنقوم بتحليل أنواع البيانات الأساسية. أول شيء نحتاج إلى فهمه هو أن ECMASCript يستخدم القيم والكائنات البدائية لتمييز الكيانات. لذلك ، فإن "في JavaScript ، كل شيء هو كائن" مذكور في بعض المقالات خاطئ (غير صحيح تمامًا) ، والقيمة البدائية هي بعض أنواع البيانات التي سنناقشها هنا.
نوع البيانات
على الرغم من أن ECMASCRIPT هي لغة ديناميكية ضعيفة من النوع يمكنها تحويل الأنواع ديناميكيًا ، إلا أنها لا تزال تحتوي على أنواع بيانات. بمعنى آخر ، يجب أن ينتمي الكائن إلى نوع حقيقي.
هناك 9 أنواع بيانات محددة في المواصفات القياسية ، ولكن 6 فقط يمكن الوصول إليها مباشرة في برامج ECMASCRIPT. هم: غير محددة ، لاغية ، منطقية ، سلسلة ، العدد ، والكائن.
لا يمكن الوصول إلى الأنواع الثلاثة الأخرى إلا على مستوى التنفيذ (لا يمكن استخدام هذه الأنواع بواسطة كائنات ECMASCRIPT) ويتم استخدامها للمواصفات لشرح بعض السلوك التشغيلي وحفظ القيم المتوسطة. هذه الأنواع الثلاثة هي: المرجع ، القائمة والإكمال.
لذلك ، يتم استخدام المرجع لشرح المشغلين مثل الحذف ، typeof ، وهذا ، ويحتوي على كائن أساسي واسم السمة ؛ تصف القائمة سلوك قائمة المعلمات (عندما تعبيرات جديدة ومكالمات الوظائف) ؛ يتم استخدام الانتهاء لشرح عبارات السلوك والمتابعة والعودة والرمي.
نوع القيمة البدائية
إذا نظرنا إلى الوراء في أنواع البيانات المستخدمة في برامج ECMASCRIPT في 6 ، فإن 5 أنواع القيمة البدائية ، بما في ذلك غير محددة ، فارغة ، منطقية ، سلسلة ، رقم ، والكائن.
مثال على نوع القيمة الأصلية:
نسخة الكود كما يلي:
var a = غير محدد ؛
var b = null ؛
var c = true ؛
var d = 'test' ؛
var e = 10 ؛
يتم تنفيذ هذه القيم مباشرة في الطبقة السفلية ، فهي ليست كائنات ، لذلك لا يوجد نموذج أولي ، لا مُنشئ.
ملاحظة العم: على الرغم من أن هذه القيم الأصلية تشبه تلك التي نستخدمها عادة (منطقية ، سلسلة ، رقم ، كائن) ، ولكنها ليست نفس الشيء. لذلك ، تختلف نتائج typeof (true) و typeof (Boolean) ، لأن نتيجة typeof (boolean) هي وظيفة ، وبالتالي فإن الوظائف المنطقية والسلسلة والرقم لها نماذج أولية (سيتم ذكر فصل سمات القراءة والكتابة التالية).
إذا كنت تريد معرفة نوع البيانات الأفضل ، فمن الأفضل استخدام typeof. هناك مثال يجب الإشارة إليه. إذا كنت تستخدم typeof للحكم على نوع فارغة ، فإن النتيجة هي كائن. لماذا؟ لأن نوع فارغة يتم تعريفها على أنها فارغة.
نسخة الكود كما يلي:
تنبيه (typeof null) ؛ // "هدف"
سبب عرض "كائن" هو أن المواصفات تنص على هذا: إرجاع "كائن" لقيمة سلسلة typeof للقيمة الفارغة.
لا تتخيل المواصفات التي تشرح ذلك ، لكن بريندان إيتش (مخترع جافا سكريبت) لاحظت أن فارغًا يستخدم في الغالب لكائنات الظهور بالنسبة إلى غير محددة ، مثل تعيين كائن إلى مرجع فارغ. ومع ذلك ، فإن بعض المستندات لديها بعض المزعجة لتنسبها إلى خطأ ، ووضع الخطأ في قائمة الأخطاء التي شارك فيها بريندان إيتش أيضًا في المناقشة. والنتيجة هي أنه إذا تركت الأمر ، فيجب عليك تعيين نتيجة typeof null على الكائن (على الرغم من أن المعيار 262-3 يحدد نوع فارغ بأنه ، وتغيير 262-5 المعيار إلى فارغ ككائن).
نوع الكائن
بعد ذلك ، فإن نوع الكائن (لا ينبغي الخلط بينه وبين مُنشئ الكائن ، والآن تتم مناقشة النوع التجريدي فقط) هو نوع البيانات الوحيد الذي يصف كائن ECMASCRIPT.
الكائن عبارة عن مجموعة غير مرتبة من أزواج القيمة الرئيسية.
الكائن عبارة عن مجموعة غير مرتبة تحتوي على أزواج القيمة الرئيسية
تسمى القيمة الرئيسية للكائن خاصية ، والخاصية هي حاوية للقيمة الأصلية والكائنات الأخرى. إذا كانت قيمة الخاصية وظيفة ، فإننا نسميها طريقة.
على سبيل المثال:
نسخة الكود كما يلي:
var x = {// الكائن "x" له 3 خصائص: a ، b ، c
ج: 10 ، // القيمة الأصلية
B: {z: 100} ، // الكائن "B" لديه سمة z
C: Function () {// function (method)
التنبيه ('method x.c') ؛
}
} ؛
تنبيه (XA) ؛ // 10
تنبيه (XB) ؛ // [كائن كائن]
تنبيه (XBZ) ؛ // 100
xc () ؛ // 'الطريقة x.c'
متحرك
كما أشرنا في الفصل 17 ، فإن الأشياء في ES ديناميكية تمامًا. هذا يعني أنه عند تنفيذ البرنامج ، يمكننا إضافة أو تعديل أو حذف خصائص الكائن في الإرادة.
على سبيل المثال:
نسخة الكود كما يلي:
var foo = {x: 10} ؛
// إضافة سمات جديدة
foo.y = 20 ؛
console.log (foo) ؛ // {x: 10 ، y: 20}
// قم بتعديل قيمة الخاصية إلى وظيفة
foo.x = function () {
console.log ('foo.x') ؛
} ؛
foo.x () ؛ // 'foo.x'
// حذف السمات
حذف foo.x ؛
console.log (foo) ؛ // {y: 20}
لا يمكن تعديل بعض الخصائص - (خصائص للقراءة فقط ، أو الخصائص المحذوفة ، أو الخصائص غير القابلة للتشكيل). سنشرح ذلك في خصائص السمة لاحقًا.
بالإضافة إلى ذلك ، تنص مواصفات ES5 على أنه لا يمكن للكائنات الثابتة تمديد سمات جديدة ، ولا يمكن حذف صفحات الممتلكات الخاصة بهم أو تعديلها. إنها ما يسمى الكائنات المجمدة ، والتي يمكن الحصول عليها عن طريق تطبيق طريقة الكائن.
نسخة الكود كما يلي:
var foo = {x: 10} ؛
// تجميد الكائن
Object.freeze (foo) ؛
console.log (object.isfrozen (foo)) ؛ // حقيقي
لا يمكن تعديل //
foo.x = 100 ؛
لا يمكن التوسع //
foo.y = 200 ؛
// لا يمكن حذفها
حذف foo.x ؛
console.log (foo) ؛ // {x: 10}
في مواصفات ES5 ، يتم استخدام طريقة الكائن.
نسخة الكود كما يلي:
var foo = {x: 10} ؛
Object.DefineProperty (foo ، "y" ، {
القيمة: 20 ،
قابلة للكتابة: خطأ ، // اقرأ فقط
قابلة للتكوين: false // غير قابل للتكوين
}) ؛
لا يمكن تعديل //
foo.y = 200 ؛
// لا يمكن حذفها
حذف foo.y ؛ // خطأ شنيع
// التوسع في الوقاية والسيطرة
Object.PreventStextensions (FOO) ؛
console.log (object.isextensEbl (foo)) ؛ // خطأ شنيع
// لا يمكن إضافة سمات جديدة
foo.z = 30 ؛
console.log (foo) ؛ {x: 10 ، y: 20}
الكائنات المدمجة والكائنات الأصلية والكائنات المضيفة
من الضروري أن نلاحظ أن المواصفات تميز أيضًا هذه الكائنات المدمجة وكائنات العناصر والكائنات المضيفة.
يتم تعريف الكائنات المدمجة وكائنات العناصر وتنفيذها بواسطة مواصفات ECMAScript ، والفرق بين الاثنين تافهة. جميع كائنات تنفيذ ECMascript هي كائنات أصلية (بعضها كائنات مدمجة ، يتم إنشاء بعضها عند تنفيذ البرنامج ، مثل الكائنات المعرفة من قبل المستخدم). الكائن المدمج هو مجموعة فرعية من الكائن الأصلي ومدمج في ecmascript قبل بدء البرنامج (على سبيل المثال ، parseint ، المطابقة ، إلخ). يتم توفير جميع الكائنات المضيفة بواسطة بيئة المضيف ، وعادة ما تكون متصفحًا ، وقد تتضمن ، على سبيل المثال ، نافذة ، تنبيه ، إلخ.
لاحظ أنه يمكن تنفيذ الكائن المضيف من قبل ES نفسه ويتوافق تمامًا مع الدلالات المعيارية. في هذا الصدد ، يمكن تسمية كائنات "مضيف أصلي" (في أقرب وقت ممكن) ، لكن المعيار لا يحدد مفهوم الكائنات "المضيف الأصلي".
كائنات منطقية وسلسلة وأرقام
بالإضافة إلى ذلك ، تحدد المواصفات أيضًا بعض فئات التفاف الخاصة الأصلية ، هذه الكائنات هي:
1. كائن منطقي
2. كائن السلسلة
3. الكائنات الرقمية
يتم إنشاء هذه الكائنات بواسطة مُنشئ مدمج مقابل ويحتوي على قيم أصلية كخصائصها الداخلية. يمكن تحويل هذه الكائنات لحفظ القيمة الأصلية والعكس بالعكس.
نسخة الكود كما يلي:
var c = new Boolean (true) ؛
var d = سلسلة جديدة ('test') ؛
var e = number number (10) ؛
// تحويل إلى القيمة الأصلية
// استخدم الوظائف بدون كلمة رئيسية جديدة
с = منطقية (ج) ؛
d = String (d) ؛
e = number (e) ؛
// إعادة الكشف عن الكائن
с = كائن (ج) ؛
d = كائن (d) ؛
e = object (e) ؛
بالإضافة إلى ذلك ، هناك كائنات تم إنشاؤها بواسطة مُنشئات مدمجة خاصة: الوظيفة (مُنشئ كائن الوظيفة) ، صفيف (مُنشئ الصفيف) ، REGEXP (مُنشئ التعبير العادي) ، MATH (وحدة الرياضيات) ، التاريخ (مُنشئ التاريخ) ، إلخ. تتم إدارة اختلافاتهم عن طريق الخصائص الداخلية. سنناقش هذه الأشياء أدناه.
حرفي
بالنسبة لقيم ثلاثة كائنات: الكائن والمصفوفة والتعبير العادي ، فقد اختصروا معرفات تسمى تهيئة الكائن ، ومهيج الصفيف ، وتهيئة التعبير العادية:
نسخة الكود كما يلي:
// ما يعادل صفيف جديد (1 ، 2 ، 3) ؛
// أو صفيف = صفيف جديد () ؛
// Array [0] = 1 ؛
// Array [1] = 2 ؛
// Array [2] = 3 ؛
var Array = [1 ، 2 ، 3] ؛
// ما يعادل
// var object = new Object () ؛
// object.a = 1 ؛
// object.b = 2 ؛
// object.c = 3 ؛
var object = {a: 1 ، b: 2 ، c: 3} ؛
// ما يعادل regexp الجديد ("^// d+$" ، "g")
var re =/^/d+$/g ؛
لاحظ أنه إذا تم إعادة تعيين الكائنات الثلاثة أعلاه إلى نوع جديد ، يتم استخدام دلالات التنفيذ اللاحقة وفقًا للنوع الجديد. على سبيل المثال ، في وحيد القرن الحالي والإصدار القديم من SpidermonKey 1.7 ، سيتم إنشاء الكائنات بنجاح مع مُنشئ الكلمة الرئيسية الجديدة ، ولكن قد لا تتغير الدلالات في بعض التطبيقات (العنكبوت/tracemonkey الحالي) بعد تغيير النوع.
نسخة الكود كما يلي:
var getClass = object.prototype.toString ؛
كائن = الرقم ؛
var foo = كائن جديد ؛
تنبيه ([foo ، getClass.call (foo)]) ؛ // 0 ، "[رقم الكائن]"
var bar = {} ؛
// Rhino ، Spidermonkey 1.7- 0 ، "[رقم الكائن]"
// الآخرين: لا يزال "[كائن كائن]" ، "[كائن كائن]"
ALERT ([bar ، getClass.Call (bar)]) ؛
// صفيف له نفس التأثير
صفيف = رقم ؛
foo = صفيف جديد ؛
تنبيه ([foo ، getClass.call (foo)]) ؛ // 0 ، "[رقم الكائن]"
شريط = [] ؛
// Rhino ، Spidermonkey 1.7- 0 ، "[رقم الكائن]"
// الآخرين: لا يزال "" ، "[كائن كائن]"
ALERT ([bar ، getClass.Call (bar)]) ؛
// ولكن بالنسبة لـ regexp ، لم يتم تغيير دلالات الحرفيين. دلالات الحرفي
// لم يتم تغييرها في جميع التطبيقات المختبرة
regexp = الرقم ؛
foo = جديد regexp ؛
تنبيه ([foo ، getClass.call (foo)]) ؛ // 0 ، "[رقم الكائن]"
شريط = /(؟!) /ز ؛
ALERT ([bar ، getClass.Call (bar)]) ؛ ///(؟!)/g ، "[regexp]"
regex الحرفيين وكائنات regexp
لاحظ أنه في المثالين التاليين ، في مواصفات الإصدار الثالث ، تكون دلالات التعبيرات العادية مكافئة. توجد حرفية RegexP فقط في جملة واحدة ويتم إنشاؤها في مرحلة التحليل. ومع ذلك ، فإن مُنشئ REGEXP ينشئ كائنًا جديدًا ، لذلك قد يتسبب ذلك في بعض المشكلات ، مثل قيمة LastIndex خاطئ أثناء الاختبار:
نسخة الكود كما يلي:
لـ (var k = 0 ؛ k <4 ؛ k ++) {
var re = /ecma /g ؛
تنبيه (re.lastindex) ؛ // 0 ، 4 ، 0 ، 4
ALERT (re.test ("ecmascript")) ؛ // صحيح ، خطأ ، حقيقي ، خطأ
}
// يقارن
لـ (var k = 0 ؛ k <4 ؛ k ++) {
var re = new regexp ("ecma" ، "g") ؛
تنبيه (re.lastindex) ؛ // 0 ، 0 ، 0 ، 0
ALERT (re.test ("ecmascript")) ؛ // صحيح ، حقيقي ، حقيقي ، حقيقي
}
ملاحظة: ومع ذلك ، تم تصحيح هذه المشكلات في مواصفات ES في الطبعة الخامسة. بغض النظر عما إذا كانت تعتمد على الحرفيين أو المُنشئين ، فإنها تنشئ كائنات جديدة.
صفيف ترابط
تسمى المناقشات الثابتة المختلفة للنص ، وكائنات javaScript (غالبًا ما يتم إنشاؤها باستخدام تهيئة الكائن {}) جداول التجزئة وجداول التجزئة أو العناوين البسيطة الأخرى: التجزئة (المفاهيم في روبي أو بيرل) ، ومصفوفات الإدارة (المفاهيم في PHP) ، والقواعد (المفاهيم في البيثون) ، إلخ.
لا يوجد سوى مثل هذه المصطلحات ، وذلك أساسًا لأن هياكلها متشابهة ، أي باستخدام أزواج "القيمة الرئيسية" لتخزين الكائنات ، والتي تتوافق تمامًا مع بنية البيانات المحددة بواسطة نظرية "مجموعة الجمعية" أو "جدول التجزئة". بالإضافة إلى ذلك ، عادةً ما تستخدم أنواع البيانات التجريدية لجدول التجزئة على مستوى التنفيذ.
ومع ذلك ، على الرغم من أن المفهوم موصوف من حيث المصطلحات ، فإن هذا خطأ في الواقع. من منظور ECMASCRIPT: يحتوي ECMASCRIPT على كائن واحد فقط ، النوع ونوعه الفرعي ، والذي لا يختلف عن "القيمة الرئيسية" إلى التخزين ، لذلك لا يوجد مفهوم خاص حول هذا. لأن الخصائص الداخلية لأي كائن يمكن تخزينها كقيمة رئيسية "أزواج:
نسخة الكود كما يلي:
var a = {x: 10} ؛
a ['y'] = 20 ؛
AZ = 30 ؛
var b = number number (1) ؛
bx = 10 ؛
بواسطة = 20 ؛
B ['Z'] = 30 ؛
var c = وظيفة جديدة ('') ؛
CX = 10 ؛
cy = 20 ؛
C ['z'] = 30 ؛
// إلخ ، النوع الفرعي لأي كائن "نوع فرعي"
علاوة على ذلك ، نظرًا لأن الكائنات يمكن أن تكون فارغة في ECMascript ، فإن مفهوم "التجزئة" غير صحيح أيضًا:
نسخة الكود كما يلي:
Object.prototype.x = 10 ؛
var a = {} ؛ // إنشاء "تجزئة" فارغة
تنبيه (a ["x"]) ؛ // 10 ، ولكن ليس فارغًا
تنبيه (A.ToString) ؛ // وظيفة
A ["y"] = 20 ؛ // أضف زوجًا جديدًا لقيمة المفاتيح إلى "التجزئة"
تنبيه (a ["y"]) ؛ // 20
Object.prototype.y = 20 ؛ // إضافة سمات النموذج الأولي
حذف A ["y"] ؛ // يمسح
تنبيه (a ["y"]) ؛ // ولكن هنا لا يزال المفتاح والقيمة 20
يرجى ملاحظة أن معيار ES5 يسمح لنا بإنشاء كائنات غير نماذج (تم تنفيذها باستخدام طريقة الكائن. من هذا المنظور ، يمكن تسمية مثل هذه الكائنات جداول التجزئة:
نسخة الكود كما يلي:
var ahashtable = object.create (null) ؛
console.log (ahashtable.toString) ؛ // غير محدد
بالإضافة إلى ذلك ، تحتوي بعض الخصائص على أساليب محددة/setter ، لذلك يمكن أن تؤدي أيضًا إلى الارتباك حول هذا المفهوم:
نسخة الكود كما يلي:
var a = سلسلة جديدة ("foo") ؛
A ["الطول"] = 10 ؛
تنبيه (A ["الطول"]) ؛ // 3
ومع ذلك ، حتى إذا كان "التجزئة" قد يكون له "نموذج أولي" (على سبيل المثال ، فئة تنفد كائنات التجزئة في Ruby أو Python) ، فإن هذا المصطلح غير صحيح في Ecmascript لأنه لا يوجد فرق دلالي بين التدوينين (أي تدوين DOT AB و A ["B"].
لم يتم فصل مفهوم "سمة الخاصية" في ECMASCRIPT بشكل دلالي عن "المفتاح" ، ومؤشر الصفيف ، والطرق. هنا ، يجب أن تتبع جميع قراءة السمات وكتابة الكائنات قاعدة موحدة: تحقق من سلسلة النموذج الأولي.
في مثال روبي التالي ، يمكننا أن نرى الفرق الدلالي:
نسخة الكود كما يلي:
a = {}
A.Class # التجزئة
A.Length # 0
# زوج جديد "القيمة الرئيسية"
A ["الطول"] = 10 ؛
# بشكل دلالي ، السمة أو الطريقة التي يتم الوصول إليها بنقطة ، وليس مفتاح
A.Length # 1
# يقوم المفهرس بالوصول إلى المفتاح في التجزئة
A ["الطول"] # 10
# إنه يشبه إعلان فئة التجزئة ديناميكيًا على كائن موجود
# ثم أعلن السمة أو الطريقة الجديدة
تجزئة الطبقة
def z
100
نهاية
نهاية
# خصائص جديدة يمكن الوصول إليها
AZ # 100
# ولكن ليس "المفتاح"
A ['Z'] # nil
لا يحدد معيار ECMA-262-3 مفهوم "التجزئة" (والمادية). ومع ذلك ، إذا كانت هناك نظرية هيكلية ، فقد يكون الكائن المسمى بعد ذلك.
تحويل الكائن
لتحويل كائن إلى قيمة بدائية ، يمكنك استخدام طريقة ValueOF. كما قلنا ، عندما يتم تسمية مُنشئ الوظيفة كدالة (لبعض الأنواع) ، ولكن إذا لم تستخدم الكلمة الرئيسية الجديدة ، فإنك تقوم بتحويل الكائن إلى القيمة الأصلية ، فهو يعادل استدعاء طريقة القيمة الضمنية:
نسخة الكود كما يلي:
var a = new number (1) ؛
var primitivea = number (a) ؛ // مكالمة "قيمة" ضمنية
var alsoprimitivea = a.valueof () ؛ // مكالمة صريحة
يُحذًِر([
Typeof A ، // "كائن"
نوع primitivea ، // "رقم"
Typeof alsoprimitivea // "رقم"
]) ؛
تتيح هذه الطريقة الكائنات المشاركة في عمليات مختلفة ، مثل:
نسخة الكود كما يلي:
var a = new number (1) ؛
var b = new number (2) ؛
تنبيه (A + B) ؛ // 3
// حتى
var c = {
X: 10 ،
Y: 20 ،
valueof: function () {
إرجاع this.x + this.y ؛
}
} ؛
var d = {
x: 30 ،
Y: 40 ،
// نفس وظيفة قيمة C
القيمة: c.valueof
} ؛
تنبيه (C + D) ؛ // 100
ستتغير القيمة الافتراضية لـ ValueOF وفقًا لنوع الكائن (إذا لم يتم تجاوزها). بالنسبة لبعض الكائنات ، فإنه يعيد هذا - على سبيل المثال: Object.prototype.valueof () ، وكذلك القيمة المحسوبة: date.prototype.valueof () إرجاع التاريخ والوقت:
نسخة الكود كما يلي:
var a = {} ؛
التنبيه (A.Valueof () === a) ؛ // صحيح ، "valueof" إرجاع هذا
var d = new Date () ؛
تنبيه (d.valueof ()) ؛ // وقت
ALERT (D.Valueof () === D.GetTime ()) ؛ // حقيقي
بالإضافة إلى ذلك ، يحتوي الكائن على تمثيل أكثر بدائية - عرض سلسلة. طريقة tostring هذه موثوقة ويتم استخدامها تلقائيًا في بعض العمليات:
نسخة الكود كما يلي:
var a = {
valueof: function () {
إرجاع 100 ؛
} ،
tostring: function () {
العودة '__test' ؛
}
} ؛
// في هذه العملية ، يتم استدعاء طريقة tostring تلقائيًا
تنبيه (أ) ؛ // "__امتحان"
// ولكن هنا ، تسمى طريقة valueof ()
تنبيه (A + 10) ؛ // 110
// ومع ذلك ، بمجرد حذف قيمة
// يمكن استدعاء tostring تلقائيًا مرة أخرى
حذف A.Valueof ؛
تنبيه (A + 10) ؛ // "_test10"
طريقة toString المحددة على الكائن. النمط الأنيق له أهمية خاصة ، ويعيد قيمة السمة الداخلية [[class]] التي سنناقشها أدناه.
بالمقارنة مع التحويل إلى القيم الأصلية (toprimitive) ، هناك أيضًا مواصفات تحويل (TOOBject) لتحويل القيم إلى أنواع الكائنات.
تتمثل الطريقة الصريحة في استخدام مُنشئ الكائن المدمج كدالة لاستدعاء Toobject (شيء مثل استخدام الكلمة الرئيسية الجديدة على ما يرام):
نسخة الكود كما يلي:
var n = object (1) ؛ // [رقم الكائن]
var s = object ('test') ؛ // [سلسلة الكائنات]
// بعض أوجه التشابه ممكن أيضًا مع المشغل الجديد
var b = كائن جديد (صحيح) ؛ // [كائن منطقي]
// إذا قمت بتطبيق كائن جديد للمعلمة ، يمكنك إنشاء كائن بسيط
var o = new Object () ؛ // [كائن كائن]
// إذا كانت المعلمة كائنًا موجودًا
// نتيجة الخلق هي ببساطة إعادة الكائن
var a = [] ؛
التنبيه (a === كائن جديد (أ)) ؛ // حقيقي
التنبيه (a === كائن (أ)) ؛ // حقيقي
فيما يتعلق باستدعاء المُنشئين المدمجين ، لا توجد قواعد مشتركة لاستخدام أو عدم تطبيق المشغل الجديد ، اعتمادًا على المنشئ. على سبيل المثال ، يستخدم المصفوفة أو الوظيفة مُنشئًا للمشغل الجديد أو وظيفة بسيطة لا تستخدم المشغل الجديد لإنتاج نفس النتيجة:
نسخة الكود كما يلي:
var a = array (1 ، 2 ، 3) ؛ // [صفيف الكائن]
var b = صفيف جديد (1 ، 2 ، 3) ؛ // [صفيف الكائن]
var c = [1 ، 2 ، 3] ؛ // [صفيف الكائن]
var d = function ('') ؛ // [وظيفة الكائن]
var e = وظيفة جديدة ('') ؛ // [وظيفة الكائن]
عند استخدام بعض المشغلين ، هناك أيضًا بعض عمليات العرض والتحويلات الضمنية:
نسخة الكود كما يلي:
var a = 1 ؛
var b = 2 ؛
// ضمنيًا
var c = a + b ؛ // 3 ، الرقم
var d = a + b + '5' // "35" ، سلسلة
// صريح
var e = '10' ؛ // "10" ، سلسلة
var f = +e ؛ // 10 ، الرقم
var g = parseint (e ، 10) ؛ // 10 ، الرقم
// إلخ
خصائص السمات
يمكن أن يكون لجميع الخصائص العديد من السمات.
1. {readonly} - تجاهل عملية الكتابة لتعيين القيم للسمات ، ولكن يمكن تغيير سمات القراءة فقط من خلال سلوك بيئة المضيف - أي أنها ليست "قيمًا ثابتة" ؛
2. {dontenum}-لا يمكن تعداد السمة من أجل .. في حلقة
3. {dontdelete}-يتم تجاهل سلوك عامل الحذف (أي أنه لا يمكن حذفه) ؛
4. {internal} - السمة الداخلية ، بدون اسم (يستخدم فقط على مستوى التنفيذ) ، لا يمكن الوصول إلى هذه السمات في ECMASCRIPT.
لاحظ أنه في ES5 {readonly} و {dontenum} و {dontdelete} تتم إعادة تسمية إلى [[[critable]] ، [[التعداد]] و [[التكوين]] ، ويمكن إدارة هذه الخصائص يدويًا من خلال object.defineProperty أو طرق مماثلة.
نسخة الكود كما يلي:
var foo = {} ؛
Object.DefineProperty (Foo ، "X" ، {
القيمة: 10 ،
قابل للكتابة: صحيح ، // أي {readonly} = false
التعداد: false ، // أي {dontenum} = true
قابلة للتكوين: true // أي {dontdelete} = false
}) ؛
console.log (foo.x) ؛ // 10
// احصل على السمات من خلال الوصف
var desc = object.getownPropertyDescriptor (foo ، "x") ؛
console.log (desc.enumerable) ؛ // خطأ شنيع
console.log (desc.writable) ؛ // حقيقي
// إلخ
الخصائص والأساليب الداخلية
يمكن أن يكون للكائنات أيضًا خصائص داخلية (جزء من مستوى التنفيذ) ، ولا يمكن لبرامج ECMASCRIPT الوصول مباشرة (لكننا سنرى أدناه أن بعض التطبيقات تسمح بالوصول إلى بعض هذه الخصائص). يتم الوصول إلى هذه الخصائص من خلال الأقواس المتداخلة [[]]. دعونا نلقي نظرة على بعض هذه الخصائص. يمكن العثور على وصف هذه الخصائص في المواصفات.
يجب على كل كائن تنفيذ الخصائص والأساليب الداخلية التالية:
1. [[النموذج الأولي]] - النموذج الأولي للكائن (الذي سيتم تقديمه بالتفصيل أدناه)
2. [[class]] - تمثيل كائن سلسلة (على سبيل المثال ، صفيف الكائن ، كائن الوظيفة ، الدالة ، إلخ) ؛ تستخدم لتمييز الكائنات
3. [[GET]]-طريقة الحصول على قيم السمة
4. [[put]]-طريقة إعداد قيم السمة
5. [[canput]] - تحقق مما إذا كانت السمة قابلة للكتابة
6. [[hasproperty]]-تحقق مما إذا كان الكائن لديه هذا الخاصية بالفعل
7. [[حذف]] - حذف هذه الخاصية من الكائن
8. [[DefaultValue]] إرجاع القيمة الأصلية للكائن (استدعاء طريقة ValueOF ، وقد يلقي بعض الكائنات استثناء من النوع).
يمكن الحصول على قيمة الخاصية الداخلية [[الفئة]] بشكل غير مباشر من خلال طريقة الكائن. على سبيل المثال:
نسخة الكود كما يلي:
var getClass = object.prototype.toString ؛
getClass.call ({}) ؛ // [كائن كائن]
getClass.call ([]) ؛ // [صفيف الكائن]
getClass.call (رقم جديد (1)) ؛ // [رقم الكائن]
// إلخ
عادةً ما يتم استخدام هذه الوظيفة للتحقق من الكائنات ، ولكن في المواصفات ، يمكن أن يكون [[class]] لكائن المضيف أي قيمة ، بما في ذلك قيمة السمة [[الفئة]] للكائن المدمج ، لذلك لا يمكن أن يكون دقيقًا 100 ٪. على سبيل المثال ، تُرجع طريقة [[class]] الخاصة بـ document.childnodes.item (...) "سلسلة" في IE ، لكن الوظيفة "التي تم إرجاعها في التطبيقات الأخرى هي بالفعل.
نسخة الكود كما يلي:
// في IE - "سلسلة" ، في وظيفة أخرى - "وظيفة"
ALERT (getClass.call (document.childnodes.item)) ؛
مُنشئ
لذلك ، كما ذكرنا أعلاه ، يتم إنشاء كائنات في ECMASCRIPT من خلال ما يسمى بمنشئات.
المُنشئ هو وظيفة تنشئ وتهيئة الكائن الذي تم إنشاؤه حديثًا.
المنشئ هو وظيفة تنشئ وتهيئة الكائنات التي تم إنشاؤها حديثًا.
إنشاء الكائنات (تخصيص الذاكرة) هو مسؤولية الطريقة الداخلية للمُنشئ [[بنية]]. يتم تعريف سلوك هذه الطريقة الداخلية ، ويستخدم جميع المنشئين هذه الطريقة لتخصيص الذاكرة للكائنات الجديدة.
تتم إدارة التهيئة عن طريق استدعاء الوظيفة لأعلى ولأسفل الكائن الجديد ، وهو المسؤول عن طريق الطريقة الداخلية للمُنشئ [[Call]].
لاحظ أنه لا يمكن الوصول إلى رمز المستخدم إلا في مرحلة التهيئة ، على الرغم من أنه يمكننا إرجاع كائنات مختلفة أثناء مرحلة التهيئة (تجاهل كائنات TIHS التي تم إنشاؤها في المرحلة الأولى):
نسخة الكود كما يلي:
وظيفة A () {
// تحديث الكائنات التي تم إنشاؤها حديثًا
this.x = 10 ؛
// لكن العودة كائن مختلف
العودة [1 ، 2 ، 3] ؛
}
var a = new a () ؛
console.log (ax ، a) ؛ غير محدد ، [1 ، 2 ، 3]
بالإشارة إلى وظائف الفصل 15 - قسم الخوارزمية لإنشاء وظائف ، يمكننا أن نرى أن الوظيفة هي كائن أصلي ، بما في ذلك خصائص [[Construct]]] و [[Call]]] وكذلك النموذج الأولي للنموذج الأولي المعروض - النموذج الأولي للكائن المستقبلي (ملاحظة: NativeObject هي مؤتمر للأشياء الأصلية الأصلية ، المستخدمة في البوزة pseudo -code).
نسخة الكود كما يلي:
f = new NativeObject () ؛
f. [[class]] = "function"
.... // سمات أخرى
f. [[call]] = <مرجع إلى الدالة> // الدالة نفسها
و. [[بنية]] = مُنشئ داخلي داخلي //
.... // سمات أخرى
// النموذج الأولي للكائن الذي تم إنشاؤه بواسطة مُنشئ F
__ObjectPrototype = {} ؛
__ObjectPrototype.constructor = f // {dontenum}
F.Prototype = __ObjectPrototype
[[Call]]] هي الطريقة الرئيسية لتمييز الكائنات باستثناء سمة [[class]] (والتي تعادل "الوظيفة" هنا) ، وبالتالي فإن السمة الداخلية [[call]] للكائن تسمى كدالة. إذا كان مثل هذا الكائن يستخدم مشغل typeof ، فإنه يعيد "وظيفة". ومع ذلك ، فإنه يرتبط أساسا بالأشياء الأصلية. في بعض الحالات ، يستخدم التنفيذ typeof للحصول على القيمة بشكل مختلف ، مثل تأثير Window.Alert (...) في IE:
نسخة الكود كما يلي:
// IE Browser - "Object" ، "Object" ، متصفحات أخرى - "وظيفة" ، "وظيفة"
ALERT (Object.prototype.toString.Call (window.alert)) ؛
تنبيه (typeof window.alert) ؛ // "هدف"
يتم تنشيط الطريقة الداخلية [[البناء]] باستخدام مُنشئ مع مشغل جديد ، كما قلنا ، مسؤول عن تخصيص الذاكرة وإنشاء الكائنات. إذا لم تكن هناك معلمات ، فيمكن أيضًا حذف الأقواس لاتصال المُنشئ:
نسخة الكود كما يلي:
الوظيفة A (x) {// constructor а
this.x = x || 10 ؛
}
// إذا لم تقم بتمرير معلمات ، فيمكن أيضًا حذف الأقواس
var a = جديد a ؛ // أو New A () ؛
تنبيه (AX) ؛ // 10
// تمرير المعلمة بشكل صريح x
var b = new A (20) ؛
تنبيه (BX) ؛ // 20
نعلم أيضًا أن SHIS في مُنشئ (مرحلة التهيئة) يتم تعيينه على الكائن الذي تم إنشاؤه حديثًا.
دعونا ندرس الخوارزمية لإنشاء الكائن.
خوارزمية إنشاء الكائن
يمكن وصف سلوك الطريقة الداخلية [[البناء]] على النحو التالي:
نسخة الكود كما يلي:
و.
o = new NativeObject () ؛
// تم تعيين الخاصية [[class]] على "كائن"
س. [[class]] = "كائن"
// احصل على الكائن G عند الإشارة إلى F.Prototype
var __ObjectPrototype = f.prototype ؛
// إذا كان __oBjectPrototype هو كائن ، ثم:
o. [[النموذج الأولي]] = __ObjectPrototype
// خلاف ذلك:
o. [[النموذج الأولي]] = Object.prototype ؛
// هنا يا. [[النموذج الأولي]] هو النموذج الأولي لكائن الكائن
// f. [[call]] يتم تطبيقه عند تهيئة الكائن الذي تم إنشاؤه حديثًا
// اضبط هذا ككائن تم إنشاؤه حديثًا
// المعلمات هي نفس المقاييس الأولية في F
r = f. [[call]] (initialParameters) ؛ هذا === o ؛
// هنا r هي قيمة إرجاع [[call]]
// في JS ، مثل هذا:
// r = f.apply (o ، initialParameters) ؛
// إذا كان R كائنًا
العودة ص
// خلاف ذلك
العودة س
يرجى ملاحظة اثنين من الميزات الرئيسية:
1. أولاً ، يتم الحصول على النموذج الأولي للكائن الذي تم إنشاؤه حديثًا من خاصية النموذج الأولي للوظيفة في الوقت الحالي (وهذا يعني أن خصائص النموذج الأولي للكائنين الذي تم إنشاؤه بواسطة نفس المنشئ يمكن أن تكون مختلفة لأن خصائص النموذج الأولي للوظيفة يمكن أن تختلف أيضًا).
2. ثانياً ، كما ذكرنا أعلاه ، إذا قام [[Call]] بإرجاع الكائن عند تهيئة الكائن ، فهذه هي بالضبط النتيجة المستخدمة للمشغل الجديد بأكمله:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype.x = 10 ؛
var a = new a () ؛
تنبيه (AX) ؛ // 10 احصل عليه من النموذج الأولي
// قم بتعيين خاصية .prototype على كائن جديد
// لماذا سيتم شرح سمة Bonstructor بشكل صريح أدناه
A.Prototype = {
مُنشئ: أ ،
Y: 100
} ؛
var b = new A () ؛
// الكائن "B" له خصائص جديدة
تنبيه (BX) ؛ // غير محدد
تنبيه (بواسطة) ؛ // 100 احصل من النموذج الأولي
// ولكن لا يزال بإمكان النموذج الأولي للكائن A الحصول على النتيجة الأصلية
تنبيه (AX) ؛ // 10 - الحصول على النموذج الأولي
الوظيفة B () {
this.x = 10 ؛
إرجاع مجموعة جديدة () ؛
}
// إذا لم يعد مُنشئ "B" (أو يرجع هذا)
// ثم يمكن استخدام هذا الكائن ، لكن الموقف التالي يعيد صفيف
var b = new b () ؛
تنبيه (BX) ؛ // غير محدد
ALERT (Object.prototype.toString.Call (B)) ؛ // [صفيف الكائن]
دعونا نتعلم المزيد عن النموذج الأولي
النموذج الأولي
كل كائن لديه نموذج أولي (باستثناء بعض كائنات النظام). يتم إجراء اتصالات النموذج الأولي من خلال الوصول الداخلي ، الضمني ، وغير المباشر إلى خصائص النموذج الأولي [[النموذج الأولي]]. يمكن أن يكون النموذج الأولي كائنًا أو قيمة خالية.
منشئ الممتلكات
المثال أعلاه له نقطتان معرفتان مهمتان. الأول يدور حول خاصية النموذج الأولي لخاصية المنشئ للوظيفة. في خوارزمية إنشاء الوظائف ، نعلم أن خاصية المنشئ يتم تعيينها كخاصية النموذج الأولي للوظيفة أثناء مرحلة إنشاء الوظائف. قيمة خاصية المنشئ هي إشارة مهمة إلى الوظيفة نفسها:
نسخة الكود كما يلي:
الدالة A () {}
var a = new a () ؛
تنبيه (A.Constructor) ؛ // الوظيفة A () {} ، عن طريق التفويض
تنبيه (a.constructor === a) ؛ // حقيقي
عادة في هذه الحالة ، هناك سوء فهم: من الخطأ بناء خاصية ككائن تم إنشاؤه حديثًا نفسه ، ولكن كما نرى ، تنتمي هذه الخاصية إلى النموذج الأولي والوصول إلى الكائن من خلال الميراث.
من خلال ورث مثيل سمة المنشئ ، يمكنك الحصول على مرجع إلى كائن النموذج الأولي بشكل غير مباشر:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype.x = رقم جديد (10) ؛
var a = new a () ؛
ALERT (A.Constructor.Prototype) ؛ // [كائن كائن]
تنبيه (AX) ؛ // 10 ، حسب النموذج الأولي
// نفس التأثير مثل. [[النموذج الأولي]]. x
ALERT (A.Constructor.Prototype.x) ؛ // 10
التنبيه (A.Constructor.Prototype.x === AX) ؛ // حقيقي
ولكن يرجى ملاحظة أنه يمكن إعادة تعريف خصائص المنشئ والنموذج الأولي للوظيفة بعد إنشاء الكائن. في هذه الحالة ، يفقد الكائن الآلية المذكورة أعلاه. إذا قمت بتحرير النموذج الأولي للنموذج الأولي لعنصر من خلال سمة النموذج الأولي للوظيفة (إضافة كائنات جديدة أو تعديل الكائنات الموجودة) ، فسترى السمة المضافة حديثًا على المثيل.
ومع ذلك ، إذا قمنا بتغيير خاصية النموذج الأولي تمامًا للدالة (عن طريق تعيين كائن جديد) ، يتم فقد المرجع إلى المنشئ الأصلي ، لأن الكائن الذي نقوم بإنشائه لا يتضمن خاصية المنشئ:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype = {
X: 10
} ؛
var a = new a () ؛
تنبيه (AX) ؛ // 10
تنبيه (a.constructor === a) ؛ // خطأ شنيع!
لذلك ، يجب استعادة الإشارات النموذجية إلى الوظائف يدويًا:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype = {
مُنشئ: أ ،
X: 10
} ؛
var a = new a () ؛
تنبيه (AX) ؛ // 10
تنبيه (a.constructor === a) ؛ // حقيقي
لاحظ أنه على الرغم من أن سمة المنشئ يتم استعادةها يدويًا ، مقارنة بالنموذج الأولي الأصلي المفقود ، إلا أن ميزة {dontenum} قد ولت ، أي أن بيان الحلقة في النمط A.Prototype لم يعد مدعومًا ، ولكن في مواصفات الإصدار الخامس ، توفر ميزة [[enumerable]] القدرة على السيطرة على محدد الحالة من خلال [[enumerable]].
نسخة الكود كما يلي:
var foo = {x: 10} ؛
Object.DefineProperty (foo ، "y" ، {
القيمة: 20 ،
التعداد: false // aka {dontenum} = true
}) ؛
console.log (foo.x ، foo.y) ؛ // 10 ، 20
لـ (var k in foo) {
console.log (k) ؛ // فقط "x"
}
var xdesc = object.getownPropertyDescriptor (foo ، "x") ؛
var ydesc = object.getownPropertyDescriptor (foo ، "y") ؛
console.log (
xdesc.Enumerable ، // صحيح
ydesc.Enumerable // false
) ؛
النموذج الأولي الصريح وخصائص [[النموذج الأولي]]
بشكل عام ، من غير الصحيح الرجوع صراحة إلى النموذج الأولي للكائن من خلال خاصية النموذج الأولي للوظيفة. يشير إلى نفس الكائن ، خاصية [[النموذج الأولي]] للكائن:
A. [[النموذج الأولي]] ---> النموذج الأولي <----
بالإضافة إلى ذلك ، يتم بالفعل الحصول على قيمة [[النموذج الأولي]] للمثال على خاصية النموذج الأولي للمُنشئ.
ومع ذلك ، فإن إرسال سمة النموذج الأولي لن يؤثر على النموذج الأولي الذي تم إنشاؤه (سيؤثر فقط على أن سمة النموذج الأولي لتغييرات المنشئ) ، أي أن الكائن الذي تم إنشاؤه حديثًا يحتوي على نموذج أولي جديد ، وسيظل الكائن الذي تم إنشاؤه يشير إلى النموذج الأولي القديم الأصلي (لم يعد بإمكان هذا النموذج الأولي تعديل).
نسخة الكود كما يلي:
// الموقف قبل تعديل النموذج الأولي للنمط A.Proty
A. [[النموذج الأولي]] ---> النموذج الأولي <----
// بعد التعديل
A.Prototype ---> نموذج أولي جديد // سيحصل كائن جديد على هذا النموذج الأولي
أ. [[النموذج الأولي]] ---> النموذج الأولي // على النموذج الأولي الأصلي للحذاء
على سبيل المثال:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype.x = 10 ؛
var a = new a () ؛
تنبيه (AX) ؛ // 10
A.Prototype = {
مُنشئ: أ ،
X: 20
Y: 30
} ؛
// الكائن A عبارة عن قيمة تم الحصول عليها من النموذج الأولي للزيت الخام من خلال مرجع ضمني [[النموذج الأولي]]
تنبيه (AX) ؛ // 10
تنبيه (AY) // غير محدد
var b = new A () ؛
// لكن الكائن الجديد هو القيمة التي تم الحصول عليها من النموذج الأولي الجديد
تنبيه (BX) ؛ // 20
تنبيه (بواسطة) // 30
لذلك ، من الخطأ أن تقول بعض المقالات أن "التعديل الديناميكي للنماذج الأولية سيؤثر على جميع الكائنات التي لها نماذج أولية جديدة" ، وأن النموذج الأولي الجديد يسري فقط على الكائنات التي تم إنشاؤها حديثًا بعد تعديل النموذج الأولي.
القاعدة الرئيسية هنا هي: يتم إنشاء النموذج الأولي للكائن عند إنشاء الكائن ، ولا يمكن تعديله في كائن جديد بعد ذلك. إذا كان لا يزال يتم الرجوع إلى نفس الكائن ، فيمكن الرجوع إليه من خلال النموذج الأولي الصريح للمقدم. بعد إنشاء الكائن ، لا يمكن إضافة أو تعديل خصائص النموذج الأولي إلا.
غير قياسي __proto__ سمات
ومع ذلك ، توفر بعض التطبيقات (مثل SpidermonKey) خصائص صريحة غير قياسية للإشارة إلى النموذج الأولي للكائن:
نسخة الكود كما يلي:
الدالة A () {}
A.Prototype.x = 10 ؛
var a = new a () ؛
تنبيه (AX) ؛ // 10
var __newprototype = {
مُنشئ: أ ،
X: 20 ،
Y: 30
} ؛
// الإشارة إلى كائن جديد
A.Prototype = __newprototype ؛
var b = new A () ؛
تنبيه (BX) ؛ // 20
تنبيه (بواسطة) ؛ // 30
// لا يزال الكائن "A" يستخدم النموذج الأولي القديم
تنبيه (AX) ؛ // 10
تنبيه (AY) ؛ // غير محدد
// تعديل النموذج الأولي صراحة
a .__ proto__ = __newprototype ؛
// الآن يشير كائن "а" إلى كائن جديد
تنبيه (AX) ؛ // 20
تنبيه (AY) ؛ // 30
注意,ES5提供了Object.getPrototypeOf(O)方法,该方法直接返回对象的[[Prototype]]属性――实例的初始原型。 然而,和__proto__相比,它只是getter,它不允许set值。
نسخة الكود كما يلي:
var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // حقيقي
对象独立于构造函数
因为实例的原型独立于构造函数和构造函数的prototype属性,构造函数完成了自己的主要工作(创建对象)以后可以删除。原型对象通过引用[[Prototype]]属性继续存在:
نسخة الكود كما يلي:
function A() {}
A.prototype.x = 10;
var a = new a () ؛
alert(ax); // 10
// 设置A为null - 显示引用构造函数
A = null;
// 但如果.constructor属性没有改变的话,
// 依然可以通过它创建对象
var b = new a.constructor();
alert(bx); // 10
// 隐式的引用也删除掉
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;
// 通过A的构造函数再也不能创建对象了
// 但这2个对象依然有自己的原型
alert(ax); // 10
alert(bx); // 10
instanceof操作符的特性
我们是通过构造函数的prototype属性来显示引用原型的,这和instanceof操作符有关。该操作符是和原型链一起工作的,而不是构造函数,考虑到这一点,当检测对象的时候往往会有误解:
نسخة الكود كما يلي:
if (foo instanceof Foo) {
...
}
这不是用来检测对象foo是否是用Foo构造函数创建的,所有instanceof运算符只需要一个对象属性――foo.[[Prototype]],在原型链中从Foo.prototype开始检查其是否存在。instanceof运算符是通过构造函数里的内部方法[[HasInstance]]来激活的。
让我们来看看这个例子:
نسخة الكود كما يلي:
function A() {}
A.prototype.x = 10;
var a = new a () ؛
alert(ax); // 10
alert(a instanceof A); // حقيقي
// 如果设置原型为null
A.prototype = null;
// ..."a"依然可以通过a.[[Prototype]]访问原型
alert(ax); // 10
// 不过,instanceof操作符不能再正常使用了
// 因为它是从构造函数的prototype属性来实现的
alert(a instanceof A); // 错误,A.prototype不是对象
另一方面,可以由构造函数来创建对象,但如果对象的[[Prototype]]属性和构造函数的prototype属性的值设置的是一样的话,instanceof检查的时候会返回true:
نسخة الكود كما يلي:
function B() {}
var b = new b () ؛
alert(b instanceof B); // حقيقي
function C() {}
var __proto = {
constructor: C
} ؛
C.prototype = __proto;
b.__proto__ = __proto;
alert(b instanceof C); // حقيقي
alert(b instanceof B); // خطأ شنيع
原型可以存放方法并共享属性
大部分程序里使用原型是用来存储对象的方法、默认状态和共享对象的属性。
事实上,对象可以拥有自己的状态,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。
نسخة الكود كما يلي:
function A(x) {
this.x = x || 100;
}
A.prototype = (function () {
// تهيئة السياق
// 使用额外的对象
var _someSharedVar = 500;
function _someHelper() {
alert('internal helper: ' + _someSharedVar);
}
function method1() {
alert('method1: ' + this.x);
}
function method2() {
alert('method2: ' + this.x);
_someHelper();
}
// 原型自身
يعود {
constructor: A,
method1: method1,
method2: method2
} ؛
}) () ؛
var a = new A(10);
var b = new A(20);
a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500
b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500
// 2个对象使用的是原型里相同的方法
alert(a.method1 === b.method1); // حقيقي
alert(a.method2 === b.method2); // حقيقي
读写属性
正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:
نسخة الكود كما يلي:
// يكتب
foo.bar = 10; // 调用了[[Put]]
console.log(foo.bar); // 10, 调用了[[Get]]
console.log(foo['bar']); // 效果一样
让我们用伪代码来看一下这些方法是如何工作的:
[[Get]]方法
[[Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。
O.[[Get]](P):
نسخة الكود كما يلي:
// 如果是自己的属性,就返回
if (O.hasOwnProperty(P)) {
return OP;
}
// 否则,继续分析原型
var __proto = O.[[Prototype]];
// 如果原型是null,返回undefined
// 这是可能的:最顶层Object.prototype.[[Prototype]]是null
if (__proto === null) {
return undefined;
}
// 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
// 直到原型为null
return __proto.[[Get]](P)
请注意,因为[[Get]]在如下情况也会返回undefined:
نسخة الكود كما يلي:
if (window.someObject) {
...
}
这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。
注意:in操作符也可以负责查找属性(也会查找原型链):
نسخة الكود كما يلي:
if ('someObject' in window) {
...
}
这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。
[[Put]]方法
[[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。
O.[[Put]](P, V):
نسخة الكود كما يلي:
// 如果不能给属性写值,就退出
if (!O.[[CanPut]](P)) {
يعود؛
}
// 如果对象没有自身的属性,就创建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
createNewProperty(O, P, attributes: {
ReadOnly: false,
DontEnum: false,
DontDelete: false,
Internal: false
});
}
// 如果属性存在就设置值,但不改变attributes特性
OP = V
يعود؛
على سبيل المثال:
نسخة الكود كما يلي:
Object.prototype.x = 100;
var foo = {};
console.log(foo.x); // 100, 继承属性
foo.x = 10; // [[يضع]]
console.log(foo.x); // 10, 自身属性
delete foo.x;
console.log(foo.x); // 重新是100,继承属性
请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。
// 例如,属性length是只读的,我们来掩盖一下length试试
function SuperString() {
/* لا شئ */
}
SuperString.prototype = new String("abc");
var foo = new SuperString();
console.log(foo.length); // 3, "abc"的长度
// 尝试掩盖
foo.length = 5;
console.log(foo.length); // 依然是3
但在ES5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。
属性访问器
内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。
نسخة الكود كما يلي:
var a = {testProperty: 10};
alert(a.testProperty); // 10, 点
alert(a['testProperty']); // 10, 索引
var propertyName = 'Property';
alert(a['test' + propertyName]); // 10, 动态属性通过索引的方式
这里有一个非常重要的特性――属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。
如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。
على سبيل المثال:
نسخة الكود كما يلي:
var a = 10; // 原始值
// 但是可以访问方法(就像对象一样)
alert(a.toString()); // "10"
// 此外,我们可以在a上创建一个心属性
a.test = 100; // 好像是没问题的
// 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
alert(a.test); // غير محدد
那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?
答案很简单:
首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:
نسخة الكود كما يلي:
// 执行a.toString()的原理:
1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;
接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:
نسخة الكود كما يلي:
// 执行a.test = 100的原理:
1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;
我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了――删除包装对象本身。
然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:
نسخة الكود كما يلي:
// 执行a.test的原理:
1. wrapper = new Number(a);
2. wrapper.test; // غير محدد
这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。
继承
我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。
如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。
经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:
نسخة الكود كما يلي:
alert(1..toString()); // "1"
我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:
1.首先,从原始值1,通过new Number(1)创建包装对象
2.然后toString方法是从这个包装对象上继承得到的
为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。
注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:
نسخة الكود كما يلي:
1.toString(); // 语法错误!
(1).toString(); // نعم
1..toString(); // نعم
1['toString'](); // نعم
原型链
让我们展示如何为用户定义对象创建原型链,非常简单:
نسخة الكود كما يلي:
وظيفة A () {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;
var a = new a () ؛
alert([ax, ay]); // 10 (自身), 20 (继承)
function B() {}
// 最近的原型链方式就是设置对象的原型为另外一个新对象
B.Prototype = جديد A () ؛
// 修复原型的constructor属性,否则的话是A了
B.prototype.constructor = B;
var b = new b () ؛
alert([bx, by]); // 10, 20, 2个都是继承的
// [[Get]] bx:
// bx (no) -->
// b.[[Prototype]].x (yes) - 10
// [[Get]] by
// by (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20
// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype
这种方法有两个特性:
首先,B.prototype将包含x属性。乍一看这可能不对,你可能会想x属性是在A里定义的并且B构造函数也是这样期望的。尽管原型继承正常情况是没问题的,但B构造函数有时候可能不需要x属性,与基于class的继承相比,所有的属性都复制到后代子类里了。
尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。
其次,这不是一个特征而是缺点――子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次――当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!
下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。
نسخة الكود كما يلي:
function A(param) {
if (!param) {
throw 'Param required';
}
this.param = param;
}
A.prototype.x = 10;
var a = new A(20);
alert([ax, a.param]); // 10 ، 20
function B() {}
B.Prototype = جديد A () ؛ // خطأ
此外,在父类的构造函数有太多代码的话也是一种缺点。
解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。
نسخة الكود كما يلي:
وظيفة A () {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;
var a = new a () ؛
alert([ax, ay]); // 10 (自身), 20 (集成)
الوظيفة B () {
// 或者使用A.apply(this, arguments)
B.superproto.constructor.apply(this, arguments);
}
// 继承:通过空的中间构造函数将原型连在一起
var F = function () {};
F.prototype = A.prototype; // يقتبس
B.prototype = new F();
B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar"
// 修复原型的constructor属性,否则的就是A了
B.prototype.constructor = B;
var b = new b () ؛
alert([bx, by]); // 10 (自身), 20 (集成)
注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。
我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。
为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。
نسخة الكود كما يلي:
function inherit(child, parent) {
var F = function () {};
F.prototype = parent.prototype
child.prototype = new F();
child.prototype.constructor = child;
child.superproto = parent.prototype;
إعادة الطفل
}
因此,继承:
نسخة الكود كما يلي:
function A() {}
A.prototype.x = 10;
function B() {}
inherit(B, A); // 连接原型
var b = new b () ؛
alert(bx); // 10, 在A.prototype查找到
也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。
例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:
نسخة الكود كما يلي:
var inherit = (function(){
function F() {}
return function (child, parent) {
F.prototype = parent.prototype;
child.prototype = new F;
child.prototype.constructor = child;
child.superproto = parent.prototype;
إعادة الطفل
} ؛
}) () ؛
由于对象的真实原型是[[Prototype]]属性,这意味着F.prototype可以很容易修改和重用,因为通过new F创建的child.prototype可以从child.prototype的当前值里获取[[Prototype]]:
نسخة الكود كما يلي:
function A() {}
A.prototype.x = 10;
function B() {}
inherit(B, A);
B.prototype.y = 20;
B.prototype.foo = function () {
alert("B#foo");
} ؛
var b = new b () ؛
alert(bx); // 10, 在A.prototype里查到
function C() {}
inherit(C, B);
// 使用"superproto"语法糖
// 调用父原型的同名方法
C.ptototype.foo = function () {
C.superproto.foo.call(this);
alert("C#foo");
} ؛
var c = new C();
alert([cx, cy]); // 10 ، 20
c.foo(); // B#foo, C#foo
注意,ES5为原型链标准化了这个工具函数,那就是Object.create方法。ES3可以使用以下方式实现:
نسخة الكود كما يلي:
Object.create ||
Object.create = function (parent, properties) {
function F() {}
F.prototype = parent;
var child = new F;
for (var k in properties) {
child[k] = properties[k].value;
}
إعادة الطفل
}
// 用法
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10 ، 20
此外,所有模仿现在基于类的经典继承方式都是根据这个原则实现的,现在可以看到,它实际上不是基于类的继承,而是连接原型的一个很方便的代码重用。
ختاماً
本章内容已经很充分和详细了,希望这些资料对你有用,并且消除你对ECMAScript的疑问,如果你有任何问题,请留言,我们一起讨论。