ค้นหาเว็บไซต์

typescript - ทำไมถึงไม่สามารถเรียกใช้เมธอดได้ถ้าคลาสสืบทอดจาก Object โดยตรง?


เมื่อฉันมีคลาสใน Typescript ที่สืบทอดมาจาก Object อย่างชัดเจน แล้วพยายามเรียกใช้งานเมธอดของออบเจ็กต์จากคลาสนั้นก็ล้มเหลว:

class Example extends Object {
    constructor() {
        super();
    }
    public getHello() : string {
        return "Hello";
    }
}

let greeter = new Example();
alert(greeter.getHello());

ข้อผิดพลาด: greeter.getHello ไม่ใช่ฟังก์ชัน ทำไมถึงเป็นเช่นนั้น? ถ้าฉันลบ extends และการเรียก super() ออก มันก็กลับใช้งานได้ทันที

ปัญหาของฉันก็คือโค้ดถูกสร้างขึ้นโดยอัตโนมัติจากเวอร์ชัน JSweet ที่ปรับแต่งไว้ และเราทำการทรานสไพล์เพียงบางส่วนของฐานโค้ด คลาสในลำดับชั้นของคลาสที่ไม่ควรถูกทรานสไพล์จะถูกแมปไปที่ Object เพราะ extends ไม่สามารถถูกลบได้ง่ายโดยไม่เปลี่ยนแปลง JSweet อย่างมาก

คำตอบ #1

ฉันคิดว่าคุณอาจโต้แย้งได้ว่านี่เป็นบั๊กใน TypeScript อาจจะไม่ใช่บั๊กที่มีความสำคัญสูงนัก แต่ก็... :-)

ก็เพราะว่า Object มองข้าม this ที่มันถูกเรียกด้วย และจะคืนค่าอ็อบเจ็กต์ใหม่ที่ว่าง วิธีที่ TypeScript แปลงโค้ดนั้น (เมื่อกำหนดเป้าหมายเป็นสภาพแวดล้อม ES5) มันจะลงเอยด้วยการเรียก Object แบบนี้ใน Example:

function Example() {
    return Object.call(this) || this;
}

ผลลัพธ์ของ Object.call(this) คือวัตถุใหม่ ราวกับว่าคุณทำ {}

ดังนั้นทางแก้คือ...อย่าทำแบบนั้น :-)

ปัญหาของฉันคือโค้ดถูกสร้างอัตโนมัติจากเวอร์ชัน JSweet ที่ปรับแต่งเอง และเราทำการคอมไพล์เพียงบางส่วนของฐานโค้ดเท่านั้น คลาสในลำดับชั้นของคลาสที่ไม่ควรถูกคอมไพล์จะถูกแมปไปยัง Object อย่างง่ายดาย เพราะการลบ extends ออกอย่างง่ายโดยไม่เปลี่ยนแปลง JSweet อย่างมากนั้นทำไม่ได้

โอ๊ย ถ้าคุณสามารถตั้งเป้าไปที่ ES2015+ ปัญหานี้ก็จะหายไป เพราะนี่เกี่ยวข้องกับเวอร์ชันที่ TypeScript สร้างสำหรับ ES5 และเวอร์ชันก่อนหน้านั้นเท่านั้น ถ้าคุณทำไม่ได้ ผมคิดว่าคุณอาจต้องการรายงานปัญหาในรายการปัญหาของ TypeScript และอาจทำการ pull request พร้อมกับการแก้ไข (ผมเคยลองค้นหาการรายงานที่มีอยู่แล้วแต่ก็ไม่พบ) สถานการณ์นี้แตกต่างเล็กน้อยจากการขยาย built-ins อื่นๆ (Error, Array) เพราะมีวิธีแก้ที่ง่ายมาก: แค่ละเว้นเงื่อนไข extends เท่านั้น

หรือ ตามที่คุณกล่าวถึงในความคิดเห็น คุณอาจมีคลาสจำลองที่ไม่ทำอะไรเลย เช่น:

class FauxObject { }

แล้วจึงใช้สิ่งนั้นแทน Object (class Example extends FauxObject)

เพื่อความละเอียดในการอธิบาย เวอร์ชันที่คอมไพล์เต็มคือแบบนี้ การเรียกไปยัง Object ถูกทำเครื่องหมายด้วย ******:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Example = /** @class */ (function (_super) {
    __extends(Example, _super);
    function Example() {
        return _super.call(this) || this;             // ******
    }
    Example.prototype.getHello = function () {
        return "Hello";
    };
    return Example;
}(Object));
var greeter = new Example();
alert(greeter.getHello());

_super คือ Object ในกรณีของคุณ

คำตอบ #2

หลังจากที่ส่งรายงานข้อผิดพลาดไปแล้ว ฉันได้รับคำแนะนำไปยังรายการคำถามที่พบบ่อยที่กล่าวถึงการขยายฟังก์ชัน built-in ของ TypeScript:

ใน ES2015 ตัวสร้างที่คืนค่าเป็นวัตถุจะซ่อนการแทนที่ค่าของ this สำหรับผู้เรียกใด ๆ ของ super(...) จำเป็นสำหรับโค้ดตัวสร้างที่สร้างขึ้นเพื่อจับค่าส่งกลับที่เป็นไปได้ใด ๆ ของ super(...) และแทนที่ด้วย this

ผลลัพธ์คือ การสร้าง subclass ของ Error, Array และอื่น ๆ อาจไม่ทำงานตามที่คาดไว้ เนื่องจากฟังก์ชัน constructor สำหรับ Error, Array และอื่น ๆ ใช้ new.target ของ ECMAScript 6 เพื่อปรับ chain ของ prototype; อย่างไรก็ตาม ไม่มีวิธีที่จะรับรองค่า new.target เมื่อเรียกใช้ constructor ใน ECMAScript 5 คอมไพเลอร์รุ่นล่างอื่น ๆ โดยทั่วไปก็มีข้อจำกัดเดียวกันโดยค่าเริ่มต้น

คำอธิบายคล้ายกับคำตอบของ T.J. Crowder รวมถึงพวกเขาแนะนำวิธีแก้ปัญหาเพิ่มเติมอีกวิธี:

ตามคำแนะนำ คุณสามารถปรับโปรโตไทป์ด้วยตนเองทันทีหลังจากการเรียก super(...) ใดๆ

ซึ่งในกรณีของฉันจะเป็นการเพิ่ม Object.setPrototypeOf(this, Example.prototype); ลงในตัวสร้าง (หรือชักชวน JSweet ให้ทำเช่นนั้น)

แต่:

น่าเสียดาย ที่วิธีแก้ปัญหาเหล่านี้จะใช้ไม่ได้กับ Internet Explorer 10 และรุ่นก่อนหน้า คุณสามารถคัดลอกเมธอดจากโปรโตไทป์ไปยังอินสแตนซ์เองได้ (เช่น FooError.prototype ไปยัง this) แต่โซ่โปรโตไทป์เองไม่สามารถแก้ไขได้

แก้ไข: สุดท้ายฉันใช้วิธีแก้ปัญหานี้เพราะฉันพบว่าง่ายต่อการนำไปใช้ในโครงการ JSweet ของเรา

แก้ไข 2: วิธีแก้ปัญหานี้มีข้อเสียอื่น ๆ ดังนั้นเราจึงไปข้างหน้าและใช้คลาสตัวอย่างตามที่อธิบายสั้น ๆ ในคำตอบที่ได้รับการยอมรับ

บทความที่เกี่ยวข้อง