ปัญหาการกำหนดขอบเขต TypeScript เมื่อถูกเรียกในการเรียกกลับ jquery


107

ฉันไม่แน่ใจถึงแนวทางที่ดีที่สุดในการจัดการขอบเขตของ "this" ใน TypeScript

นี่คือตัวอย่างของรูปแบบทั่วไปในโค้ดที่ฉันกำลังแปลงเป็น TypeScript:

class DemonstrateScopingProblems {
    private status = "blah";
    public run() {
        alert(this.status);
    }
}

var thisTest = new DemonstrateScopingProblems();
// works as expected, displays "blah":
thisTest.run(); 
// doesn't work; this is scoped to be the document so this.status is undefined:
$(document).ready(thisTest.run); 

ตอนนี้ฉันสามารถเปลี่ยนการโทรเป็น ...

$(document).ready(thisTest.run.bind(thisTest));

... ซึ่งได้ผล แต่มันค่อนข้างน่ากลัว หมายความว่าโค้ดทั้งหมดสามารถคอมไพล์และทำงานได้ดีในบางสถานการณ์ แต่ถ้าเราลืมผูกขอบเขตมันจะพัง

ฉันต้องการวิธีที่จะทำภายในชั้นเรียนเพื่อที่เมื่อใช้ชั้นเรียนเราไม่จำเป็นต้องกังวลว่า "สิ่งนี้" จะถูกกำหนดขอบเขตไว้อย่างไร

ข้อเสนอแนะใด ๆ ?

อัปเดต

อีกวิธีหนึ่งที่ใช้ได้ผลคือการใช้ลูกศรอ้วน:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}

เป็นแนวทางที่ถูกต้องหรือไม่?


2
สิ่งนี้จะเป็นประโยชน์: youtube.com/watch?v=tvocUcbCupA
basarat

หมายเหตุ: Ryan คัดลอกคำตอบของเขาที่จะtypescript วิกิพีเดีย
Franklin Yu

ดูที่นี่สำหรับการแก้ปัญหา typescript 2+
Deilan

คำตอบ:


166

คุณมีทางเลือกสองสามทางที่นี่โดยแต่ละรายการจะมีการแลกเปลี่ยนของตนเอง น่าเสียดายที่ไม่มีทางออกที่ดีที่สุดที่ชัดเจนและจะขึ้นอยู่กับแอปพลิเคชันจริงๆ

การผูกคลาสอัตโนมัติ
ดังที่แสดงในคำถามของคุณ:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • ดี / ไม่ดี: สิ่งนี้จะสร้างการปิดเพิ่มเติมต่อวิธีต่ออินสแตนซ์ของชั้นเรียนของคุณ หากมักใช้วิธีนี้เฉพาะในการเรียกใช้เมธอดปกตินั่นคือ overkill อย่างไรก็ตามหากมีการใช้งานมากในตำแหน่งการเรียกกลับอินสแตนซ์ของคลาสจะมีประสิทธิภาพมากกว่าในการจับthisบริบทแทนที่จะเป็นไซต์การโทรแต่ละไซต์ที่สร้างการปิดใหม่เมื่อเรียกใช้
  • ดี: เป็นไปไม่ได้ที่ผู้โทรภายนอกจะลืมจัดการthisบริบท
  • ดี: typesafe ใน TypeScript
  • ดี: ไม่ต้องทำงานพิเศษหากฟังก์ชันมีพารามิเตอร์
  • ไม่ดี: คลาสที่ได้รับมาไม่สามารถเรียกเมธอดคลาสพื้นฐานที่เขียนด้วยวิธีนี้ได้ super.
  • ไม่ดี: ความหมายที่แน่นอนของวิธีการที่ "ผูกไว้ล่วงหน้า" และไม่ได้สร้างสัญญาเพิ่มเติมที่ไม่ปลอดภัยระหว่างคลาสของคุณกับผู้บริโภค

Function.bind
ตามที่แสดง:

$(document).ready(thisTest.run.bind(thisTest));
  • ดี / ไม่ดี: การแลกเปลี่ยนหน่วยความจำ / ประสิทธิภาพตรงข้ามเมื่อเทียบกับวิธีแรก
  • ดี: ไม่ต้องทำงานพิเศษหากฟังก์ชันมีพารามิเตอร์
  • ไม่ดี: ใน TypeScript ขณะนี้ไม่มีประเภทความปลอดภัย
  • ไม่ดี: ใช้ได้เฉพาะใน ECMAScript 5 เท่านั้นหากเป็นเรื่องสำคัญสำหรับคุณ
  • ไม่ดี: คุณต้องพิมพ์ชื่ออินสแตนซ์สองครั้ง

