มีสองรุ่นสำหรับการนำคลาสและอินสแตนซ์มาใช้ใน JavaScript: วิธีการสร้างต้นแบบและวิธีการปิด ทั้งสองมีข้อดีและข้อเสียและมีรูปแบบที่หลากหลายมากมาย โปรแกรมเมอร์และห้องสมุดหลายคนมีวิธีการที่แตกต่างกันและฟังก์ชั่นยูทิลิตี้การจัดการชั้นเรียนเพื่อกระดาษมากกว่าบางส่วนของภาษาที่น่าเกลียด
ผลที่ได้คือใน บริษัท ที่ผสมคุณจะมี mashmash ของ metaclasses ทั้งหมดทำงานแตกต่างกันเล็กน้อย มีอะไรที่แย่กว่านั้นคือเนื้อหาการสอน JavaScript ส่วนใหญ่นั้นแย่มากและมีการประนีประนอมระหว่างการปกปิดฐานทั้งหมดทำให้คุณสับสนมาก (อาจเป็นไปได้ว่าผู้เขียนสับสนเช่นกันโมเดลวัตถุของ JavaScript นั้นแตกต่างอย่างมากกับภาษาการเขียนโปรแกรมส่วนใหญ่และในหลาย ๆ สถานที่ได้รับการออกแบบอย่างไม่ดีนัก)
มาเริ่มด้วยวิธีต้นแบบกันก่อน นี่เป็น native-JavaScript ที่คุณจะได้รับ: มีรหัสค่าใช้จ่ายขั้นต่ำและ instanceof จะทำงานกับอินสแตนซ์ของวัตถุประเภทนี้
function Shape(x, y) {
this.x= x;
this.y= y;
}
เราสามารถเพิ่มวิธีการไปยังอินสแตนซ์ที่สร้างขึ้นnew Shape
โดยการเขียนพวกเขาไปยังการprototype
ค้นหาของฟังก์ชั่นคอนสตรัคนี้:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
ทีนี้ก็ทำการ subclass มันเท่าที่คุณสามารถเรียกสิ่งที่ JavaScript ทำ subclassing เราทำเช่นนั้นโดยแทนที่prototype
คุณสมบัติมหัศจรรย์แปลก ๆ ทั้งหมด:
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
ก่อนที่จะเพิ่มวิธีการ:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
ตัวอย่างนี้จะใช้งานได้และคุณจะเห็นรหัสเหมือนในบทช่วยสอนจำนวนมาก แต่มนุษย์นั่นnew Shape()
น่าเกลียด: เราสร้างคลาสฐานขึ้นทันทีแม้ว่าจะไม่มีการสร้างรูปร่างจริง มันเกิดขึ้นกับการทำงานในกรณีง่าย ๆ นี้เพราะจาวาสคริปต์นั้นเลอะเทอะมาก: มันยอมให้มีการส่งผ่านค่าศูนย์ไม่ได้, ในกรณีนี้x
และy
กลายเป็นundefined
และถูกกำหนดให้กับต้นแบบthis.x
และthis.y
และถ้าฟังก์ชั่นคอนสตรัคเตอร์ทำอะไรที่ซับซ้อนกว่านี้มันจะตกลงบนใบหน้า
ดังนั้นสิ่งที่เราต้องทำคือหาวิธีในการสร้างวัตถุต้นแบบที่มีวิธีการและสมาชิกคนอื่น ๆ ที่เราต้องการในระดับชั้นเรียนโดยไม่ต้องเรียกฟังก์ชั่นคอนสตรัคเตอร์ระดับฐานของ ในการทำเช่นนี้เราจะต้องเริ่มเขียนโค้ดตัวช่วย นี่เป็นวิธีที่ง่ายที่สุดที่ฉันรู้:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
สิ่งนี้จะถ่ายโอนสมาชิกของคลาสฐานในแบบตัวอย่างไปยังฟังก์ชันตัวสร้างใหม่ซึ่งไม่ทำอะไรเลยจากนั้นใช้ตัวสร้างนั้น ตอนนี้เราสามารถเขียนได้ง่ายๆ:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
แทนการ new Shape()
ผิด ตอนนี้เรามีชุดพื้นฐานที่ยอมรับได้สำหรับคลาสที่สร้างขึ้น
มีการปรับแต่งและส่วนขยายเล็กน้อยที่เราสามารถพิจารณาได้ภายใต้โมเดลนี้ ตัวอย่างเช่นที่นี่เป็นเวอร์ชันเกี่ยวกับการสร้างประโยค:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
ทั้งสองเวอร์ชันมีข้อเสียเปรียบที่ไม่สามารถสืบทอดฟังก์ชันคอนสตรัคได้เนื่องจากมีหลายภาษา ดังนั้นแม้ว่าคลาสย่อยของคุณจะไม่เพิ่มอะไรเลยในกระบวนการก่อสร้างคุณต้องจำไว้ว่าให้เรียกตัวสร้างฐานพร้อมอาร์กิวเมนต์ใด ๆ ก็ตามที่ฐานต้องการ สิ่งนี้สามารถทำงานอัตโนมัติได้เล็กน้อยapply
แต่คุณยังคงต้องเขียนออกมา:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
ดังนั้นส่วนขยายทั่วไปคือการแบ่งเนื้อหาการเริ่มต้นเป็นฟังก์ชันของตัวเองมากกว่าตัวสร้างเอง ฟังก์ชั่นนี้สามารถสืบทอดมาจากฐานได้ดี:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
ตอนนี้เราเพิ่งได้ตัวสร้างฟังก์ชั่นสำเร็จรูปเดียวกันสำหรับแต่ละคลาส บางทีเราสามารถย้ายสิ่งนั้นออกไปเป็นฟังก์ชั่นตัวช่วยของตัวเองได้ดังนั้นเราไม่จำเป็นต้องพิมพ์มันต่อไปเช่นแทนที่จะFunction.prototype.subclass
หมุนมันและปล่อยให้คลาสพื้นฐานของ Function คายคลาสย่อย:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... ซึ่งเริ่มจะดูเหมือนภาษาอื่น ๆ อีกเล็กน้อยถึงแม้ว่าจะมีไวยากรณ์ที่อุ้ยอ้ายเล็กน้อย คุณสามารถโรยด้วยคุณสมบัติพิเศษบางอย่างได้ถ้าต้องการ บางทีคุณอาจต้องการmakeSubclass
จดชื่อชั้นเรียนและตั้งค่าเริ่มต้นให้toString
ใช้ บางทีคุณอาจต้องการให้ Constructor ตรวจพบเมื่อมีการเรียกใช้โดยไม่ตั้งใจโดยไม่ต้องnew
ดำเนินการ (ซึ่งมักจะทำให้การดีบักน่ารำคาญมาก):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
บางทีคุณอาจต้องการที่จะส่งสมาชิกใหม่ทั้งหมดและmakeSubclass
เพิ่มพวกเขาไปยังต้นแบบเพื่อช่วยให้คุณไม่ต้องเขียนClass.prototype...
มากนัก ระบบคลาสจำนวนมากทำเช่นนั้น:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
มีคุณสมบัติที่เป็นไปได้มากมายที่คุณอาจพิจารณาว่าเป็นที่ต้องการในระบบวัตถุและไม่มีใครเห็นด้วยกับสูตรหนึ่ง
ทางปิดนั้น นี่เป็นการหลีกเลี่ยงปัญหาของการสืบทอดตามต้นแบบของ JavaScript โดยไม่ใช้การสืบทอดเลย แทน:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
ตอนนี้ทุกอินสแตนซ์เดียวShape
จะมีสำเนาของตัวเองtoString
วิธีการ (และวิธีการอื่น ๆ หรือสมาชิกชั้นเรียนอื่น ๆ ที่เราเพิ่ม)
สิ่งที่ไม่ดีเกี่ยวกับทุกครั้งที่มีสำเนาของสมาชิกแต่ละคนในชั้นเรียนนั้นมีประสิทธิภาพน้อยกว่า หากคุณกำลังจัดการกับอินสแตนซ์ของคลาสย่อยจำนวนมากการสืบทอดแบบตัวอย่างอาจให้บริการคุณได้ดีขึ้น การเรียกใช้เมธอดของคลาสฐานนั้นน่ารำคาญเล็กน้อยอย่างที่คุณเห็น: เราต้องจำไว้ว่าเมธอดนั้นคืออะไรก่อนที่ตัวสร้างคลาสย่อยจะเขียนทับมันหรือมันจะหายไป
[เนื่องจากไม่มีมรดกที่นี่instanceof
ผู้ประกอบการจะไม่ทำงาน คุณจะต้องจัดเตรียมกลไกของคุณเองสำหรับการดมกลิ่นในชั้นเรียนหากคุณต้องการ ในขณะที่คุณสามารถคลุกคลีวัตถุต้นแบบในลักษณะเดียวกันกับการสืบทอดต้นแบบมันค่อนข้างยุ่งยากและไม่คุ้มค่ากับinstanceof
การทำงาน]
สิ่งที่ดีเกี่ยวกับทุกอินสแตนซ์ที่มีวิธีการของตัวเองคือวิธีนั้นอาจถูกผูกไว้กับอินสแตนซ์เฉพาะที่เป็นเจ้าของ สิ่งนี้มีประโยชน์เนื่องจากวิธีการผูกมัดแบบแปลก ๆ ของ JavaScript this
ในการเรียกใช้เมธอดซึ่งมีผลที่สุดหากคุณแยกเมธอดออกจากเจ้าของ:
var ts= mycircle.toString;
alert(ts());
จากนั้นthis
ภายในเมธอดจะไม่ใช่อินสแตนซ์ของ Circle ตามที่คาดไว้ (จริง ๆ แล้วมันจะเป็นwindow
ออบเจ็กต์ระดับโลก ในความเป็นจริงสิ่งนี้มักเกิดขึ้นเมื่อมีการใช้วิธีการและกำหนดให้กับsetTimeout
, onclick
หรือEventListener
ในทั่วไป
ด้วยวิธีต้นแบบคุณต้องรวมการปิดสำหรับการมอบหมายดังกล่าวทุกครั้ง:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
หรือในอนาคต (หรือตอนนี้ถ้าคุณแฮ็ค Function.prototype) คุณสามารถทำได้ด้วยfunction.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
หากอินสแตนซ์ของคุณเสร็จสิ้นการปิดการผูกจะทำฟรีโดยการปิดตัวแปรอินสแตนซ์ (โดยปกติแล้วจะเรียกว่าthat
หรือself
แม้ว่าโดยส่วนตัวแล้วฉันจะให้คำแนะนำกับหลังอย่างที่self
มีความหมายอื่นที่แตกต่างกันใน JavaScript) คุณจะไม่ได้รับข้อโต้แย้ง1, 1
ในตัวอย่างข้างต้นโดยไม่เสียค่าใช้จ่ายดังนั้นคุณจะยังคงต้องมีการปิดอีกครั้งหรือbind()
ถ้าคุณต้องการทำเช่นนั้น
มีวิธีการปิดแบบต่างๆมากมายเช่นกัน คุณอาจต้องการละเว้นthis
อย่างสมบูรณ์โดยสร้างใหม่that
และส่งคืนแทนที่จะใช้new
โอเปอเรเตอร์:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
วิธีไหน“ เหมาะสม”? ทั้งสอง ข้อไหนดีที่สุด? ขึ้นอยู่กับสถานการณ์ของคุณ FWIW ฉันมีแนวโน้มที่จะสร้างต้นแบบสำหรับการสืบทอด JavaScript จริงเมื่อฉันทำสิ่งที่ OO อย่างยิ่งและปิดสำหรับผลหน้าทิ้งง่าย
แต่ทั้งสองวิธีนั้นค่อนข้างตอบโต้กับโปรแกรมเมอร์ส่วนใหญ่ ทั้งคู่มีรูปแบบที่ยุ่งเหยิงมากมาย คุณจะได้พบกับทั้งสอง (รวมทั้งแผนการที่ขาดระหว่างและโดยทั่วไป) หากคุณใช้รหัส / ไลบรารีของผู้อื่น ไม่มีคำตอบที่ยอมรับกันโดยทั่วไป ยินดีต้อนรับสู่โลกมหัศจรรย์ของวัตถุ JavaScript
[สิ่งนี้เป็นส่วนที่ 94 ของเพราะเหตุใด JavaScript จึงไม่ใช่ภาษาโปรแกรมโปรดของฉัน]