แนะนำ
บทนี้เป็นบทที่สองเกี่ยวกับการใช้งานเชิงวัตถุ ECMASCRIPT ในบทแรกเรากำลังพูดถึงการเปรียบเทียบระหว่างบทนำและ Cemascript หากคุณยังไม่ได้อ่านบทแรกก่อนที่จะดำเนินการกับบทนี้ฉันขอแนะนำอย่างยิ่งให้คุณอ่านบทแรกก่อนเพราะบทนี้ยาวเกินไป (หน้า 35)
ข้อความภาษาอังกฤษต้นฉบับ: http://dmitrysoshnikov.com/ecmascript/chapter-7-2-oop-ecmascript-implementation/
หมายเหตุ: เนื่องจากบทความนั้นยาวเกินไปข้อผิดพลาดจึงหลีกเลี่ยงไม่ได้และพวกเขาได้รับการแก้ไขอย่างต่อเนื่อง
ในบทนำเราขยายไปยัง ECMASCRIPT ตอนนี้เมื่อเรารู้ว่าเป็นการใช้ OOP ให้กำหนดได้อย่างถูกต้อง:
การคัดลอกรหัสมีดังนี้:
Ecmascript เป็นภาษาการเขียนโปรแกรมเชิงวัตถุที่สนับสนุนการมอบหมายมรดกตามต้นแบบ
Ecmascript เป็นภาษาที่มุ่งเน้นวัตถุที่รองรับการสืบทอดที่ได้รับมอบหมายจากต้นแบบ
เราจะวิเคราะห์ชนิดข้อมูลพื้นฐานที่สุด สิ่งแรกที่เราต้องเข้าใจคือ Ecmascript ใช้ค่าดั้งเดิมและวัตถุเพื่อแยกความแตกต่างของเอนทิตี ดังนั้น "ในจาวาสคริปต์ทุกอย่างเป็นวัตถุ" ที่กล่าวถึงในบางบทความนั้นไม่ถูกต้อง (ไม่ถูกต้องอย่างสมบูรณ์) และค่าดั้งเดิมคือบางประเภทข้อมูลที่เราจะพูดถึงที่นี่
ชนิดข้อมูล
แม้ว่า Ecmascript เป็นภาษาที่อ่อนแอแบบไดนามิกที่สามารถแปลงประเภทแบบไดนามิกได้ แต่ก็ยังมีชนิดข้อมูล กล่าวอีกนัยหนึ่งวัตถุจะต้องเป็นของจริง
มี 9 ประเภทข้อมูลที่กำหนดไว้ในข้อกำหนดมาตรฐาน แต่มีเพียง 6 เท่านั้นที่สามารถเข้าถึงได้โดยตรงในโปรแกรม ECMASCRIPT พวกเขาคือ: ไม่ได้กำหนด, โมฆะ, บูลีน, สตริง, ตัวเลขและวัตถุ
อีกสามประเภทสามารถเข้าถึงได้ในระดับการใช้งาน (ประเภทเหล่านี้ไม่สามารถใช้งานได้โดยวัตถุ ECMASCRIPT) และใช้สำหรับข้อกำหนดเพื่ออธิบายพฤติกรรมการทำงานบางอย่างและบันทึกค่ากลาง 3 ประเภทนี้คือ: การอ้างอิงรายการและความสมบูรณ์
ดังนั้นการอ้างอิงจึงใช้เพื่ออธิบายตัวดำเนินการเช่นลบประเภทของและสิ่งนี้และมีวัตถุพื้นฐานและชื่อแอตทริบิวต์ รายการอธิบายพฤติกรรมของรายการพารามิเตอร์ (เมื่อการแสดงออกและการเรียกใช้ฟังก์ชันใหม่); ความสำเร็จใช้เพื่ออธิบายการแบ่งพฤติกรรมดำเนินการต่อส่งคืนและโยนงบ
ประเภทค่าดั้งเดิม
เมื่อมองย้อนกลับไปที่ประเภทข้อมูลที่ใช้ในโปรแกรม ECMASCRIPT ใน 6 ซึ่งเป็น 5 ประเภทแรกเป็นประเภทค่าดั้งเดิมรวมถึง Undefined, Null, Boolean, String, Number และ Object
ตัวอย่างประเภทค่าดั้งเดิม:
การคัดลอกรหัสมีดังนี้:
var a = ไม่ได้กำหนด;
var b = null;
var c = true;
var d = 'ทดสอบ';
var e = 10;
ค่าเหล่านี้ถูกนำไปใช้โดยตรงที่ชั้นล่างพวกเขาไม่ใช่วัตถุดังนั้นจึงไม่มีต้นแบบไม่มีตัวสร้าง
ลุงหมายเหตุ: แม้ว่าค่าดั้งเดิมเหล่านี้จะคล้ายกับค่าที่เรามักจะใช้ (บูลีน, สตริง, ตัวเลข, วัตถุ) แต่ไม่เหมือนกัน ดังนั้นผลลัพธ์ของ typeof (true) และ typeof (boolean) จึงแตกต่างกันเนื่องจากผลลัพธ์ของ typeof (boolean) เป็นฟังก์ชันดังนั้นฟังก์ชันบูลีนสตริงและจำนวนมีต้นแบบ (บทที่อ่านและเขียนต่อไปนี้จะถูกกล่าวถึง)
หากคุณต้องการทราบว่าข้อมูลประเภทใดที่ดีที่สุดควรใช้ typeof มีตัวอย่างที่จำเป็นต้องสังเกต หากคุณใช้ typeof เพื่อตัดสินประเภทของ null ผลลัพธ์คือวัตถุ ทำไม เพราะประเภทของโมฆะถูกกำหนดให้เป็นโมฆะ
การคัดลอกรหัสมีดังนี้:
การแจ้งเตือน (typeof null); // "วัตถุ"
เหตุผลในการแสดง "วัตถุ" เป็นเพราะข้อมูลจำเพาะกำหนดสิ่งนี้: ส่งคืน "วัตถุ" สำหรับค่าสตริง typeof ของค่า NULL
ข้อกำหนดไม่ได้จินตนาการถึงการอธิบายสิ่งนี้ แต่เบรนแดนไอค (ผู้ประดิษฐ์ JavaScript) สังเกตเห็นว่า NULL ส่วนใหญ่ใช้สำหรับวัตถุที่จะปรากฏเมื่อเทียบกับ undefined เช่นการตั้งค่าวัตถุเพื่ออ้างอิงโมฆะ อย่างไรก็ตามเอกสารบางฉบับมีบางอย่างที่น่ารำคาญที่จะให้ความสำคัญกับข้อผิดพลาดและใส่ข้อผิดพลาดในรายการข้อผิดพลาดที่เบรนแดนไอคเข้าร่วมในการสนทนา ผลลัพธ์คือถ้าคุณปล่อยมันไปคุณควรตั้งค่าผลลัพธ์ของ typeof null เป็นวัตถุ (แม้ว่ามาตรฐาน 262-3 จะกำหนดประเภทของ null เป็น null และ 262-5 ได้เปลี่ยนมาตรฐานเป็น null เป็นวัตถุ)
ประเภทวัตถุ
ถัดไปประเภทวัตถุ (ไม่ต้องสับสนกับตัวสร้างวัตถุตอนนี้เฉพาะประเภทนามธรรมเท่านั้นที่กล่าวถึง) เป็นชนิดข้อมูลเดียวที่อธิบายวัตถุ ECMASCRIPT
Object เป็นคอลเลกชันที่ไม่มีการเรียงลำดับของคู่คีย์
Object เป็นคอลเลกชันที่ไม่มีการจัดเรียงที่มีคู่คีย์-ค่า
ค่าคีย์ของวัตถุเรียกว่าคุณสมบัติและคุณสมบัติเป็นคอนเทนเนอร์ของค่าดั้งเดิมและวัตถุอื่น ๆ หากค่าของคุณสมบัติเป็นฟังก์ชันเราเรียกมันว่าวิธีการ
ตัวอย่างเช่น:
การคัดลอกรหัสมีดังนี้:
var x = {// วัตถุ "x" มี 3 คุณสมบัติ: a, b, c
A: 10, // ค่าดั้งเดิม
b: {z: 100}, // วัตถุ "b" มีแอตทริบิวต์ z
C: function () {// function (วิธีการ)
การแจ้งเตือน ('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 ยังกำหนดว่าวัตถุคงที่ไม่สามารถขยายแอตทริบิวต์ใหม่และหน้าคุณสมบัติของพวกเขาไม่สามารถลบหรือแก้ไขได้ พวกมันเรียกว่าวัตถุแช่แข็งซึ่งสามารถรับได้โดยใช้วิธีการใช้วัตถุ Freeze (O)
การคัดลอกรหัสมีดังนี้:
var foo = {x: 10};
// ตรึงวัตถุ
Object.feeze (foo);
console.log (Object.isfrozen (foo)); // จริง
// ไม่สามารถแก้ไขได้
foo.x = 100;
// ไม่สามารถขยายได้
foo.y = 200;
// ไม่สามารถลบได้
ลบ foo.x;
console.log (foo); // {x: 10}
ในข้อกำหนด ES5 วิธีการ Object.PreventExtensions (O) ยังใช้เพื่อป้องกันการขยายหรือวิธีการ defineProperty (O) ใช้เพื่อกำหนดคุณสมบัติ:
การคัดลอกรหัสมีดังนี้:
var foo = {x: 10};
Object.defineProperty (foo, "y", {
ค่า: 20,
เขียนได้: เท็จ // อ่านเท่านั้น
สามารถกำหนดค่าได้: FALSE // ไม่สามารถกำหนดค่าได้
-
// ไม่สามารถแก้ไขได้
foo.y = 200;
// ไม่สามารถลบได้
ลบ foo.y; // เท็จ
// การขยายและควบคุมการขยายตัว
Object.preventExtensions (foo);
console.log (object.isextensible (foo)); // เท็จ
// ไม่สามารถเพิ่มแอตทริบิวต์ใหม่ได้
foo.z = 30;
console.log (foo); {x: 10, y: 20}
วัตถุในตัววัตถุดั้งเดิมและวัตถุโฮสต์
มีความจำเป็นที่จะต้องทราบว่าข้อมูลจำเพาะยังแยกความแตกต่างของวัตถุในตัววัตถุองค์ประกอบและวัตถุโฮสต์เหล่านี้
วัตถุในตัวและวัตถุองค์ประกอบถูกกำหนดและนำไปใช้โดยข้อกำหนด ECMAScript และความแตกต่างระหว่างทั้งสองนั้นเป็นเรื่องเล็กน้อย วัตถุที่ใช้งาน ECMAScript ทั้งหมดเป็นวัตถุดั้งเดิม (บางส่วนเป็นวัตถุในตัวบางชิ้นถูกสร้างขึ้นเมื่อโปรแกรมถูกดำเนินการเช่นวัตถุที่ผู้ใช้กำหนด) วัตถุในตัวเป็นชุดย่อยของวัตถุดั้งเดิมและถูกสร้างขึ้นใน eCmascript ก่อนที่โปรแกรมจะเริ่มต้น (ตัวอย่างเช่น parseint, Match ฯลฯ ) วัตถุโฮสต์ทั้งหมดจัดทำโดยสภาพแวดล้อมโฮสต์โดยปกติจะเป็นเบราว์เซอร์และอาจรวมถึงตัวอย่างเช่นหน้าต่างการแจ้งเตือน ฯลฯ
โปรดทราบว่าวัตถุโฮสต์อาจถูกนำไปใช้โดย ES เองและสอดคล้องกับความหมายเชิงบรรทัดฐานอย่างเต็มที่ ในเรื่องนี้พวกเขาสามารถเรียกว่าวัตถุ "โฮสต์ดั้งเดิม" (โดยเร็วที่สุด) แต่บรรทัดฐานไม่ได้กำหนดแนวคิดของวัตถุ "โฮสต์ดั้งเดิม"
บูลีนสตริงและตัวเลขวัตถุ
นอกจากนี้ข้อมูลจำเพาะยังกำหนดคลาส wrapper พิเศษบางส่วนวัตถุเหล่านี้คือ:
1. วัตถุบูลีน
2. วัตถุสตริง
3. วัตถุดิจิตอล
วัตถุเหล่านี้ถูกสร้างขึ้นโดยตัวสร้างในตัวที่สอดคล้องกันและมีค่าดั้งเดิมเป็นคุณสมบัติภายใน วัตถุเหล่านี้สามารถแปลงเพื่อบันทึกค่าดั้งเดิมและในทางกลับกัน
การคัดลอกรหัสมีดังนี้:
var c = บูลีนใหม่ (จริง);
var d = สตริงใหม่ ('ทดสอบ');
var e = หมายเลขใหม่ (10);
// แปลงเป็นค่าดั้งเดิม
// ใช้ฟังก์ชั่นโดยไม่มีคำหลักใหม่
с = บูลีน (c);
d = สตริง (d);
e = number (e);
// กลับไปที่วัตถุ
с = วัตถุ (c);
d = วัตถุ (d);
e = วัตถุ (e);
นอกจากนี้ยังมีวัตถุที่สร้างขึ้นโดยตัวสร้างในตัวพิเศษ: ฟังก์ชั่น (ตัวสร้างวัตถุฟังก์ชั่น), อาร์เรย์ (ตัวสร้างอาร์เรย์), regexp (ตัวสร้างนิพจน์ทั่วไป), คณิตศาสตร์ (โมดูลคณิตศาสตร์), วันที่ (ตัวสร้างวันที่) ฯลฯ วัตถุเหล่านี้ยังเป็นค่าของประเภทวัตถุ ความแตกต่างของพวกเขาได้รับการจัดการโดยคุณสมบัติภายใน เราจะพูดถึงสิ่งเหล่านี้ด้านล่าง
ตามตัวอักษร
สำหรับค่าของวัตถุสามชิ้น: วัตถุอาร์เรย์และนิพจน์ทั่วไปพวกเขามีตัวบ่งชี้ตัวย่อที่เรียกว่าวัตถุเริ่มต้นวัตถุ, array initializer และการเริ่มต้นการแสดงออกปกติ:
การคัดลอกรหัสมีดังนี้:
// เทียบเท่ากับอาร์เรย์ใหม่ (1, 2, 3);
// หรือ array = new Array ();
// อาร์เรย์ [0] = 1;
// อาร์เรย์ [1] = 2;
// อาร์เรย์ [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 วัตถุจะถูกสร้างขึ้นอย่างประสบความสำเร็จด้วยตัวสร้างคำหลักใหม่ แต่ความหมายของการใช้งานบางอย่าง (Spider/Tracemonkey ปัจจุบัน) อาจไม่เปลี่ยนแปลงหลังจากประเภทเปลี่ยนไป
การคัดลอกรหัสมีดังนี้:
var getClass = object.prototype.toString;
วัตถุ = จำนวน;
var foo = วัตถุใหม่;
การแจ้งเตือน ([foo, getclass.call (foo)]); // 0, "[หมายเลขวัตถุ]"
var bar = {};
// rhino, Spidermonkey 1.7- 0, "[หมายเลขวัตถุ]"
// อื่น ๆ : ยังคง "[วัตถุวัตถุ]", "[วัตถุวัตถุ]"
การแจ้งเตือน ([bar, getclass.call (bar)]);
// อาร์เรย์มีเอฟเฟกต์เดียวกัน
array = number;
foo = อาร์เรย์ใหม่;
การแจ้งเตือน ([foo, getclass.call (foo)]); // 0, "[หมายเลขวัตถุ]"
bar = [];
// rhino, Spidermonkey 1.7- 0, "[หมายเลขวัตถุ]"
// อื่น ๆ : ยังคง "", "[วัตถุวัตถุ]"
การแจ้งเตือน ([bar, getclass.call (bar)]);
// แต่สำหรับ regexp ความหมายของตัวอักษรจะไม่เปลี่ยนแปลง ความหมายของตัวอักษร
// จะไม่เปลี่ยนแปลงในการใช้งานที่ผ่านการทดสอบทั้งหมด
regexp = number;
foo = ใหม่ regexp;
การแจ้งเตือน ([foo, getclass.call (foo)]); // 0, "[หมายเลขวัตถุ]"
bar = /(?!) /g;
การแจ้งเตือน ([bar, getclass.call (bar)]); ///(?!)/g, "[Object regexp]"
ตัวอักษร Regex และวัตถุ regexp
โปรดทราบว่าในสองตัวอย่างต่อไปนี้ในข้อกำหนดของรุ่นที่สามความหมายของนิพจน์ปกติจะเทียบเท่า ตัวอักษร regexp มีอยู่ในประโยคเดียวเท่านั้นและถูกสร้างขึ้นในขั้นตอนการแยกวิเคราะห์ อย่างไรก็ตามตัวสร้าง Regexp สร้างวัตถุใหม่ดังนั้นสิ่งนี้อาจทำให้เกิดปัญหาบางอย่างเช่นค่า LastIndex นั้นผิดระหว่างการทดสอบ:
การคัดลอกรหัสมีดังนี้:
สำหรับ (var k = 0; k <4; k ++) {
var re = /ecma /g;
การแจ้งเตือน (re.lastindex); // 0, 4, 0, 4
การแจ้งเตือน (re.test ("ecmascript")); // true, false, true, false
-
// เปรียบเทียบ
สำหรับ (var k = 0; k <4; k ++) {
var re = ใหม่ regexp ("ecma", "g");
การแจ้งเตือน (re.lastindex); // 0, 0, 0, 0
การแจ้งเตือน (re.test ("ecmascript")); // จริงจริงจริงจริงจริง
-
หมายเหตุ: อย่างไรก็ตามปัญหาเหล่านี้ได้รับการแก้ไขในข้อกำหนด ES ในรุ่นที่ 5 ไม่ว่าพวกเขาจะขึ้นอยู่กับตัวอักษรหรือตัวสร้างหรือไม่พวกเขาสร้างวัตถุใหม่
อาร์เรย์เชื่อมโยง
การอภิปรายแบบคงที่ต่าง ๆ ของข้อความวัตถุ JavaScript (มักจะสร้างขึ้นด้วยวัตถุเริ่มต้นวัตถุ {}) เรียกว่าตารางแฮชและตารางแฮชหรือชื่อง่าย ๆ อื่น ๆ : แฮช (แนวคิดในทับทิมหรือเพอร์ล), อาร์เรย์การจัดการ (แนวคิดใน PHP), พจนานุกรม (แนวคิดใน Python) ฯลฯ )
มีเพียงคำศัพท์ดังกล่าวส่วนใหญ่เป็นเพราะโครงสร้างของพวกเขาคล้ายกันนั่นคือการใช้คู่ "คีย์-ค่า" เพื่อจัดเก็บวัตถุซึ่งสอดคล้องกับโครงสร้างข้อมูลที่กำหนดโดยทฤษฎี "Association Array" หรือ "Hash Table" นอกจากนี้ประเภทข้อมูลนามธรรมของตารางแฮชมักจะใช้ในระดับการใช้งาน
อย่างไรก็ตามแม้ว่าแนวคิดจะอธิบายไว้ในแง่ของคำนี้ แต่นี่เป็นความผิดพลาด จากมุมมองของ ECMASCript: ECMASCript มีวัตถุเพียงชิ้นเดียวประเภทและชนิดย่อยซึ่งไม่แตกต่างจาก "คีย์-ค่า" ไปยังการจัดเก็บดังนั้นจึงไม่มีแนวคิดพิเศษเกี่ยวกับเรื่องนี้ เนื่องจากคุณสมบัติภายในของวัตถุใด ๆ สามารถเก็บไว้เป็นคู่คีย์-ค่า ": คู่:
การคัดลอกรหัสมีดังนี้:
var a = {x: 10};
a ['y'] = 20;
AZ = 30;
var b = หมายเลขใหม่ (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; // เพิ่มแอตทริบิวต์ต้นแบบ
ลบ ["y"]; // ลบ
การแจ้งเตือน (a ["y"]); // แต่นี่คือคีย์และคุณค่ายังคงมีค่า 20
โปรดทราบว่ามาตรฐาน ES5 ช่วยให้เราสามารถสร้างวัตถุที่ไม่ได้ใช้งาน (นำไปใช้โดยใช้วิธี Object.create (NULL)) จากมุมมองนี้วัตถุดังกล่าวสามารถเรียกได้ว่าตารางแฮช:
การคัดลอกรหัสมีดังนี้:
var ahashtable = object.create (null);
console.log (ahashtable.toString); // ไม่ได้กำหนด
นอกจากนี้คุณสมบัติบางอย่างมีวิธีการ getter/setter เฉพาะดังนั้นจึงสามารถนำไปสู่ความสับสนเกี่ยวกับแนวคิดนี้:
การคัดลอกรหัสมีดังนี้:
var a = สตริงใหม่ ("foo");
A ['ความยาว'] = 10;
การแจ้งเตือน (a ['ความยาว']); // 3
อย่างไรก็ตามแม้ว่า "แฮช" อาจมี "ต้นแบบ" (ตัวอย่างเช่นคลาสที่มอบหมายวัตถุแฮชในทับทิมหรือหลาม) คำนี้ไม่ถูกต้องใน ecmascript เพราะไม่มีความแตกต่างทางความหมายระหว่างสองสัญกรณ์
แนวคิดของ "แอตทริบิวต์คุณสมบัติ" ใน ECMASCript ไม่ได้แยกความหมายออกจาก "คีย์" ดัชนีอาร์เรย์และวิธีการ ที่นี่การอ่านแอตทริบิวต์ทั้งหมดและการเขียนวัตถุจะต้องปฏิบัติตามกฎแบบครบวงจร: ตรวจสอบห่วงโซ่ต้นแบบ
ในตัวอย่างทับทิมต่อไปนี้เราสามารถเห็นความแตกต่างทางความหมาย:
การคัดลอกรหัสมีดังนี้:
a = {}
A.class # แฮช
A. ความยาว # 0
# คู่ "คีย์-ค่า" คู่ใหม่
A ['ความยาว'] = 10;
# ความหมายแอตทริบิวต์หรือวิธีการที่เข้าถึงได้ด้วยจุดไม่ใช่คีย์
A. ความยาว # 1
# ตัวทำดัชนีเข้าถึงคีย์ในแฮช
A ['ความยาว'] # 10
# มันคล้ายกับการประกาศคลาสแฮชแบบไดนามิกบนวัตถุที่มีอยู่
# จากนั้นประกาศแอตทริบิวต์หรือวิธีการใหม่
แฮชคลาส
def z
100
จบ
จบ
# คุณสมบัติใหม่สามารถเข้าถึงได้
AZ # 100
# แต่ไม่ใช่ "คีย์"
a ['z'] # nil
มาตรฐาน ECMA-262-3 ไม่ได้กำหนดแนวคิดของ "แฮช" (และคล้ายกัน) อย่างไรก็ตามหากมีทฤษฎีโครงสร้างวัตถุที่ตั้งชื่อตามนี้อาจเป็น
การแปลงวัตถุ
ในการแปลงวัตถุเป็นค่าดั้งเดิมคุณสามารถใช้วิธีการของค่า ดังที่เราได้กล่าวไว้เมื่อตัวสร้างของฟังก์ชั่นเรียกว่าเป็นฟังก์ชัน (สำหรับบางประเภท) แต่ถ้าคุณไม่ใช้คำหลักใหม่คุณจะแปลงวัตถุเป็นค่าดั้งเดิมมันจะเทียบเท่ากับการเรียกค่าเฉลี่ยโดยนัย:
การคัดลอกรหัสมีดังนี้:
var a = หมายเลขใหม่ (1);
var primitivea = number (a); // การโทร "โดยปริยาย
var alsoprimitivea = a.valueof (); // การโทรที่ชัดเจน
เตือน([
typeof a, // "วัตถุ"
ประเภทของ primitivea, // "number"
typeof alsoprimitivea // "number"
-
วิธีนี้ช่วยให้วัตถุมีส่วนร่วมในการดำเนินการต่าง ๆ เช่น:
การคัดลอกรหัสมีดังนี้:
var a = หมายเลขใหม่ (1);
var b = หมายเลขใหม่ (2);
การแจ้งเตือน (a + b); // 3
// สม่ำเสมอ
var c = {
x: 10,
y: 20,
valueof: function () {
ส่งคืนสิ่งนี้ x + this.y;
-
-
var d = {
x: 30
y: 40,
// ฟังก์ชั่นเดียวกับค่าของ C
ค่าของ: c.valueof
-
การแจ้งเตือน (C + D); // 100
ค่าเริ่มต้นของค่าของค่าจะเปลี่ยนแปลงตามประเภทของวัตถุ (ถ้าไม่ถูกแทนที่) สำหรับวัตถุบางอย่างจะส่งคืนสิ่งนี้ - ตัวอย่างเช่น: object.prototype.valueof () และค่าที่คำนวณได้: date.prototype.valueof () ส่งคืนวันที่และเวลา:
การคัดลอกรหัสมีดังนี้:
var a = {};
การแจ้งเตือน (a.valueof () === a); // true, "valueof" ส่งคืนสิ่งนี้
var d = วันที่ใหม่ ();
การแจ้งเตือน (d.valueof ()); // เวลา
การแจ้งเตือน (d.valueof () === d.gettime ()); // จริง
นอกจากนี้วัตถุยังมีการแสดงแบบดั้งเดิมมากขึ้น - แสดงสตริง วิธีการ toString นี้มีความน่าเชื่อถือและใช้โดยอัตโนมัติในการดำเนินการบางอย่าง:
การคัดลอกรหัสมีดังนี้:
var a = {
valueof: function () {
กลับ 100;
-
ToString: function () {
กลับ '__test';
-
-
// ในการดำเนินการนี้วิธีการ toString จะถูกเรียกโดยอัตโนมัติ
การแจ้งเตือน (a); // "__ทดสอบ"
// แต่ที่นี่วิธีการ Valueof () เรียกว่า
การแจ้งเตือน (A + 10); // 110
// อย่างไรก็ตามเมื่อลบค่าของการลบ
// ToString สามารถเรียกได้โดยอัตโนมัติอีกครั้ง
ลบ a.valueof;
การแจ้งเตือน (A + 10); // "_test10"
วิธีการ ToString ที่กำหนดไว้ใน Object.prototype มีความสำคัญเป็นพิเศษและจะส่งคืนค่าแอตทริบิวต์ [[คลาส]] ภายในที่เราจะพูดถึงด้านล่าง
เมื่อเปรียบเทียบกับการแปลงเป็นค่าดั้งเดิม (toprimitive) นอกจากนี้ยังมีข้อกำหนดการแปลง (toObject) เพื่อแปลงค่าเป็นประเภทวัตถุ
วิธีการที่ชัดเจนคือการใช้ตัวสร้างวัตถุในตัวเป็นฟังก์ชันที่จะเรียก toobject (สิ่งต่าง ๆ เช่นการใช้คำหลักใหม่คือตกลง):
การคัดลอกรหัสมีดังนี้:
var n = วัตถุ (1); // [หมายเลขวัตถุ]
var s = วัตถุ ('ทดสอบ'); // [สตริงวัตถุ]
// ความคล้ายคลึงกันบางอย่างก็เป็นไปได้ด้วยผู้ให้บริการใหม่
var b = วัตถุใหม่ (จริง); // [วัตถุบูลีน]
// ถ้าคุณใช้วัตถุพารามิเตอร์ใหม่คุณจะสร้างวัตถุอย่างง่าย
var o = วัตถุใหม่ (); // [วัตถุวัตถุ]
// ถ้าพารามิเตอร์เป็นวัตถุที่มีอยู่
// ผลลัพธ์ของการสร้างคือเพียงแค่ส่งคืนวัตถุ
var a = [];
การแจ้งเตือน (a === วัตถุใหม่ (a)); // จริง
การแจ้งเตือน (a === วัตถุ (a)); // จริง
เกี่ยวกับการเรียกตัวสร้างในตัวไม่มีกฎทั่วไปสำหรับการใช้หรือไม่ใช้ผู้ให้บริการใหม่ขึ้นอยู่กับตัวสร้าง ตัวอย่างเช่นอาร์เรย์หรือฟังก์ชั่นใช้ตัวสร้างของตัวดำเนินการใหม่หรือฟังก์ชั่นง่าย ๆ ที่ไม่ได้ใช้ตัวดำเนินการใหม่เพื่อสร้างผลลัพธ์เดียวกัน:
การคัดลอกรหัสมีดังนี้:
var a = อาร์เรย์ (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. {อ่านอย่างเดียว} - ละเว้นการดำเนินการเขียนของการกำหนดค่าให้กับแอตทริบิวต์ แต่แอตทริบิวต์แบบอ่านอย่างเดียวสามารถเปลี่ยนแปลงได้โดยพฤติกรรมสภาพแวดล้อมของโฮสต์ - นั่นคือพวกเขาไม่ใช่ "ค่าคงที่";
2. {dontenum}-แอตทริบิวต์ไม่สามารถระบุได้โดยสำหรับ .. ในลูป
3. {DontDelete}-พฤติกรรมของตัวดำเนินการลบถูกละเว้น (นั่นคือมันไม่สามารถลบได้);
4. {ภายใน} - แอตทริบิวต์ภายในโดยไม่มีชื่อ (ใช้เฉพาะในระดับการใช้งาน) แอตทริบิวต์ดังกล่าวไม่สามารถเข้าถึงได้ใน ECMASCript
โปรดทราบว่าใน ES5 {READONLY}, {DonTenum} และ {DontDelete} ถูกเปลี่ยนชื่อเป็น [[writable]], [[enumerable]] และ [[กำหนดค่า]] และคุณสมบัติเหล่านี้สามารถจัดการได้ด้วยตนเองผ่าน Object.DefineProperty หรือวิธีการที่คล้ายกัน
การคัดลอกรหัสมีดังนี้:
var foo = {};
Object.defineProperty (foo, "x", {
ค่า: 10,
เขียนได้: จริง, // เช่น {อ่านอย่างง่าย ๆ } = false
enumerable: 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. [[คลาส]] - การแสดงวัตถุสตริง (ตัวอย่างเช่นอาร์เรย์วัตถุวัตถุฟังก์ชันฟังก์ชัน ฯลฯ ); ใช้เพื่อแยกความแตกต่างของวัตถุ
3. [[รับ]]-วิธีการรับค่าแอตทริบิวต์
4. [[ใส่]]-วิธีการตั้งค่าแอตทริบิวต์
5. [[canput]] - ตรวจสอบว่าแอตทริบิวต์นั้นสามารถเขียนได้หรือไม่
6. [[HasProperty]]-ตรวจสอบว่าวัตถุนั้นมีคุณสมบัตินี้อยู่แล้ว
7. [[ลบ]] - ลบคุณสมบัตินี้ออกจากวัตถุ
8. [[defaultValue]] ส่งคืนค่าดั้งเดิมของวัตถุสำหรับ (เรียกค่า Valueof และวัตถุบางอย่างอาจส่งข้อยกเว้น TypeError)
ค่าของคุณสมบัติภายใน [[class]] สามารถรับทางอ้อมผ่านวิธีการผ่านวัตถุ prototype.toString () ซึ่งควรส่งคืนสตริงต่อไปนี้: "[object" + [[class]] + "]" ตัวอย่างเช่น:
การคัดลอกรหัสมีดังนี้:
var getClass = object.prototype.toString;
getClass.call ({}); // [วัตถุวัตถุ]
getclass.call ([]); // [อาร์เรย์วัตถุ]
getClass.CALL (หมายเลขใหม่ (1)); // [หมายเลขวัตถุ]
// ฯลฯ
ฟังก์ชั่นนี้มักจะใช้เพื่อตรวจสอบวัตถุ แต่ในข้อกำหนด [[คลาส]] ของวัตถุโฮสต์อาจเป็นค่าใด ๆ รวมถึงค่าของแอตทริบิวต์ [[คลาส]] ของวัตถุในตัวดังนั้นในทางทฤษฎีจึงไม่สามารถแม่นยำได้ 100% ตัวอย่างเช่นคุณสมบัติ [[class]] ของ document.childnodes.item (... ) ส่งคืน "สตริง" ใน IE แต่ฟังก์ชั่น "ที่ส่งคืน" ในการใช้งานอื่น ๆ
การคัดลอกรหัสมีดังนี้:
// ใน IE - "String" ในอื่น - "ฟังก์ชั่น"
การแจ้งเตือน (getclass.call (document.childnodes.item));
ตัวสร้าง
ดังนั้นดังที่เราได้กล่าวไว้ข้างต้นวัตถุใน eCmascript ถูกสร้างขึ้นผ่านตัวสร้างที่เรียกว่า
ตัวสร้างเป็นฟังก์ชั่นที่สร้างและเริ่มต้นวัตถุที่สร้างขึ้นใหม่
ตัวสร้างเป็นฟังก์ชั่นที่สร้างและเริ่มต้นวัตถุที่สร้างขึ้นใหม่
การสร้างวัตถุ (การจัดสรรหน่วยความจำ) เป็นความรับผิดชอบของวิธีการภายในของตัวสร้าง [[construct]] พฤติกรรมของวิธีการภายในนี้ถูกกำหนดและตัวสร้างทั้งหมดใช้วิธีนี้เพื่อจัดสรรหน่วยความจำสำหรับวัตถุใหม่
การเริ่มต้นได้รับการจัดการโดยการเรียกใช้ฟังก์ชันขึ้นและลงวัตถุใหม่ซึ่งรับผิดชอบโดยวิธีการภายในของตัวสร้าง [[call]]
โปรดทราบว่ารหัสผู้ใช้สามารถเข้าถึงได้ในขั้นตอนการเริ่มต้นเท่านั้นแม้ว่าเราสามารถส่งคืนวัตถุต่าง ๆ ในระหว่างขั้นตอนการเริ่มต้น (ไม่สนใจวัตถุ TIHS ที่สร้างขึ้นในเฟสแรก):
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {
// อัปเดตวัตถุที่สร้างขึ้นใหม่
this.x = 10;
// แต่ผลตอบแทนเป็นวัตถุที่แตกต่างกัน
return [1, 2, 3];
-
var a = new a ();
console.log (ขวาน, a); ไม่ได้กำหนด [1, 2, 3]
อ้างถึงบทที่ 15 ฟังก์ชั่น - ส่วนอัลกอริทึมสำหรับการสร้างฟังก์ชั่นเราจะเห็นว่าฟังก์ชั่นเป็นวัตถุดั้งเดิมรวมถึง [[construct]]] และ [[call]]] คุณสมบัติเช่นเดียวกับต้นแบบต้นแบบของต้นแบบที่แสดง - ต้นแบบของวัตถุในอนาคต
การคัดลอกรหัสมีดังนี้:
f = new NativeObject ();
f. [[คลาส]] = "ฟังก์ชั่น"
.... // คุณลักษณะอื่น ๆ
f. [[call]] = <อ้างอิงถึงฟังก์ชัน> // ฟังก์ชันเอง
f. [[construct]] = InternalConstructor // constructor ภายในสามัญ
.... // คุณลักษณะอื่น ๆ
// ต้นแบบวัตถุที่สร้างขึ้นโดย F constructor
__objectPrototype = {};
__objectPrototype.Constructor = f // {dontenum}
f.prototype = __objectPrototype
[[CALL]]] เป็นวิธีหลักในการแยกแยะวัตถุยกเว้นแอตทริบิวต์ [[คลาส]] (ซึ่งเทียบเท่ากับ "ฟังก์ชั่น" ที่นี่) ดังนั้นคุณลักษณะภายใน [[CALL]] ของวัตถุเรียกว่าเป็นฟังก์ชัน หากวัตถุดังกล่าวใช้ตัวดำเนินการ typeof มันจะส่งคืน "ฟังก์ชั่น" อย่างไรก็ตามส่วนใหญ่เกี่ยวข้องกับวัตถุดั้งเดิม ในบางกรณีการใช้งานจะใช้ typeof เพื่อให้ได้ค่าที่แตกต่างกันเช่นผลของ window.alert (... ) ใน IE:
การคัดลอกรหัสมีดังนี้:
// IE เบราว์เซอร์ - "วัตถุ", "วัตถุ", เบราว์เซอร์อื่น - "ฟังก์ชั่น", "ฟังก์ชั่น"
การแจ้งเตือน (object.prototype.toString.call (window.alert));
การแจ้งเตือน (typeof window.alert); // "วัตถุ"
วิธีการภายใน [[construct]] ถูกเปิดใช้งานโดยใช้ตัวสร้างกับตัวดำเนินการใหม่ดังที่เรากล่าวว่ามีหน้าที่รับผิดชอบในการจัดสรรหน่วยความจำและการสร้างวัตถุ หากไม่มีพารามิเตอร์วงเล็บสำหรับการเรียกตัวสร้างสามารถละเว้นได้:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่น a (x) {// constructor а
this.x = x || 10;
-
// หากคุณไม่ผ่านพารามิเตอร์
var a = ใหม่ a; // หรือใหม่ a ();
การแจ้งเตือน (ขวาน); // 10
// ผ่านพารามิเตอร์ x อย่างชัดเจน x
var b = ใหม่ a (20);
การแจ้งเตือน (bx); // 20
นอกจากนี้เรายังรู้ว่า SHIS ในตัวสร้าง (ขั้นตอนการเริ่มต้น) ถูกตั้งค่าเป็นวัตถุที่สร้างขึ้นใหม่
มาศึกษาอัลกอริทึมสำหรับการสร้างวัตถุ
อัลกอริทึมการสร้างวัตถุ
พฤติกรรมของวิธีการภายใน [[construct]] สามารถอธิบายได้ดังนี้:
การคัดลอกรหัสมีดังนี้:
f. [[construct]] (เริ่มต้นพารามิเตอร์):
o = new NativeObject ();
// คุณสมบัติ [[คลาส]] ถูกตั้งค่าเป็น "วัตถุ"
o. [[คลาส]] = "วัตถุ"
// รับวัตถุ g เมื่ออ้างถึง f.prototype
var __objectPrototype = f.prototype;
// ถ้า __objectPrototype เป็นวัตถุแล้ว:
o. [[ต้นแบบ]] = __ObjectPrototype
// มิฉะนั้น:
o. [[ต้นแบบ]] = Object.prototype;
// ที่นี่ o. [[ต้นแบบ]] เป็นต้นแบบของวัตถุวัตถุ
// f. [[call]] ถูกนำไปใช้เมื่อวัตถุที่สร้างขึ้นใหม่เริ่มต้น
// ตั้งค่านี่เป็นวัตถุที่สร้างขึ้นใหม่ o
// พารามิเตอร์เหมือนกับพารามิเตอร์เริ่มต้นใน F F
r = f. [[call]] (เริ่มต้นพารามิเตอร์); สิ่งนี้ === o;
// นี่คือค่าส่งคืนของ [[call]]
// ใน JS เช่นนี้:
// r = f.apply (o, พารามิเตอร์เริ่มต้น);
// ถ้า r เป็นวัตถุ
กลับ r
// มิฉะนั้น
กลับ o
โปรดทราบคุณสมบัติหลักสองประการ:
1. อันดับแรกต้นแบบของวัตถุที่สร้างขึ้นใหม่นั้นได้มาจากคุณสมบัติต้นแบบของฟังก์ชั่นในขณะนี้ (ซึ่งหมายความว่าคุณสมบัติต้นแบบของวัตถุที่สร้างขึ้นทั้งสองที่สร้างโดยตัวสร้างเดียวกันอาจแตกต่างกันเนื่องจากคุณสมบัติต้นแบบของฟังก์ชันอาจแตกต่างกัน)
2. ประการที่สองตามที่เราได้กล่าวไว้ข้างต้นถ้า [[call]] ส่งคืนวัตถุเมื่อวัตถุเริ่มต้น
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype.x = 10;
var a = new a ();
การแจ้งเตือน (ขวาน); // 10 รับจากต้นแบบ
// ตั้งค่าคุณสมบัติ. prototype เป็นวัตถุใหม่
// ทำไมแอตทริบิวต์ที่ประกาศอย่างชัดเจนจะอธิบายไว้ด้านล่าง
A.prototype = {
ตัวสร้าง: a,
y: 100
-
var b = ใหม่ a ();
// วัตถุ "b" มีคุณสมบัติใหม่
การแจ้งเตือน (bx); // ไม่ได้กำหนด
การแจ้งเตือน (โดย); // 100 รับจากต้นแบบ
// แต่ต้นแบบของวัตถุ A ยังคงได้รับผลลัพธ์ดั้งเดิม
การแจ้งเตือน (ขวาน); // 10 - รับจากต้นแบบ
ฟังก์ชั่น b () {
this.x = 10;
ส่งคืนอาร์เรย์ใหม่ ();
-
// ถ้าตัวสร้าง "B" ไม่ส่งคืน (หรือส่งคืนสิ่งนี้)
// จากนั้นวัตถุนี้สามารถใช้งานได้ แต่สถานการณ์ต่อไปนี้ส่งคืนอาร์เรย์
var b = new b ();
การแจ้งเตือน (bx); // ไม่ได้กำหนด
การแจ้งเตือน (object.prototype.toString.call (b)); // [อาร์เรย์วัตถุ]
มาเรียนรู้เพิ่มเติมเกี่ยวกับต้นแบบ
ต้นแบบ
แต่ละวัตถุมีต้นแบบ (ยกเว้นวัตถุระบบบางชิ้น) การสื่อสารต้นแบบดำเนินการผ่านการเข้าถึงภายในคุณสมบัติต้นแบบ [[ต้นแบบ] ทางอ้อม] ต้นแบบสามารถเป็นวัตถุหรือค่าว่าง
ผู้สร้างทรัพย์สิน
ตัวอย่างข้างต้นมีสองจุดความรู้ที่สำคัญ ครั้งแรกคือเกี่ยวกับคุณสมบัติต้นแบบของคุณสมบัติตัวสร้างของฟังก์ชั่น ในอัลกอริทึมของการสร้างฟังก์ชั่นเรารู้ว่าคุณสมบัติตัวสร้างถูกตั้งค่าเป็นคุณสมบัติต้นแบบของฟังก์ชั่นในระหว่างขั้นตอนการสร้างฟังก์ชั่น ค่าของคุณสมบัติตัวสร้างเป็นการอ้างอิงที่สำคัญสำหรับฟังก์ชั่นเอง:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
var a = new a ();
การแจ้งเตือน (A.Constructor); // function a () {} โดยการมอบหมาย
การแจ้งเตือน (A.Constructor === A); // จริง
โดยปกติในกรณีนี้มีความเข้าใจผิด: มันผิดที่จะสร้างคุณสมบัติเป็นวัตถุที่สร้างขึ้นใหม่เอง แต่อย่างที่เราเห็นคุณสมบัตินี้เป็นของต้นแบบและเข้าถึงวัตถุผ่านการสืบทอด
โดยการสืบทอดอินสแตนซ์ของแอตทริบิวต์คอนสตรัคเตอร์คุณสามารถได้รับการอ้างอิงไปยังวัตถุต้นแบบทางอ้อม:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype.x = หมายเลขใหม่ (10);
var a = new a ();
การแจ้งเตือน (a.constructor.prototype); // [วัตถุวัตถุ]
การแจ้งเตือน (ขวาน); // 10 โดยต้นแบบ
// เอฟเฟกต์เดียวกับ a. [[ต้นแบบ]]. x
การแจ้งเตือน (a.constructor.prototype.x); // 10
การแจ้งเตือน (a.constructor.prototype.x === ax); // จริง
แต่โปรดทราบว่าคุณสมบัติตัวสร้างและต้นแบบของฟังก์ชั่นสามารถนิยามใหม่ได้หลังจากสร้างวัตถุ ในกรณีนี้วัตถุจะสูญเสียกลไกที่กล่าวถึงข้างต้น หากคุณแก้ไขต้นแบบต้นแบบขององค์ประกอบผ่านแอตทริบิวต์ต้นแบบของฟังก์ชั่น (เพิ่มวัตถุใหม่หรือแก้ไขวัตถุที่มีอยู่) คุณจะเห็นแอตทริบิวต์ที่เพิ่มขึ้นใหม่ในอินสแตนซ์
อย่างไรก็ตามหากเราเปลี่ยนคุณสมบัติต้นแบบของฟังก์ชั่นอย่างสมบูรณ์ (โดยการกำหนดวัตถุใหม่) การอ้างอิงไปยังตัวสร้างดั้งเดิมจะหายไปเนื่องจากวัตถุที่เราสร้างไม่รวมถึงคุณสมบัติตัวสร้าง:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype = {
x: 10
-
var a = new a ();
การแจ้งเตือน (ขวาน); // 10
การแจ้งเตือน (A.Constructor === A); // เท็จ!
ดังนั้นการอ้างอิงต้นแบบไปยังฟังก์ชั่นจำเป็นต้องได้รับการฟื้นฟูด้วยตนเอง:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype = {
ตัวสร้าง: a,
x: 10
-
var a = new a ();
การแจ้งเตือน (ขวาน); // 10
การแจ้งเตือน (A.Constructor === A); // จริง
โปรดทราบว่าแม้ว่าแอตทริบิวต์ตัวสร้างจะได้รับการบูรณะด้วยตนเองเมื่อเทียบกับต้นแบบที่หายไปดั้งเดิม แต่คุณลักษณะ {dontenum} หายไปนั่นคือสำหรับ .. ในคำสั่งวนรอบใน a.prototype ไม่ได้รับการสนับสนุนอีกต่อไป
การคัดลอกรหัสมีดังนี้:
var foo = {x: 10};
Object.defineProperty (foo, "y", {
ค่า: 20,
enumerable: false // aka {dontenum} = true
-
console.log (foo.x, foo.y); // 10, 20
สำหรับ (var k ใน foo) {
console.log (k); // เท่านั้น "x"
-
var xdesc = object.getownpropertydescriptor (foo, "x");
var ydesc = object.getownpropertydescriptor (foo, "y");
console.log (
xdesc.enumerable, // true
ydesc.enumerable // false
-
ต้นแบบที่ชัดเจนและโดยปริยาย [[ต้นแบบ]] คุณสมบัติ
โดยทั่วไปมันไม่ถูกต้องที่จะอ้างถึงต้นแบบของวัตถุอย่างชัดเจนผ่านคุณสมบัติต้นแบบของฟังก์ชั่น มันหมายถึงวัตถุเดียวกันคุณสมบัติ [[prototype]] ของวัตถุ:
a. [[ต้นแบบ]] ---> ต้นแบบ <---- a.prototype
นอกจากนี้ค่า [[ต้นแบบ]] ของอินสแตนซ์นั้นได้มาจากคุณสมบัติต้นแบบของตัวสร้าง
อย่างไรก็ตามการส่งแอตทริบิวต์ต้นแบบจะไม่ส่งผลกระทบต่อต้นแบบที่ถูกสร้างขึ้น (จะส่งผลเฉพาะเมื่อแอตทริบิวต์ต้นแบบของการเปลี่ยนแปลงตัวสร้าง) นั่นคือวัตถุที่สร้างขึ้นใหม่มีต้นแบบใหม่และวัตถุที่สร้างขึ้นจะยังคงอ้างถึงต้นแบบเก่าเดิม (ต้นแบบนี้ไม่สามารถแก้ไขได้อีกต่อไป)
การคัดลอกรหัสมีดังนี้:
// สถานการณ์ก่อนแก้ไขต้นแบบ A.Prototype
a. [[ต้นแบบ]] ---> ต้นแบบ <---- a.prototype
// หลังการดัดแปลง
a.prototype ---> ต้นแบบใหม่ // วัตถุใหม่จะมีต้นแบบนี้
a. [[ต้นแบบ]] ---> ต้นแบบ // บนต้นแบบดั้งเดิมของการบูต
ตัวอย่างเช่น:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype.x = 10;
var a = new a ();
การแจ้งเตือน (ขวาน); // 10
A.prototype = {
ตัวสร้าง: a,
x: 20
y: 30
-
// Object A เป็นค่าที่ได้จากต้นแบบของน้ำมันดิบผ่านการอ้างอิง [[ต้นแบบ] โดยนัย]
การแจ้งเตือน (ขวาน); // 10
แจ้งเตือน (ay) // undefined
var b = ใหม่ a ();
// แต่วัตถุใหม่คือค่าที่ได้จากต้นแบบใหม่
การแจ้งเตือน (bx); // 20
การแจ้งเตือน (โดย) // 30
ดังนั้นจึงผิดสำหรับบางบทความที่จะบอกว่า "การปรับเปลี่ยนแบบไดนามิกของต้นแบบจะส่งผลกระทบต่อวัตถุทั้งหมดที่มีต้นแบบใหม่" และต้นแบบใหม่จะมีผลเฉพาะกับวัตถุที่สร้างขึ้นใหม่หลังจากแก้ไขต้นแบบ
กฎหลักที่นี่คือ: ต้นแบบของวัตถุถูกสร้างขึ้นเมื่อวัตถุถูกสร้างขึ้นและไม่สามารถแก้ไขเป็นวัตถุใหม่ได้หลังจากนั้น หากวัตถุเดียวกันยังคงอ้างอิงสามารถอ้างอิงได้ผ่านต้นแบบที่ชัดเจนของตัวสร้าง หลังจากสร้างวัตถุคุณสมบัติของต้นแบบสามารถเพิ่มหรือแก้ไขได้เท่านั้น
คุณลักษณะ __proto__ ที่ไม่ได้มาตรฐาน
อย่างไรก็ตามการใช้งานบางอย่าง (เช่น SpiderMonkey) ให้คุณสมบัติที่ชัดเจน __proto__ ที่ไม่ได้มาตรฐานเพื่ออ้างอิงต้นแบบของวัตถุ:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชัน A () {}
A.prototype.x = 10;
var a = new a ();
การแจ้งเตือน (ขวาน); // 10
var __NewPrototype = {
ตัวสร้าง: a,
x: 20
y: 30
-
// อ้างอิงถึงวัตถุใหม่
A.prototype = __NewPrototype;
var b = ใหม่ a ();
alert(bx); // 20
alert(by); // 30
// "a"对象使用的依然是旧的原型
alert(ax); // 10
alert(ay); // ไม่ได้กำหนด
// 显式修改原型
a.__proto__ = __newPrototype;
// 现在"а"对象引用的是新对象
alert(ax); // 20
alert(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'](); // ตกลง
原型链
让我们展示如何为用户定义对象创建原型链,非常简单:
การคัดลอกรหัสมีดังนี้:
function 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 = new 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 = new A(); // ข้อผิดพลาด
此外,在父类的构造函数有太多代码的话也是一种缺点。
解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。
การคัดลอกรหัสมีดังนี้:
function A() {
alert('A.[[Call]] activated');
this.x = 10;
-
A.prototype.y = 20;
var a = new A();
alert([ax, ay]); // 10 (自身), 20 (集成)
function 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的疑问,如果你有任何问题,请留言,我们一起讨论。