Fat arrow
ใน TypeScript (แสดงที่นี่พร้อมกับพารามิเตอร์จำลองเพื่อเหตุผลอธิบาย):

$(document).ready((n, m) => thisTest.run(n, m));
  • ดี / ไม่ดี: การแลกเปลี่ยนหน่วยความจำ / ประสิทธิภาพตรงข้ามเมื่อเทียบกับวิธีแรก
  • ดี: ใน TypeScript มีความปลอดภัย 100%
  • ดี: ใช้งานได้ใน ECMAScript 3
  • ดี: คุณต้องพิมพ์ชื่ออินสแตนซ์เพียงครั้งเดียว
  • ไม่ดี: คุณจะต้องพิมพ์พารามิเตอร์สองครั้ง
  • ไม่ดี: ใช้ไม่ได้กับพารามิเตอร์ตัวแปร

1
+1 คำตอบที่ดี Ryan ชอบการแจกแจงข้อดีข้อเสียขอบคุณ!
Jonathan Moffatt

- ใน Function.bind ของคุณคุณสร้างการปิดใหม่ทุกครั้งที่คุณต้องการแนบเหตุการณ์
131

1
ศรอ้วนเพิ่งทำ !! : D: D = () => ขอบคุณมาก! : D
Christopher Stock

@ ryan-cavanaugh สิ่งที่ดีและไม่ดีในแง่ของเวลาที่วัตถุจะถูกปลดปล่อย? ดังตัวอย่างสปาที่ใช้งานได้มากกว่า 30 นาทีข้อใดที่ดีที่สุดสำหรับพนักงานเก็บขยะ JS ในการจัดการ
abbaf33f

ทั้งหมดนี้จะสามารถใช้ได้ฟรีเมื่ออินสแตนซ์ของคลาสนั้นฟรี สองตัวหลังจะสามารถใช้งานได้ฟรีก่อนหน้านี้หากอายุการใช้งานของตัวจัดการเหตุการณ์สั้นลง โดยทั่วไปแล้วฉันจะบอกว่าจะไม่มีความแตกต่างที่วัดได้
Ryan Cavanaugh

16

อีกวิธีหนึ่งที่ต้องใช้การตั้งค่าเริ่มต้น แต่จ่ายออกด้วยการใช้ไวยากรณ์คำเดียวที่เบาอย่างไม่อาจหลีกเลี่ยงได้คือการใช้Method Decoratorsกับวิธีผูก JIT ผ่าน getters

ฉันได้สร้างrepo บน GitHubเพื่อแสดงการนำแนวคิดนี้ไปใช้(มันค่อนข้างยาวเพื่อให้พอดีกับคำตอบที่มีรหัส 40 บรรทัดรวมถึงความคิดเห็น)ซึ่งคุณจะใช้ง่ายๆดังนี้:

class DemonstrateScopingProblems {
    private status = "blah";

    @bound public run() {
        alert(this.status);
    }
}

ฉันยังไม่เคยเห็นสิ่งนี้ที่กล่าวถึงเลย แต่มันทำงานได้อย่างไม่มีที่ติ นอกจากนี้ไม่มีข้อเสียที่น่าสังเกตสำหรับวิธีนี้: การใช้งานมัณฑนากรนี้ - รวมถึงการตรวจสอบบางประเภทสำหรับความปลอดภัยของรันไทม์ - เป็นเรื่องเล็กน้อยและตรงไปตรงมาและมาพร้อมกับค่าโสหุ้ยที่เป็นศูนย์หลังจากเรียกวิธีการเริ่มต้น

ส่วนสำคัญคือการกำหนด getter ต่อไปนี้บนคลาสต้นแบบซึ่งดำเนินการทันทีก่อนการเรียกครั้งแรก:

