如何在类型方法中使用另一个类方法和此上下文?

时间:2018-04-06 15:53:31

标签: javascript typescript javascript-events this typescript2.0

我想重写JavaScript"方法"在TypeScript下面。 我很想在课堂上这样做:

// export default class 
export default class GroupItemMetadataProvider1
{
    protected m_instance;
    protected _grid;
    protected _defaults;

    protected m_options;
    constructor(options)
    {
        this.m_instance = this;

        this._defaults = {
            groupCssClass: "slick-group",
            groupTitleCssClass: "slick-group-title",
            totalsCssClass: "slick-group-totals",
            groupFocusable: true,
            totalsFocusable: false,
            toggleCssClass: "slick-group-toggle",
            toggleExpandedCssClass: "expanded",
            toggleCollapsedCssClass: "collapsed",
            enableExpandCollapse: true,
            groupFormatter: this.defaultGroupCellFormatter,
            totalsFormatter: this.defaultTotalsCellFormatter
        };

        options = $.extend(true, {}, this._defaults, options);
        this.m_options = options;
    }


    protected defaultGroupCellFormatter(row, cell, value, columnDef, item)
    {
        if (!this.m_options.enableExpandCollapse)
        {
            return item.title;
        }

        let indentation = item.level * 15 + "px";

        return "<span class='" + this.m_options.toggleCssClass + " " +
            (item.collapsed ? this.m_options.toggleCollapsedCssClass : this.m_options.toggleExpandedCssClass) +
            "' style='margin-left:" + indentation + "'>" +
            "</span>" +
            "<span class='" + this.m_options.groupTitleCssClass + "' level='" + item.level + "'>" +
            item.title +
            "</span>";
    }


    protected defaultTotalsCellFormatter(row, cell, value, columnDef, item)
    {
        return (columnDef.groupTotalsFormatter && columnDef.groupTotalsFormatter(item, columnDef)) || "";
    }


    protected init(grid)
    {
        this._grid = grid;
        this._grid.onClick.subscribe(this.handleGridClick);
        this._grid.onKeyDown.subscribe(this.handleGridKeyDown);
    }


    protected destroy()
    {
        if (this._grid)
        {
            this._grid.onClick.unsubscribe(this.handleGridClick);
            this._grid.onKeyDown.unsubscribe(this.handleGridKeyDown);
        }
    }


    protected handleGridClick(e, args)
    {
        let context = (<any>this);

        let item = context.getDataItem(args.row);
        if (item && item instanceof Slick.Group && $(e.target).hasClass(this.m_options.toggleCssClass))
        {
            let range = this._grid.getRenderedRange();
            context.getData().setRefreshHints({
                ignoreDiffsBefore: range.top,
                ignoreDiffsAfter: range.bottom + 1
            });

            if (item.collapsed)
            {
                context.getData().expandGroup(item.groupingKey);
            } else
            {
                context.getData().collapseGroup(item.groupingKey);
            }

            e.stopImmediatePropagation();
            e.preventDefault();
        }

    }


    // TODO:  add -/+ handling
    protected handleGridKeyDown(e)
    {
        let context = (<any>this);

        if (this.m_options.enableExpandCollapse && (e.which == Slick.keyCode.SPACE))
        {
            let activeCell = context.getActiveCell();
            if (activeCell)
            {
                let item = context.getDataItem(activeCell.row);
                if (item && item instanceof Slick.Group)
                {
                    let range = this._grid.getRenderedRange();
                    context.getData().setRefreshHints({
                        ignoreDiffsBefore: range.top,
                        ignoreDiffsAfter: range.bottom + 1
                    });

                    if (item.collapsed)
                    {
                        context.getData().expandGroup(item.groupingKey);
                    } else
                    {
                        context.getData().collapseGroup(item.groupingKey);
                    }

                    e.stopImmediatePropagation();
                    e.preventDefault();
                }
            }
        }

    }

