Чи існує псевдонім для 'this' у TypeScript?


75

Я намагався написати клас у TypeScript, який має визначений метод, який діє як зворотний виклик обробника події до події jQuery.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This is not good.
    }
}

В обробнику подій onFocusIn TypeScript бачить 'this' як 'this' класу. Однак jQuery замінює це посилання і встановлює його як об'єкт DOM, пов'язаний з подією.

Одним із варіантів є визначення лямбди в конструкторі як обробника подій, і в цьому випадку TypeScript створює своєрідне закриття із прихованим псевдонімом _this.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e) => {
            var height = this.textarea.css('height'); // <-- This is good.
        });
    }
}

Моє питання полягає в тому, чи є інший спосіб отримати доступ до цього посилання в обробнику подій на основі методу за допомогою TypeScript, щоб подолати цю поведінку jQuery?


3
Відповідь з використанням () => {} виглядає добре
Даніель Літт

Відповіді:


22

Отож, як зазначено, не існує механізму TypeScript для забезпечення того, щоб метод завжди був прив’язаний до його thisвказівника (і це не просто проблема jQuery.) Це не означає, що немає досить простого способу вирішення цієї проблеми. Вам потрібно створити проксі-сервер для вашого методу, який відновлює thisпокажчик перед викликом зворотного виклику. Потім вам потрібно обернути свій зворотний виклик цим проксі-сервером, перш ніж передавати його у подію. jQuery має вбудований механізм для цього jQuery.proxy(). Ось приклад наведеного вище коду за допомогою цього методу (зверніть увагу на доданий $.proxy()дзвінок).

class Editor { 
    textarea: JQuery; 

    constructor(public id: string) { 
        this.textarea = $(id); 
        this.textarea.focusin($.proxy(onFocusIn, this)); 
    } 

    onFocusIn(e: JQueryEventObject) { 
        var height = this.textarea.css('height'); // <-- This is not good. 
    } 
} 

Це розумне рішення, але я особисто виявив, що розробники часто забувають включити виклик проксі, тому я придумав альтернативне рішення цієї проблеми на основі TypeScript. Використовуючи, HasCallbacksклас нижче всього, що вам потрібно зробити, це вивести ваш клас, HasCallbacksі тоді будь-які методи, префіксовані 'cb_', матимуть thisпостійно пов’язаний вказівник. Ви просто не можете викликати цей метод за допомогою іншого thisвказівника, що в більшості випадків є кращим. Будь-який механізм працює, тому саме той, який вам зручніший у використанні.

class HasCallbacks {
    constructor() {
        var _this = this, _constructor = (<any>this).constructor;
        if (!_constructor.__cb__) {
            _constructor.__cb__ = {};
            for (var m in this) {
                var fn = this[m];
                if (typeof fn === 'function' && m.indexOf('cb_') == 0) {
                    _constructor.__cb__[m] = fn;                    
                }
            }
        }
        for (var m in _constructor.__cb__) {
            (function (m, fn) {
                _this[m] = function () {
                    return fn.apply(_this, Array.prototype.slice.call(arguments));                      
                };
            })(m, _constructor.__cb__[m]);
        }
    }
}

class Foo extends HasCallbacks  {
    private label = 'test';

    constructor() {
        super();

    }

    public cb_Bar() {
        alert(this.label);
    }
}

var x = new Foo();
x.cb_Bar.call({});

1
$.proxy()Виклик є відмінним рішенням для моєї ситуації. Надання thisпсевдоніма означало б ускладнення ТС, і, як зазначали інші, це було б важко зробити назад-сумісним способом.
Тодд,

1
Думаю, я хотів би додати, що я знаю, що код для HasCallbacks виглядає трохи страшним, але все це робиться через перегляд членів ваших класів та попереднє з'єднання їх із проксі. Якщо ваш проект невеликий, можливо, найкраще використовувати $ .proxy (). Однак, якщо ваш проект великий, клас HasCallbacks призведе до того, що менше буде завантажено коду в клієнт (у вас немає всіх цих зайвих викликів $ .proxy ()) і буде менше схильне до помилок. З точки зору виконання, обидва підходи мають приблизно однакову продуктивність (HasCallbacks має одноразові накладні витрати на перерахування для кожного класу), тому це насправді ваш вибір.
Steven Ickman

3
Я думаю, що ваша відповідь може ввести в оману, оскільки у мене склалося враження, що TypeScript цього не підтримує, тоді як насправді це робить, якщо ви просто визначаєте свої методи за допомогою синтаксису стрілки.
Сем

