TypeScript "ця" проблема розгляду, коли викликається у зворотному звороті jquery


107

Я не впевнений у найкращому підході до визначення масштабу "цього" в 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

Примітка: Райан скопіював свою відповідь у TypeScript Wiki .
— Франклін Ю

Подивіться тут на рішення TypeScript 2+.
— Дейлан

Відповіді:


166

Тут у вас є кілька варіантів, кожен з яких має свої компроміси. На жаль, очевидного найкращого рішення немає, і це дійсно залежатиме від програми.

Автоматичне прив'язування класів
Як показано у вашому запитанні

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Добре / погано: Це створює додаткове закриття методу на примірник вашого класу. Якщо цей метод зазвичай використовується лише у звичайних викликах методів, це зайвий рівень. Однак якщо він використовується багато в позиціях зворотного виклику, для класового екземпляра ефективніше захоплювати thisконтекст замість кожного сайту виклику, створюючи нове закриття після виклику.
  • Добре: зовнішні абоненти не можуть забути обробляти thisконтекст
  • Добре: Typesafe в TypeScript
  • Добре: Немає зайвої роботи, якщо функція має параметри
  • Погано: Отримані класи не можуть викликати методи базового класу, написані таким чином, використовуючи super.
  • Погано: точна семантика методів, які "заздалегідь пов'язані", а які не створюють додатковий нетиповий контракт між вашим класом та його споживачами.

Function.bind
Також як показано:

$(document).ready(thisTest.run.bind(thisTest));
  • Добре / погано: протилежний обмін пам'яті / продуктивності порівняно з першим методом
  • Добре: Немає зайвої роботи, якщо функція має параметри
  • Погано: у TypeScript цей тип безпеки на даний момент не має
  • Погано: Доступно лише в ECMAScript 5, якщо це важливо для вас
  • Погано: Вам потрібно ввести ім'я екземпляра двічі

Стрілка жиру
в TypeScript (показано тут з деякими фіктивними параметрами з пояснювальних причин):

$(document).ready((n, m) => thisTest.run(n, m));
  • Добре / погано: протилежний обмін пам'яті / продуктивності порівняно з першим методом
  • Добре: у TypeScript це безпека на 100%
  • Добре: працює в ECMAScript 3
  • Добре: Вам потрібно лише ввести ім'я екземпляра один раз
  • Погано: вам доведеться вводити параметри двічі
  • Погано: не працює з різними параметрами

1
+1 Відмінна відповідь, Райан, люблю розбиття плюсів і мінусів, дякую!
— Джонатан Моффат

- У своєму Function.bind ви створюєте нове закриття кожного разу, коли вам потрібно прикріпити подію.
— 131

1
Жирна стрілка просто зробила це !! : D: D = () => Дуже дякую! : D
— Крістофер Сток

@ ryan-cavanaugh, що з добрим і поганим з точки зору того, коли об’єкт буде звільнений? Як у прикладі SPA, який працює протягом 30 хвилин, який із перерахованих вище найкращий для збору сміття JS?
— abbaf33f

Все це було б доступним, коли екземпляр класу є вільним. Останні два будуть доступними раніше, якщо термін служби обробника подій коротший. Загалом, я б сказав, що помітна різниця не буде.
— Райан Кавано

16

Ще одне рішення, яке вимагає певної налаштування, але окупається своїм незламно легким, буквально однословним синтаксисом, використовує Методи декораторів для приєднання методів JIT через getters.

Я створив репортаж на GitHub, щоб продемонструвати реалізацію цієї ідеї (трохи довгий, щоб вписатись у відповідь з її 40 рядків коду, включаючи коментарі) , які ви б використовували так само, як:

class DemonstrateScopingProblems {
    private status = "blah";

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

Я цього ще ніде не бачив, але це працює бездоганно. Також немає помітного недоліку цього підходу: реалізація цього декоратора - включаючи деяку перевірку типів на безпеку типу виконання - є тривіальною та простою і має фактично нульові накладні витрати після початкового виклику методу.

Істотною частиною є визначення наступного геттера в прототипі класу, який виконується безпосередньо перед першим викликом:

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

Повне джерело


Ідею можна також зробити ще на крок далі, зробивши це замість цього в декораторі класів, повторивши методи та визначивши описаний вище дескриптор властивості для кожного з них за один прохід.


просто те, що мені було потрібно!
— Марсель ван дер Дрифт

14

Некромантування.
Існує очевидне просте рішення, яке не потребує стрілочних функцій (функції стрілок на 30% повільніше) або JIT-методів через гетерів.
Це рішення полягає в прив'язці цього контексту в конструкторі.

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


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

Ви можете написати метод автозв’язку для автоматичного прив'язування всіх функцій у конструкторі класу:

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(this);і ніthis.autoBind(this);

А також, вищезазначена функція автозв'язки скидається на екран, щоб показати принцип.
Якщо ви хочете, щоб це надійно працювало, вам потрібно перевірити, чи функція є також збирачем / налаштовачем властивості, бо в іншому випадку - бум - якщо ваш клас містить властивості, тобто.

Подобається це:

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)" not "this.autoBind (this)"
— JohnOpincar

@JohnOpincar: так, this.autoBind (це) передбачає, що автопосилання знаходиться всередині класу, а не як окремий експорт.
— Стефан Штайгер

Я тепер розумію. Ви ставите метод на той самий клас. Я помістив його в модуль «утиліта».
— ДжонОпінкар

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.