    public getGroupRowMetadata(item)
    {
        return {
            selectable: false,
            focusable: this.m_options.groupFocusable,
            cssClasses: this.m_options.groupCssClass,
            columns: {
                0: {
                    colspan: "*",
                    formatter: this.m_options.groupFormatter,
                    editor: null
                }
            }
        };
    }

    public getTotalsRowMetadata(item)
    {
        return {
            selectable: false,
            focusable: this.m_options.totalsFocusable,
            cssClasses: this.m_options.totalsCssClass,
            formatter: this.m_options.totalsFormatter,
            editor: null
        };
    }
}

然而,handleGridClick&amp; handleGridKeyDown都引用了&#39; this&#39; &#34; event-context&#34;,以及&#39; this&#39; class-context获取选项。

问题是,我不能在构造函数中绑定它,因为否则,对象的this-context是错误的。我怎样才能做到这一点 ?

这是普通的JavaScript变体:

// import $ from '../../wwwroot/jQuery-3.3.js';
import Slick from './slick.core.js';

export default GroupItemMetadataProvider;

/***
 * Provides item metadata for group (Slick.Group) and totals (Slick.Totals) rows produced by the DataView.
 * This metadata overrides the default behavior and formatting of those rows so that they appear and function
 * correctly when processed by the grid.
 *
 * This class also acts as a grid plugin providing event handlers to expand & collapse groups.
 * If "grid.registerPlugin(...)" is not called, expand & collapse will not work.
 *
 * @class GroupItemMetadataProvider
 * @module Data
 * @namespace Slick.Data
 * @constructor
 * @param options
 */
function GroupItemMetadataProvider(options)
{
    let _grid;
    let _defaults = {
        groupCssClass: "slick-group",
        groupTitleCssClass: "slick-group-title",
        totalsCssClass: "slick-group-totals",
        groupFocusable: true,
        totalsFocusable: false,
        toggleCssClass: "slick-group-toggle",
        toggleExpandedCssClass: "expanded",
        toggleCollapsedCssClass: "collapsed",
        enableExpandCollapse: true,
        groupFormatter: defaultGroupCellFormatter,
        totalsFormatter: defaultTotalsCellFormatter
    };

    options = $.extend(true, {}, _defaults, options);

    function defaultGroupCellFormatter(row, cell, value, columnDef, item)
    {
        if (!options.enableExpandCollapse)
        {
            return item.title;
        }

        let indentation = item.level * 15 + "px";

        return "<span class='" + options.toggleCssClass + " " +
            (item.collapsed ? options.toggleCollapsedCssClass : options.toggleExpandedCssClass) +
            "' style='margin-left:" + indentation + "'>" +
            "</span>" +
            "<span class='" + options.groupTitleCssClass + "' level='" + item.level + "'>" +
            item.title +
            "</span>";
    }

    function defaultTotalsCellFormatter(row, cell, value, columnDef, item)
    {
        return (columnDef.groupTotalsFormatter && columnDef.groupTotalsFormatter(item, columnDef)) || "";
    }

    function init(grid)
    {
        _grid = grid;
        _grid.onClick.subscribe(handleGridClick);
        _grid.onKeyDown.subscribe(handleGridKeyDown);
    }

    function destroy()
    {
        if (_grid)
        {
            _grid.onClick.unsubscribe(handleGridClick);
            _grid.onKeyDown.unsubscribe(handleGridKeyDown);
        }
    }

    function handleGridClick(e, args)
    {
        let item = this.getDataItem(args.row);
        if (item && item instanceof Slick.Group && $(e.target).hasClass(options.toggleCssClass))
        {
            let range = _grid.getRenderedRange();
            this.getData().setRefreshHints({
                ignoreDiffsBefore: range.top,
                ignoreDiffsAfter: range.bottom + 1
            });

            if (item.collapsed)
            {
                this.getData().expandGroup(item.groupingKey);
            } else
            {
                this.getData().collapseGroup(item.groupingKey);
            }

            e.stopImmediatePropagation();
            e.preventDefault();
        }
    }

    // TODO:  add -/+ handling
    function handleGridKeyDown(e)
    {
        if (options.enableExpandCollapse && (e.which == Slick.keyCode.SPACE))
        {
            let activeCell = this.getActiveCell();
            if (activeCell)
            {
                let item = this.getDataItem(activeCell.row);
                if (item && item instanceof Slick.Group)
                {
                    let range = _grid.getRenderedRange();
                    this.getData().setRefreshHints({
                        ignoreDiffsBefore: range.top,
                        ignoreDiffsAfter: range.bottom + 1
                    });

                    if (item.collapsed)
                    {
                        this.getData().expandGroup(item.groupingKey);
                    } else
                    {
                        this.getData().collapseGroup(item.groupingKey);
                    }

                    e.stopImmediatePropagation();
                    e.preventDefault();
                }
            }
        }
    }

    function getGroupRowMetadata(item)
    {
        return {
            selectable: false,
            focusable: options.groupFocusable,
            cssClasses: options.groupCssClass,
            columns: {
                0: {
                    colspan: "*",
                    formatter: options.groupFormatter,
                    editor: null
                }
            }
        };
    }

    function getTotalsRowMetadata(item)
    {
        return {
            selectable: false,
            focusable: options.totalsFocusable,
            cssClasses: options.totalsCssClass,
            formatter: options.totalsFormatter,
            editor: null
        };
    }

    function getOptions()
    {
        return options;
    }


    return {
        init,
        destroy,
        getGroupRowMetadata,
        getTotalsRowMetadata,
        getOptions
    };
}

