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.