Ця відповідь може запропонувати рішення, але вона є неправильною, оскільки TypeScript надає механізм підтримки лексичного масштабу цього. Дивіться відповіді Сема на 2 варіанти використання функцій зі стрілками для їх вирішення.
бінглінг

100

Сфера thisзберігається при використанні синтаксису функції стрілки () => { ... }- ось приклад, взятий з TypeScript для програмістів JavaScript .

var ScopeExample = { 
  text: "Text from outer function", 
  run: function() { 
    setTimeout( () => { 
      alert(this.text); 
    }, 1000); 
  } 
};

Зверніть увагу, що це this.textдає вам, Text from outer functionоскільки синтаксис функції стрілки зберігає "лексичний обсяг".


Контекст "цього" не може бути змінено на вашому прикладі, і тому те, що ви показали, насправді не застосовується.
Пол Мендоса,

@PaulMendoza насправді, якщо ви його зміните, то setTimeout( function () { alert(this.text); }, 1000);отримаєте undefined.
Фентон,

Ти маєш рацію. Моє ліжко. Якось () => повідомляє компілятору, що це означає щось інше, ніж виконання функції () {}
Пол Мендоса,

9
Це чудово. Реквізит! Це робить за кадром створення a var _this = thisу верхній частині методу, а потім посилання _thisв анонімній функції.
Річард Рут,

21

Як описано в деяких інших відповідях, використання синтаксису стрілки для визначення функції змушує посилання thisзавжди посилатися на клас, що вкладає.

Отже, щоб відповісти на ваше запитання, ось два простих обхідних шляхи.

Зверніться до методу, використовуючи синтаксис стрілки

constructor(public id: string) {
    this.textarea = $(id);
    this.textarea.focusin(e => this.onFocusIn(e));
}

Визначте метод, використовуючи синтаксис стрілки

onFocusIn = (e: JQueryEventObject) => {
    var height = this.textarea.css('height');
}

6

Ви можете прив'язати функцію-член до її екземпляра в конструкторі.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
        this.onFocusIn = this.onFocusIn.bind(this); // <-- Bind to 'this' forever
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height');   // <-- This is now fine
    }
}

Або ж просто прив’яжіть його, коли додаєте обробник.

        this.textarea.focusin(onFocusIn.bind(this));

це мій улюблений метод, але, на жаль, вам потрібен поліфіл bindдля старих браузерів.
antoine129

3

Спробуйте це

class Editor 
{

    textarea: JQuery;
    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e)=> { this.onFocusIn(e); });
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This will work
    }

}

3

Рішення Стівена Ікмана зручне, але неповне. Відповіді Денні Бекета та Сема коротші та більш ручні, і вони не вдаються в тому самому загальному випадку, коли є зворотний виклик, який потребує як динамічного, так і лексичного охоплення "цього" одночасно. Перейти до мого коду, якщо моє пояснення нижче TL; DR ...

Мені потрібно зберегти "this" для динамічного масштабування для використання із зворотними викликами бібліотеки, і мені потрібно мати "this" з лексичним масштабуванням до екземпляра класу. Я стверджую, що найелегантніше передавати екземпляр у генератор зворотних викликів, фактично дозволяючи закрити параметр над екземпляром класу. Компілятор повідомляє, якщо ви пропустили це. Я використовую домовленість про виклик лексично визначеного параметра "externalThis", але "self" або інше ім'я може бути кращим.

Використання ключового слова "this" викрадено з світу OO, і коли TypeScript прийняв його (із 6 специфікацій ECMAScript, я припускаю), вони поєднали поняття з лексичним масштабом та поняття з динамічним масштабом, коли метод викликається іншим суб'єктом . Я трохи збентежений цим; Я віддав би перевагу ключовому слову "self" у TypeScript, щоб я міг видалити екземпляр об'єкта з лексичним масштабом. Крім того, JS може бути перевизначений, щоб вимагати явний параметр "викликає" першої позиції, коли це потрібно (і, таким чином, розбивати всі веб-сторінки одним махом).

Ось моє рішення (вирізане з великого класу). Ознайомтеся особливо з методом виклику методів та, зокрема, тілом "dragmoveLambda":

export class OntologyMappingOverview {

initGraph(){
...
// Using D3, have to provide a container of mouse-drag behavior functions
// to a force layout graph
this.nodeDragBehavior = d3.behavior.drag()
        .on("dragstart", this.dragstartLambda(this))
        .on("drag", this.dragmoveLambda(this))
        .on("dragend", this.dragendLambda(this));

...
}

dragmoveLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
    console.log("redefine this for dragmove");