4 个答案:

答案 0 :(得分:4)

这更像是一个javascript而不是打字稿问题。

在示例1中,您尝试使用“类模式”,在示例2中,您使用类似“闭包类”的东西(这是我不记得的模式的名称)。

这两种模式都可以在TS中编写,我个人更喜欢保留“封闭类”(例2)。因此,您可以保留代码并添加类型注释。打开strict: true并为编译器发出的任何内容提供类型注释“具有隐式任意类型”。

提前个人意见

模式nr.2通常更具有可维护性(来源?),模式1更难以重构,需要更多类型注释,更多思考并为this绑定问题提供空间。你仍然希望在性能密集的事情上使用模式1(例如:游戏,这似乎不是你的情况),除此之外,模式2是你的去。即使是经典的OOP案例(扩展类和覆盖方法)也可以通过选项2(选项包模式)轻松获得。

Typescript的类型系统是结构化的 - 比“经典”java / C#强大得多 - 并且类在TS中不是名义上的。这些是不使用课程的另外两个原因。 class A {}class B {}或任何对象如果具有相同的属性,则可分配。

编辑:关于此绑定问题

如果你真的想坚持下去......班......

  • 您不能同时拥有this两件事。如果您的this是您的班级,那么您可以通过event.target找到您的元素。如果你的this反弹到一个元素......哦,好吧。

  • 所以你必须打电话给element.addEventListener("click", Instance.doSomething.bind(this))addEventListener重新绑定你的函数this.bind说:不。

  • element.addEventListener("click", (...i) => Instance.doSomething(...i))

  • 如果您的方法真的要从另一个this上下文调用,那么请写一些类似

    的内容

    方法(这:HTMLInputElement,x:number,y:string){}

this只不过是一个隐藏的函数参数(例如,python和lua显式地将其作为第一个参数发送),它被onX调用覆盖,这是一个数十亿美元的问题,这是JS课程吮吸的原因之一。

答案 1 :(得分:0)

我不完全确定我已正确理解您的问题,但我认为您说的是事件处理程序引用了错误的对象。

您需要在事件之外保存您的上下文/范围,然后您可以在内部引用它,如此

class GroupItemMetadataProvider1
{
    function init(grid)
    {
        let context = this;
        _grid = grid;
        _grid.addEventListener('click', (e, args) => context.handleGridClick(e, args));
        _grid.onKeyDown.subscribe(handleGridKeyDown);
    }

