javascript - ทำไมการขยายอ็อบเจ็กต์เนทีฟจึงเป็นการปฏิบัติที่ไม่ดี?
ผู้นำความคิดเห็นเกี่ยวกับ JS ทุกคนพูดว่าการขยายวัตถุในตัวเป็นแนวปฏิบัติที่ไม่ดี แต่ทำไมล่ะ? เราจะได้รับผลกระทบต่อประสิทธิภาพหรือไม่? พวกเขากลัวว่ามีคนทำมัน "ผิดวิธี" และเพิ่มประเภทที่สามารถวนรอบได้ให้กับ Object จนทำลายลูปทั้งหมดบนวัตถุใด ๆ หรือไม่?
ยกตัวอย่าง should.js ของ TJ Holowaychuk เขาเพิ่ม getter ง่าย ๆ ให้กับ Object และทุกอย่างทำงานได้ดี (แหล่งที่มา)
Object.defineProperty(Object.prototype, 'should', {
set: function(){},
get: function(){
return new Assertion(Object(this).valueOf());
},
configurable: true
});
นี่สมเหตุสมผลจริงๆ ยกตัวอย่างเช่น ผู้หนึ่งอาจขยาย Array ได้
Array.defineProperty(Array.prototype, "remove", {
set: function(){},
get: function(){
return removeArrayElement.bind(this);
}
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);
มีข้อโต้แย้งใด ๆ ต่อต้านการขยายประเภทเนทีฟหรือไม่?
คำตอบ #1
เมื่อคุณขยายวัตถุ คุณจะเปลี่ยนพฤติกรรมของมัน
การเปลี่ยนพฤติกรรมของวัตถุที่ใช้เฉพาะในโค้ดของคุณเองนั้นเป็นเรื่องปกติ แต่เมื่อคุณเปลี่ยนพฤติกรรมของสิ่งที่ถูกใช้โดยโค้ดอื่นๆ ด้วย จะมีความเสี่ยงที่คุณจะทำให้โค้ดอื่นๆ นั้นพังได้
เมื่อมีการเพิ่มเมธอดไปยังคลาสออบเจ็กต์และอาร์เรย์ในจาวาสคริปต์ ความเสี่ยงในการทำให้บางสิ่งเสียหายมีสูงมาก เนื่องจากวิธีการทำงานของจาวาสคริปต์ ปีแห่งประสบการณ์ได้สอนฉันว่าของแบบนี้ก่อให้เกิดบั๊กที่น่าสยดสยองทุกประเภทในจาวาสคริปต์
ถ้าคุณต้องการพฤติกรรมที่กำหนดเอง จะดีกว่ามากถ้ากำหนดคลาสของตัวเอง (อาจเป็นซับคลาส) แทนที่จะเปลี่ยนคลาสที่มีอยู่ วิธีนี้คุณจะไม่ทำให้สิ่งใดเสียหายเลย
ความสามารถในการเปลี่ยนวิธีที่คลาสทำงานโดยไม่ต้องสร้างคลาสย่อยเป็นคุณสมบัติที่สำคัญของภาษาการเขียนโปรแกรมที่ดีใด ๆ แต่เป็นคุณสมบัติที่ต้องใช้ไม่บ่อยและต้องใช้ด้วยความระมัดระวัง
คำตอบ #2
ไม่มีข้อเสียที่วัดได้ เช่น การลดประสิทธิภาพ อย่างน้อยก็ไม่มีใครบอกถึงเรื่องนี้ ดังนั้นนี่จึงเป็นเรื่องของความชอบและประสบการณ์ส่วนตัว
ข้อโต้แย้งหลักของผู้สนับสนุน: มันดูดีกว่าและใช้งานง่ายขึ้น: เป็น syntax sugar มันเป็นฟังก์ชันที่เฉพาะเจาะจงกับประเภท/อินสแตนซ์ ดังนั้นจึงควรถูกผูกไว้โดยเฉพาะกับประเภท/อินสแตนซ์นั้น
ข้อโต้แย้งหลัก: โค้ดสามารถรบกวนได้ หากไลบรารี A เพิ่มฟังก์ชัน มันอาจเขียนทับฟังก์ชันของไลบรารี B ซึ่งอาจทำให้โค้ดเสียหายได้อย่างง่ายดาย
ทั้งสองฝ่ายมีเหตุผล เมื่อคุณพึ่งพาสองไลบรารีที่เปลี่ยนประเภทของคุณโดยตรง คุณมีแนวโน้มที่จะจบลงด้วยโค้ดที่เสียหายเนื่องจากฟังก์ชันที่คาดหวังอาจไม่เหมือนกัน ฉันเห็นด้วยอย่างยิ่งในประเด็นนั้น ไลบรารีมาโครไม่ควรจัดการประเภทพื้นฐาน มิฉะนั้นคุณในฐานะนักพัฒนาจะไม่สามารถรู้ได้เลยว่าเกิดอะไรขึ้นเบื้องหลัง
และนั่นคือเหตุผลที่ฉันไม่ชอบไลบรารีอย่าง jQuery, underscore เป็นต้น อย่าเข้าใจผิด; พวกมันถูกเขียนโปรแกรมได้ดีอย่างยิ่งและทำงานได้อย่างยอดเยี่ยม แต่พวกมัน ใหญ่ คุณใช้แค่ 10% ของพวกมัน และเข้าใจเพียงประมาณ 1%
นั่นเป็นเหตุผลที่ฉันชอบวิธีการแบบอะตอมมิก ซึ่งคุณต้องการเฉพาะสิ่งที่คุณต้องการจริงๆ ด้วยวิธีนี้ คุณจะรู้เสมอว่าเกิดอะไรขึ้น ห้องสมุดขนาดเล็กเหล่านี้จะทำเฉพาะสิ่งที่คุณต้องการให้ทำ ดังนั้นมันจะไม่รบกวน ในบริบทของการที่ผู้ใช้ปลายทางรู้ว่าเพิ่มคุณสมบัติอะไร การขยายประเภทพื้นฐานสามารถพิจารณาว่าปลอดภัยได้
สรุปสั้นๆ เมื่อสงสัย อย่าไปขยายชนิดข้อมูลพื้นฐาน (native types) ขยายชนิดข้อมูลพื้นฐานเฉพาะเมื่อคุณมั่นใจ 100% ว่าผู้ใช้งานสามารถเข้าใจและต้องการพฤติกรรมนั้น ใน ทุกกรณี อย่าจัดการฟังก์ชันที่มีอยู่ของชนิดข้อมูลพื้นฐาน เพราะจะทำให้ส่วนติดต่อที่มีอยู่เกิดข้อผิดพลาด
ถ้าคุณตัดสินใจที่จะขยายประเภท ให้ใช้ Object.defineProperty(obj, prop, desc); ถ้าคุณไม่สามารถทำได้ ให้ใช้ prototype ของประเภทนั้น
ผมเดิมทีคิดคำถามนี้เพราะผมต้องการให้ Error สามารถส่งผ่าน JSON ได้ ดังนั้นผมจึงต้องการวิธีในการเปลี่ยนเป็นสตริง error.stringify() รู้สึกดีกว่า errorlib.stringify(error); ตามที่โครงสร้างที่สองแสดง ผมกำลังดำเนินการบน errorlib ไม่ใช่บน error ตัวมันเอง
คำตอบ #3
ในความเห็นของฉัน มันเป็นการปฏิบัติที่ไม่ดี เหตุผลหลักคือการรวมระบบ อ้างถึงเอกสารของ should.js:
โอ้พระเจ้า มันขยายวัตถุได้เหรอ???!?!@ ใช่ ใช่ มันทำได้ ด้วย getter ตัวเดียว should และไม่ มันจะไม่ทำให้โค้ดของคุณพัง
ก็แล้ว ผู้เขียนจะรู้ได้อย่างไร? ถ้าเฟรมเวิร์กล้อเลียนของฉันทำเหมือนกันล่ะ? ถ้าห้องสมุดสัญญาของฉันทำเหมือนกันล่ะ?
ถ้าคุณกำลังทำมันในโปรเจกต์ของตัวเองก็ไม่มีปัญหา แต่สำหรับไลบรารี มันถือว่าเป็นการออกแบบที่ไม่ดี Underscore.js เป็นตัวอย่างของสิ่งที่ทำได้ถูกต้อง:
var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
คำตอบ #4
ถ้าคุณพิจารณาเป็นรายกรณี บางทีการใช้งานบางอย่างอาจเป็นที่ยอมรับได้
String.prototype.slice = function slice( me ){
return me;
}; // Definite risk.
การเขียนทับเมธอดที่สร้างขึ้นแล้วสร้างปัญหามากกว่าที่จะแก้ไข ซึ่งเป็นเหตุผลว่าทำไมจึงมีการกล่าวโดยทั่วไปในหลายภาษาโปรแกรมให้หลีกเลี่ยงการปฏิบัตินี้ นักพัฒนาจะรู้ได้อย่างไรว่าฟังก์ชันถูกเปลี่ยนแปลงแล้ว?
String.prototype.capitalize = function capitalize(){
return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.
ในกรณีนี้ เราไม่ได้เขียนทับเมธอดหลักของ JS ที่ทราบอยู่แล้ว แต่เรากำลังขยาย String หนึ่งข้อโต้แย้งในโพสต์นี้กล่าวถึงว่าผู้พัฒนารายใหม่จะรู้ได้อย่างไรว่าวิธีนี้เป็นส่วนหนึ่งของ core JS หรือจะหาคู่มือเอกสารได้จากที่ไหน? แล้วจะเกิดอะไรขึ้นหากออบเจ็กต์ String ของ core JS มีเมธอดชื่อ capitalize?
ถ้าแทนที่จะเพิ่มชื่อที่อาจชนกับไลบรารีอื่น คุณใช้ตัวแก้ไขเฉพาะบริษัท/แอปที่นักพัฒนาทุกคนเข้าใจล่ะ?
String.prototype.weCapitalize = function weCapitalize(){
return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.
var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.
หากคุณยังคงขยายวัตถุอื่น ๆ ต่อไป นักพัฒนาทุกคนจะพบพวกมันในสภาพแวดล้อมจริงกับ (ในกรณีนี้) เรา ซึ่งจะแจ้งให้พวกเขาทราบว่านี่คือการขยายเฉพาะบริษัท/แอป
นี่ไม่ได้กำจัดการชนกันของชื่อออกไป แต่ช่วยลดความเป็นไปได้นั้นได้ หากคุณตัดสินใจว่าการขยายวัตถุ JS หลักเหมาะกับคุณและ/หรือทีมของคุณ บางทีนี่อาจเหมาะกับคุณ
คำตอบ #5
การขยายโปรโตไทป์ของวัตถุที่มาพร้อมกับภาษาเป็นไอเดียที่ไม่ดีจริงๆ อย่างไรก็ตาม ES2015 ได้นำเสนอเทคนิคใหม่ที่สามารถใช้เพื่อให้ได้พฤติกรรมที่ต้องการ:
การใช้ WeakMap เพื่อเชื่อมโยงประเภทกับโปรโตไทป์ที่มีอยู่ในตัว
การใช้งานต่อไปนี้ขยายโพรโทไทป์ Number และ Array โดยไม่แตะต้องพวกมันเลย:
// new types
const AddMonoid = {
empty: () => 0,
concat: (x, y) => x + y,
};
const ArrayMonoid = {
empty: () => [],
concat: (acc, x) => acc.concat(x),
};
const ArrayFold = {
reduce: xs => xs.reduce(
type(xs[0]).monoid.concat,
type(xs[0]).monoid.empty()
)};
// the WeakMap that associates types to prototpyes
types = new WeakMap();
types.set(Number.prototype, {
monoid: AddMonoid
});
types.set(Array.prototype, {
monoid: ArrayMonoid,
fold: ArrayFold
});
// auxiliary helpers to apply functions of the extended prototypes
const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);
// mock data
xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];
// and run
console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);ดังที่คุณเห็น แม้แต่ต้นแบบขั้นพื้นฐานก็สามารถขยายได้โดยเทคนิคนี้ มันใช้โครงสร้างแผนที่และ Object identity เพื่อเชื่อมโยงประเภทกับต้นแบบในตัว
ตัวอย่างของฉันทำให้สามารถใช้ฟังก์ชัน reduce ซึ่งคาดหวังเพียง Array เป็นอาร์กิวเมนต์เดียวได้ เพราะมันสามารถดึงข้อมูลเกี่ยวกับวิธีสร้างตัวสะสมว่างและวิธีรวมองค์ประกอบกับตัวสะสมนี้จากองค์ประกอบของ Array เอง
โปรดทราบว่าฉันสามารถใช้ประเภท Map ปกติได้ เนื่องจากการอ้างอิงแบบอ่อนโยนไม่มีความหมายเมื่อมันเพียงแค่แทนโปรโตไทป์ที่มีอยู่ในตัว ซึ่งไม่เคยถูกเก็บขยะ อย่างไรก็ตาม WeakMap ไม่สามารถวนซ้ำได้และไม่สามารถตรวจสอบได้เว้นแต่คุณจะมีคีย์ที่ถูกต้อง นี่เป็นคุณสมบัติที่ต้องการ เพราะฉันต้องการหลีกเลี่ยงการสะท้อนประเภทในทุกรูปแบบ