    return function(d, i){
        console.log("dragmove");
        d.px += d3.event.dx;
        d.py += d3.event.dy;
        d.x += d3.event.dx;
        d.y += d3.event.dy; 

        // Referring to "this" in dynamic scoping context
        d3.select(this).attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

        outerThis.vis.selectAll("line")
            .filter(function(e, i){ return e.source == d || e.target == d; })
            .attr("x1", function(e) { return e.source.x; })
            .attr("y1", function(e) { return e.source.y; })
            .attr("x2", function(e) { return e.target.x; })
            .attr("y2", function(e) { return e.target.y; });

    }
}

dragging: boolean  =false;
// *Call* these callback Lambda methods rather than passing directly to the callback caller.
 dragstartLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
        console.log("redefine this for dragstart");

        return function(d, i) {
            console.log("dragstart");
            outerThis.dragging = true;

            outerThis.forceLayout.stop();
        }
    }

dragendLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void}  {
        console.log("redefine this for dragend");

        return function(d, i) {
            console.log("dragend");
            outerThis.dragging = false;
            d.fixed = true;
        }
    }

}

Хоча ця реалізація начебто і потворна, це чудовий спосіб перевершити викрадення масштабу. +1
Керрі Кендалл

2

TypeScript не надає жодного додаткового способу (крім звичайних засобів JavaScript) повернутися до `` справжнього '' thisпосилання, крім thisзручності перепризначення, передбаченої синтаксисом лямбда-стрілки жирової стрілки (що дозволено з точки зору back-compat, оскільки жодного коду JS немає може використовуватися =>вираз).

Ви можете опублікувати пропозицію на сайті CodePlex, але з точки зору дизайну мови тут, мабуть, мало що може статися, оскільки будь-яке розумне ключове слово, яке компілятор може надати, може вже використовуватися наявним кодом JavaScript.



1

Я зіткнувся з подібною проблемою. Я думаю, ви можете використовувати .each()у багатьох випадках, щоб зберегти thisяк інше значення для наступних подій.

Шлях JavaScript:

$(':input').on('focus', function() {
  $(this).css('background-color', 'green');
}).on('blur', function() {
  $(this).css('background-color', 'transparent');
});

Шлях TypeScript:

$(':input').each((i, input) => {
  var $input = $(input);
  $input.on('focus', () => {
    $input.css('background-color', 'green');
  }).on('blur', () => {
    $input.css('background-color', 'transparent');
  });
});

Сподіваюся, це комусь допомагає.


0

Ви можете зберегти своє посилання на thisіншу змінну .. selfможливо, і отримати доступ до посилання таким чином. Я не використовую машинопис, але це метод, який був успішним для мене з ванільним javascript у минулому.


2
Єдина проблема з цим рішенням полягає в тому, що thisпосилання в методі зворотного виклику маскує thisпосилання на базовий об'єкт, тому немає можливості отримати доступ до нового властивості через правильний this.
Тодд,

1
Хоча це правильний підхід, я вважаю, що для досягнення цього краще використовувати вбудовану функціональність, яку надає TypeScript.
Сем

2
Тодд: Саме тому ви зберегли належне "це" у змінній до введення нової області (а отже, і нової "це"). Це досить поширена проблема JavaScript, а не лише машинопис (@Sam). Не знаю, чому голосування проти, але це все добре.
Шерідан Булгер

@SheridanBulger, голосування проти - тому, що це питання TypeScript, і TypeScript вже пропонує свій власний спосіб досягти цього. Я думаю, що загалом є гарною ідеєю використовувати мовні функції, а не переосмислювати їх самостійно.
Сем

@SheridanBulger Проголосував проти ... це не відповідь TypeScript.
Бенджамін


0

Рішення є набагато простішим, ніж усі вищезазначені відповіді. В основному ми повертаємося до JavaScript, використовуючи функцію ключового слова замість того, щоб використовувати конструкцію '=>', яка перекладе 'this' у клас 'this'

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        var self = this;                      // <-- This is save the reference
        this.textarea = $(id);
        this.textarea.focusin(function() {   // <-- using  javascript function semantics here
             self.onFocusIn(this);          //  <-- 'this' is as same as in javascript
        });
    }

    onFocusIn(jqueryObject : JQuery) {
        var height = jqueryObject.css('height'); 
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.