Vấn đề xác định phạm vi "this" của TypeScript khi được gọi trong lệnh gọi lại jquery


107

Tôi không chắc về cách tiếp cận tốt nhất để xử lý phạm vi "cái này" trong TypeScript.

Đây là một ví dụ về một mẫu phổ biến trong mã mà tôi đang chuyển đổi sang 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); 

Bây giờ, tôi có thể thay đổi cuộc gọi thành ...

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

... cái nào hoạt động. Nhưng nó hơi kinh khủng. Nó có nghĩa là tất cả mã đều có thể biên dịch và hoạt động tốt trong một số trường hợp, nhưng nếu chúng ta quên ràng buộc phạm vi, nó sẽ bị hỏng.

Tôi muốn một cách để thực hiện nó trong lớp, để khi sử dụng lớp, chúng ta không cần phải lo lắng về phạm vi "cái này" là gì.

Bất kỳ đề xuất?

Cập nhật

Một cách tiếp cận khác hiệu quả là sử dụng mũi tên béo:

class DemonstrateScopingProblems {
    private status = "blah";

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

Đó có phải là một cách tiếp cận hợp lệ?


2
Điều này sẽ hữu ích: youtube.com/watch?v=tvocUcbCupA
basarat

Lưu ý: Ryan đã sao chép câu trả lời của mình vào TypeScript Wiki .
Franklin Yu

Tìm giải pháp TypeScript 2+ ở đây .
Deilan

Câu trả lời:


166

Bạn có một vài lựa chọn ở đây, mỗi lựa chọn đều có sự đánh đổi riêng. Thật không may, không có giải pháp rõ ràng tốt nhất và nó thực sự sẽ phụ thuộc vào ứng dụng.

Liên kết lớp học tự động
Như được hiển thị trong câu hỏi của bạn:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Tốt / xấu: Điều này tạo ra một đóng bổ sung cho mỗi phương thức trên mỗi phiên bản của lớp của bạn. Nếu phương thức này thường chỉ được sử dụng trong các cuộc gọi phương thức thông thường, thì điều này là quá mức cần thiết. Tuy nhiên, nếu nó được sử dụng nhiều trong các vị trí gọi lại, thì cá thể lớp sẽ hiệu quả hơn để nắm bắt thisngữ cảnh thay vì mỗi trang web gọi tạo ra một bao đóng mới khi gọi.
  • Tốt: Người gọi bên ngoài không thể quên xử lý thisngữ cảnh
  • Tốt: An toàn trong TypeScript
  • Tốt: Không phải làm thêm nếu hàm có tham số
  • Xấu: Các lớp có nguồn gốc không thể gọi các phương thức của lớp cơ sở được viết theo cách này bằng cách sử dụng super.
  • Xấu: Ngữ nghĩa chính xác của các phương thức được "ràng buộc trước" và không tạo ra một hợp đồng không an toàn bổ sung giữa lớp của bạn và người tiêu dùng.

Function.bind
Cũng như được hiển thị:

$(document).ready(thisTest.run.bind(thisTest));
  • Tốt / xấu: Sự cân bằng bộ nhớ / hiệu suất đối lập so với phương pháp đầu tiên
  • Tốt: Không phải làm thêm nếu hàm có tham số
  • Xấu: Trong TypeScript, điều này hiện không có an toàn kiểu
  • Kém: Chỉ khả dụng trong ECMAScript 5, nếu điều đó quan trọng với bạn
  • Xấu: Bạn phải nhập tên phiên bản hai lần

Mũi tên béo
Trong TypeScript (hiển thị ở đây với một số tham số giả cho các lý do giải thích):

$(document).ready((n, m) => thisTest.run(n, m));
  • Tốt / xấu: Sự cân bằng bộ nhớ / hiệu suất đối lập so với phương pháp đầu tiên
  • Tốt: Trong TypeScript, cái này có độ an toàn 100%
  • Tốt: Hoạt động trong ECMAScript 3
  • Tốt: Bạn chỉ phải nhập tên phiên bản một lần
  • Xấu: Bạn sẽ phải nhập các thông số hai lần
  • Xấu: Không hoạt động với các thông số đa dạng

1
+1 Câu trả lời tuyệt vời Ryan, yêu thích sự phân tích ưu và nhược điểm, cảm ơn!
Jonathan Moffatt

- Trong Function.bind của bạn, bạn tạo một đóng mới mỗi khi bạn cần đính kèm sự kiện.
131

1
Mũi tên béo vừa làm được !! : D: D = () => Xin chân thành cảm ơn! : D
Christopher Stock

@ ryan-cavanaugh thì sao về mặt tốt và xấu về thời điểm vật thể sẽ được giải phóng? Như trong ví dụ về một SPA hoạt động trong> 30 phút, điều nào ở trên là tốt nhất để trình thu gom rác JS xử lý?
abbaf33f

Tất cả những điều này sẽ được tự do khi cá thể lớp có thể tự do. Hai phần sau sẽ được miễn phí sớm hơn nếu thời gian tồn tại của trình xử lý sự kiện ngắn hơn. Nói chung, tôi muốn nói rằng sẽ không có sự khác biệt có thể đo lường được.
Ryan Cavanaugh

16

Một giải pháp khác yêu cầu một số thiết lập ban đầu nhưng được đền đáp bằng ánh sáng bất khả chiến bại của nó, theo nghĩa đen, cú pháp một từ là sử dụng Method Decorator cho các phương thức JIT-bind thông qua getters.

Tôi đã tạo một repo trên GitHub để giới thiệu việc triển khai ý tưởng này (hơi dài để đưa vào một câu trả lời với 40 dòng mã của nó, bao gồm cả nhận xét) , mà bạn sẽ sử dụng đơn giản như:

class DemonstrateScopingProblems {
    private status = "blah";

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

Tôi chưa thấy điều này được đề cập ở bất cứ đâu, nhưng nó hoạt động hoàn hảo. Ngoài ra, không có nhược điểm đáng chú ý nào đối với cách tiếp cận này: việc triển khai trình trang trí này - bao gồm một số kiểm tra kiểu để đảm bảo an toàn kiểu thời gian chạy - là đơn giản và đơn giản, và về cơ bản là không có chi phí sau khi gọi phương thức ban đầu.

Phần thiết yếu là xác định getter sau trên nguyên mẫu lớp, được thực thi ngay trước lệnh gọi đầu tiên:

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];
}

