Подтвердить что ты не робот

Как сохранить лексическую область в TypeScript с помощью функции обратного вызова

У меня есть класс TypeScript с функцией, которую я намерен использовать в качестве обратного вызова:

removeRow(_this:MyClass): void {
    ...
    // 'this' is now the window object
    // I must use '_this' to get the class itself
    ...
}

Я передаю его другой функции

this.deleteRow(this.removeRow);

который в свою очередь вызывает метод JQuery Ajax, который в случае успеха вызывает обратный вызов следующим образом:

deleteItem(removeRowCallback: (_this:MyClass) => void ): void {
    $.ajax(action, {
        data: { "id": id },
        type: "POST"
    })
    .done(() => {
        removeRowCallback(this);
    })
    .fail(() => {
        alert("There was an error!");
    });
}

Единственный способ сохранить ссылку 'this' для моего класса - передать его на обратный вызов, как показано выше. Он работает, но он затягивает код. Если я не подключаю 'this' к этому (извините), то любая ссылка на это в методе обратного вызова вернулась к объекту Window. Поскольку я все время использую функции со стрелками, я ожидал, что 'this' будет самим классом, так как он находится в другом месте моего класса.

Кто-нибудь знает, как передавать обратные вызовы в TypeScript, сохраняя лексическую область?

4b9b3361

Ответ 1

Изменить 2014-01-28:

Новые читатели, убедитесь, что вы посмотрите ответ Zac ниже.

У него есть более аккуратное решение, которое позволит вам определить и создать экземпляр функции с областью в определении класса с использованием синтаксиса толстой стрелки.

Единственное, что я добавлю, это то, что в отношении ответа опции 5 в ответе Zac можно указать тип подписи метода и тип возвращаемого значения без повторения с использованием этого синтаксиса:

public myMethod = (prop1: number): string => {
    return 'asdf';
}

Редактировать 2013-05-28:

Синтаксис для определения типа свойства функции изменился (начиная с TypeScript версии 0.8).

Раньше вы определяли бы тип функции следующим образом:

class Test {
   removeRow: (): void;
}

Теперь это изменилось на:

class Test {
   removeRow: () => void;
}

Я обновил свой ответ ниже, чтобы включить это новое изменение.

В качестве дополнительного: если вам нужно определить несколько сигнатур функций для одного и того же имени функции (например, перегрузка функции времени выполнения), вы можете использовать нотацию карты объекта (это широко используется в файле дескриптора jQuery):

class Test {
    removeRow: {
        (): void;
        (param: string): string;
    };
}

Вам нужно определить подпись для removeRow() как свойство в вашем классе, но назначить реализацию в конструкторе.

Есть несколько способов сделать это.

Вариант 1

class Test {

    // Define the method signature here.
    removeRow: () => void;

    constructor (){
        // Implement the method using the fat arrow syntax.
        this.removeRow = () => {
            // Perform your logic to remove the row.
            // Reference `this` as needed.
        }
    }

}

Если вы хотите, чтобы ваш конструктор был минимальным, вы можете просто сохранить метод removeRow в определении класса и просто назначить прокси-функцию в конструкторе:

Вариант 2

class Test {

    // Again, define the method signature here.
    removeRowProxy: () => void;

    constructor (){
        // Assign the method implementation here.
        this.removeRowProxy = () => {
            this.removeRow.apply(this, arguments);
        }
    }

    removeRow(): void {
        // ... removeRow logic here.
    }

}

Вариант 3

И, наконец, если вы используете библиотеку, например, подчеркивание или jQuery, тогда вы можете просто использовать их служебный метод для создания прокси:

class Test {

    // Define the method signature here.
    removeRowProxy: () => void;

    constructor (){
        // Use jQuery to bind removeRow to this instance.
        this.removeRowProxy = $.proxy(this.removeRow, this);
    }

    removeRow(): void {
        // ... removeRow logic here.
    }

}

Затем вы можете немного убрать свой метод deleteItem:

// Specify `Function` as the callback type.
// NOTE: You can define a specific signature if needed.
deleteItem(removeRowCallback: Function ): void {
    $.ajax(action, {
        data: { "id": id },
        type: "POST"
    })

    // Pass the callback here.
    // 
    // You don't need the fat arrow syntax here
    // because the callback has already been bound
    // to the correct scope.
    .done(removeRowCallback)

    .fail(() => {
        alert("There was an error!");
    });
}

Ответ 2

ОБНОВЛЕНИЕ: см. Sly обновленный ответ. Он включает улучшенную версию нижеуказанных вариантов.