    function handleGridClick(e, args)
    {
        console.log(this); // will reference GroupItemMetadataProvider1
    }
}

答案 2 :(得分:0)

使用can arrow函数从声明上下文中捕获this,并使用辅助函数将事件上下文作为参数传递。辅助函数将使事件订阅并将订阅推送到数组中,以便取消订阅所有事件。

subscriptions: Array<{ unsubscribe: () => any; }> = []
bindAndSubscribe<TArg1, TArg2>(target: {
    subscribe(fn: (e: TArg1, data: TArg2) => any)
    unsubscribe(fn: (e: TArg1, data: TArg2) => any)
}, handler: (context: any, e: TArg1, arg: TArg2) => void) {
    let fn = function (e: TArg1, arg: TArg2) { handler(this, e, arg) };
    target.subscribe(fn);
    this.subscriptions.push({
        unsubscribe: () => target.unsubscribe(fn)
    });

}
protected init(grid: Slick.Grid<any>)
{
    this._grid = grid;
    // note paramters a and e are inffred correctly, if you ever want to add types
    this.bindAndSubscribe(this._grid.onClick, (c, e, a)=> this.handleGridClick(c, e, a));
    this.bindAndSubscribe(this._grid.onKeyDown, (c,e, a) => this.handleGridKeyDown(c,e));
}


protected destroy()
{
    if (this._grid)
    {
        this.subscriptions.forEach(s=> s.unsubscribe());
    }
}



protected handleGridClick(context, e, args)
{ 
    // correct this
    this.m_options.toggleCssClass
    //...
}


protected handleGridKeyDown(context, e)
{
    // Correct this, context is a parameter
    //...
}

您也可以直接将处理程序声明为箭头函数,任何一种方法都可以:

protected init(grid: Slick.Grid<any>)
{
    this._grid = grid;
    // Arrow function not needed here anymore, the handlers capture this themselves.
    this.bindAndSubscribe(this._grid.onClick, this.handleGridClick);
    this.bindAndSubscribe(this._grid.onKeyDown, this.handleGridKeyDown);
}




protected handleGridClick = (context, e, args) => 
{ 
    // correct this
    this.m_options.toggleCssClass
    //...
}


protected handleGridKeyDown = (context, e) => 
{
    // Correct this, context is a parameter
    //...
}

答案 3 :(得分:0)

正如本related ES6 answer中所解释的,这个问题在遗留的JavaScript库(D3等)中依然存在,它依赖于事件处理程序中的动态this,而不是将所有必要的数据作为参数传递。

同样的解决方案也适用于TypeScript,需要维护类型 安全

一种解决方案是使用旧的self = this配方,该配方已被弃用ES6箭头但在这种情况下仍需要并且有一些气味:

handleGridKeyDown(context: DynamicThisType, e: any) {
    // class instance is available as `this`
    // dynamic `this` is available as `context` param
}

...

const self = this;
this._grid.onKeyDown.subscribe(function (this: DynamicThisType, e: any) {
  return self.handleGridKeyDown(this, e);
});

另一个解决方案是使用TypeScript装饰器将此配方应用于某些方法,这也会产生一个方法,其类实例为this,动态thiscontext参数:

function bindAndPassContext(target: any, prop: string, descriptor?: PropertyDescriptor) {
    const fn = target[prop];

    return {
        configurable: true,
        get: function () {
            const classInstance = this;
            function wrapperFn (...args) {
                return fn.call(classInstance, this, ...args);
            }

            Object.defineProperty(this, prop, {
                configurable: true,
                writable: true,
                value: wrapperFn
            });

            return wrapperFn;
        }
    };
}

...

@bindAndPassContext
handleGridKeyDown(context: DynamicThisType, e: any) {
    // class instance is available as `this`
    // dynamic `this` is available as `context` param
}


...

this._grid.onKeyDown.subscribe(this.handleGridKeyDown);