get: function () {
    // Create bound override on object instance. This will hide the original method on the prototype, and instead yield a bound version from the
    // instance itself. The original method will no longer be accessible. Inside a getter, 'this' will refer to the instance.
    var instance = this;

    Object.defineProperty(instance, propKey.toString(), {
        value: function () {
            // This is effectively a lightweight bind() that skips many (here unnecessary) checks found in native implementations.
            return originalMethod.apply(instance, arguments);
        }
    });

    // The first invocation (per instance) will return the bound method from here. Subsequent calls will never reach this point, due to the way
    // JavaScript runtimes look up properties on objects; the bound method, defined on the instance, will effectively hide it.
    return instance[propKey];
}

แหล่งที่มาแบบเต็ม


แนวคิดนี้สามารถก้าวไปได้อีกขั้นหนึ่งโดยทำสิ่งนี้ในมัณฑนากรชั้นเรียนแทนโดยทำซ้ำวิธีการและกำหนดตัวบอกคุณสมบัติข้างต้นสำหรับแต่ละรายการในครั้งเดียว


สิ่งที่ฉันต้องการ!
Marcel van der Drift

14

เนโครแมนซิ่ง.
มีวิธีแก้ปัญหาง่ายๆที่เห็นได้ชัดซึ่งไม่ต้องใช้ฟังก์ชันลูกศร (ฟังก์ชันลูกศรช้าลง 30%) หรือวิธีการ JIT ผ่าน getters
วิธีแก้ปัญหานั้นคือการผูกบริบทนี้ในตัวสร้าง

class DemonstrateScopingProblems 
{
    constructor()
    {
        this.run = this.run.bind(this);
    }


    private status = "blah";
    public run() {
        alert(this.status);
    }
}

คุณสามารถเขียนวิธี autobind เพื่อผูกฟังก์ชันทั้งหมดในตัวสร้างของคลาสโดยอัตโนมัติ:

class DemonstrateScopingProblems 
{

    constructor()
    { 
        this.autoBind(this);
    }
    [...]
}


export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {
        const val = self[key];

        if (key !== 'constructor' && typeof val === 'function')
        {
            // console.log(key);
            self[key] = val.bind(self);
        } // End if (key !== 'constructor' && typeof val === 'function') 

    } // Next key 

    return self;
} // End Function autoBind

โปรดทราบว่าถ้าคุณไม่ใส่ฟังก์ชัน autobind ลงในคลาสเดียวกับฟังก์ชันสมาชิกมันก็เป็นเพียงautoBind(this);และไม่ใช่this.autoBind(this);

นอกจากนี้ฟังก์ชัน autoBind ข้างต้นจะถูกปิดลงเพื่อแสดงหลักการ
หากคุณต้องการให้สิ่งนี้ทำงานได้อย่างน่าเชื่อถือคุณต้องทดสอบว่าฟังก์ชันนั้นเป็น getter / setter ของคุณสมบัติด้วยหรือไม่เพราะไม่เช่นนั้น - boom - ถ้าคลาสของคุณมีคุณสมบัตินั่นคือ

แบบนี้:

export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {

        if (key !== 'constructor')
        {
            // console.log(key);

            let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);

            if (desc != null)
            {
                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                {
                    if (g)
                        desc.get = desc.get.bind(self);

                    if (s)
                        desc.set = desc.set.bind(self);

                    Object.defineProperty(self.constructor.prototype, key, desc);
                    continue; // if it's a property, it can't be a function 
                } // End if (g || s) 

            } // End if (desc != null) 

            if (typeof (self[key]) === 'function')
            {
                let val = self[key];
                self[key] = val.bind(self);
            } // End if (typeof (self[key]) === 'function') 

        } // End if (key !== 'constructor') 

    } // Next key 

    return self;
} // End Function autoBind

ฉันต้องใช้ "autoBind (this)" ไม่ใช่ "this.autoBind (this)"
JohnOpincar

@JohnOpincar: ใช่ this.autoBind (นี่) ถือว่า autobind อยู่ในคลาสไม่ใช่เป็นการส่งออกแยกต่างหาก
สเตฟานสไตเกอร์

ฉันเข้าใจแล้ว. คุณวางเมธอดในคลาสเดียวกัน ฉันใส่ลงในโมดูล "ยูทิลิตี้"
JohnOpincar

2

ในรหัสของคุณคุณได้ลองเปลี่ยนบรรทัดสุดท้ายดังนี้หรือไม่?

$(document).ready(() => thisTest.run());
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.