ДРУГОЕ ОБНОВЛЕНИЕ: Generics

Иногда вы хотите указать общий тип в сигнатуре функции, не указывая его на весь класс. Мне потребовалось несколько попыток выяснить синтаксис, поэтому я подумал, что это может быть полезно:

class MyClass {  //no type parameter necessary here

     public myGenericMethod = <T>(someArg:string): QPromise<T> => {

         //implementation here...

     }
}

Вариант 4

Вот еще несколько синтаксисов, чтобы добавить Sly_cardinal ответ. Эти примеры сохраняют декларацию и реализацию функции в одном и том же месте:

class Test {

      // Define the method signature AND IMPLEMENTATION here.
      public removeRow: () => void = () => {

        // Perform your logic to remove the row.
        // Reference `this` as needed.

      }

      constructor (){

      }

}

или

Вариант 5

Немного более компактный, но дает явный возвращаемый тип (компилятор должен вывести возвращаемый тип в любом случае, если не явный):

class Test {

      // Define implementation with implicit signature and correct lexical scope.
      public removeRow = () => {

        // Perform your logic to remove the row.
        // Reference `this` as needed.

      }

      constructor (){

      }

}

Ответ 3

Это своего рода перекрестная запись из другого ответа (Есть ли псевдоним для "this" в TypeScript?). Я повторно применил концепцию, используя приведенные выше примеры. Мне нравится это лучше, чем приведенные выше опции, потому что он явно поддерживает область видимости "this" как для экземпляра класса, так и для объекта динамического контекста, который вызывает этот метод.

Ниже приведены две версии. Мне нравится первый, потому что компилятор помогает правильно его использовать (вы не будете так же легко пытаться неправильно использовать сам callback lambda в качестве обратного вызова из-за явно введенного параметра).

Проверьте это: http://www.typescriptlang.org/Playground/

class Test {

    private testString: string = "Fancy this!";

    // Define the method signature here.
    removeRowLambdaCallback(outerThis: Test): {(): void} {
        alert("Defining callback for consumption");
        return function(){
            alert(outerThis.testString); // lexically scoped class instance
            alert(this); // dynamically scoped context caller
            // Put logic here for removing rows. Can refer to class
            // instance as well as "this" passed by a library such as JQuery or D3.
        }
    }
    // This approach looks nicer, but is more dangerous
    // because someone might use this method itself, rather
    // than the return value, as a callback.
     anotherRemoveRowLambdaCallback(): {(): void} {
        var outerThis = this;
        alert("Defining another callback for consumption");
        return function(){
            alert(outerThis.testString); // lexically scoped class instance
            alert(this); // dynamically scoped context caller
            // Put logic here for removing rows. Can refer to class
            // instance as well as "this" passed by a library such as JQuery or D3.
        }
    }
}

var t = new Test();
var callback1 = t.removeRowLambdaCallback(t);
var callback2 = t.anotherRemoveRowLambdaCallback();

callback1();
callback2();

Ответ 4

Используйте .bind(), чтобы сохранить контекст в обратном вызове.

Пример рабочего кода:

window.addEventListener(
  "resize",
  (()=>{this.retrieveDimensionsFromElement();}).bind(this)
)

Код в исходном вопросе станет примерно таким:

$.ajax(action, {
    data: { "id": id },
    type: "POST"
})
.done(
  (() => {
    removeRowCallback();
  }).bind(this)
)

Он установит контекст (это) внутри функции обратного вызова на все, что было передано в качестве аргумента для функции привязки, в этом случае это оригинал этого объекта.

Ответ 5

Основываясь на хитрых и Zac ответах типами: Полный пример приветствия. Надеюсь, это приветствуется, поскольку это самый лучший результат в Google, при поиске "typescript обратных вызовов javascript"

type MyCallback = () => string;

class HelloWorld {

    // The callback
    public callback: MyCallback = () => {
        return 'world';
    }

    // The caller
    public caller(callback: MyCallback) {
        alert('Hello ' + callback());
    }
}

let hello = new HelloWorld();
hello.caller(hello.callback);

Это передается в:

var HelloWorld = (function () {
    function HelloWorld() {
        // The callback
        this.callback = function () {
            return 'world';
        };
    }
    // The caller
    HelloWorld.prototype.caller = function (callback) {
        alert('Hello ' + callback());
    };
    return HelloWorld;
}());
var hello = new HelloWorld();
hello.caller(hello.callback);

Надеюсь, что кто-то найдет это немного полезным.:)