Nguồn đầy đủ


Ý tưởng cũng có thể được thực hiện thêm một bước nữa, bằng cách thực hiện điều này trong trình trang trí lớp thay vào đó, lặp lại các phương thức và xác định bộ mô tả thuộc tính ở trên cho mỗi phương thức trong một lần chuyển.


đúng thứ tôi cần!
Marcel van der Drift

14

Thăng bằng.
Có một giải pháp đơn giản rõ ràng không yêu cầu các hàm mũi tên (các hàm mũi tên chậm hơn 30%) hoặc các phương thức JIT thông qua getters.
Giải pháp đó là ràng buộc ngữ cảnh này trong hàm tạo.

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


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

Bạn có thể viết một phương thức autobind để tự động liên kết tất cả các hàm trong phương thức khởi tạo của lớp:

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

Lưu ý rằng nếu bạn không đặt hàm autobind vào cùng lớp với hàm thành viên, nó chỉ là autoBind(this);và khôngthis.autoBind(this);

Và ngoài ra, chức năng autoBind ở trên được giảm bớt, để hiển thị nguyên tắc.
Nếu bạn muốn điều này hoạt động một cách đáng tin cậy, bạn cần phải kiểm tra xem hàm có phải là getter / setter của một thuộc tính hay không, bởi vì ngược lại - boom - nếu lớp của bạn chứa các thuộc tính, nghĩa là.

Như thế này:

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

Tôi đã phải sử dụng "autoBind (this)" chứ không phải "this.autoBind (this)"
JohnOpincar

@JohnOpincar: vâng, this.autoBind (this) giả định autobind nằm trong lớp, không phải là xuất riêng biệt.
Stefan Steiger

Giờ thì tôi đã hiểu. Bạn đặt phương thức trên cùng một lớp. Tôi đặt nó vào một mô-đun "tiện ích".
JohnOpincar

2

Trong mã của bạn, bạn đã thử chỉ thay đổi dòng cuối cùng như sau chưa?

$(document).ready(() => thisTest.run());
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.