Javascript 如何防止在自定义事件调度系统中意外更改事件的属性?

Javascript 如何防止在自定义事件调度系统中意外更改事件的属性?,javascript,events,encapsulation,Javascript,Events,Encapsulation,我已经建立了一个定制的事件调度机制。我试图尽可能多地模拟DOM事件实现。这仍然是一个草案,但到目前为止效果还算不错 但有一件事让我感到困扰,那就是我的事件的侦听器很容易改变该事件的特定属性,而实际上这些属性对于外部人员来说应该是只读的。我只希望分派事件的实际EventDispatcher能够更改这些属性 现在,我意识到基本上任何用户空间的Javascript对象都可以被修改,但这并不是我所担心的。我想防止侦听器中的事件属性发生意外更改,例如: function someListener( eve

我已经建立了一个定制的事件调度机制。我试图尽可能多地模拟DOM事件实现。这仍然是一个草案,但到目前为止效果还算不错

但有一件事让我感到困扰,那就是我的事件的侦听器很容易改变该事件的特定属性,而实际上这些属性对于外部人员来说应该是只读的。我只希望分派事件的实际
EventDispatcher
能够更改这些属性

现在,我意识到基本上任何用户空间的Javascript对象都可以被修改,但这并不是我所担心的。我想防止侦听器中的
事件
属性发生意外更改,例如:

function someListener( event ) {
    if( event.currentTarget = this ) { // whoops, we've accidentally overwritten event.currentTarget
       // do something
    }
}
问题是,我不清楚(至少在没有完全重构的情况下)如何实现这个问题的合理健壮的解决方案。我已经尝试过了(请参见
target
currentTarget
eventPhase
eventPhase
设置程序中我在下面提供的代码中注释掉的部分),但当然失败得很惨(一开始就不可行)。然而,我希望,通过检查这些部件,你会看到我的目标,也许你能提供一个可行的解决方案。它不必是密封的,只是相当简单

我试着想象DOM事件如何实现这个技巧(更改
event.currentTarget
,等等),并得出结论,它可能不是在(纯)Javascript本身中实现的,而是在幕后实现的

如果可能的话,我真的希望避免克隆事件或类似的实现思想,因为在处理事件阶段和访问不同的侦听器时,DOM事件似乎也不会克隆

以下是我当前的实现:

编目器.事件.事件

codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable );
        }

        let privateVars = {
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) || null !== privateVars.target ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.target = value;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.currentTarget = value;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.eventPhase = value;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            },
            'propagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.propagationStopped;
                }
            },
            'immediatePropagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.immediatePropagationStopped;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();
codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable, detail ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable, detail );
        }

        let privateVars = {
            instance: this,
            dispatched: false,
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            detail: undefined == detail ? null : detail,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        let processListeners = function( listeners ) {
            for( let listener of listeners ) {
                if( privateVars.immediatePropagationStopped ) {
                    return false;
                }
                listener.call( privateVars.currentTarget, privateVars.instance );
            }
            return true;
        }

        let processDispatcher = function( dispatcher, useCapture ) {
            privateVars.currentTarget = dispatcher.target;
            return processListeners( dispatcher.getEventListenersForEvent( privateVars.type, useCapture ) );
        }

        let processDispatchers = function( dispatchers, useCapture ) {
            for( let i = 0, l = dispatchers.length; !privateVars.propagationStopped && i < l; i++ ) {
                let dispatcher = dispatchers[ i ];
                if( !processDispatcher( dispatcher, useCapture ) ) {
                    return false;
                }
            }

            return true;
        }

        this.dispatch = function( dispatcher ) {
            if( privateVars.dispatched ) {
                throw new Error( 'This event has already been dispatched.' );
                return false;
            }

            if( !( dispatcher instanceof codifier.event.EventDispatcher ) ) {
                throw new Error( 'Only EventDispatchers are allowed to dispatch an event.' );
                return false;
            }

            privateVars.dispatched = true;
            let ancestors = dispatcher.getAncestors();
            do_while_label: // javascript needs a label to reference to break out of outer loops
            do {
                switch( privateVars.eventPhase ) {
                    case Event.NONE:
                        privateVars.target = dispatcher.target;
                        privateVars.currentTarget = dispatcher.target;
                        privateVars.eventPhase = Event.CAPTURING_PHASE;
                    break;
                    case Event.CAPTURING_PHASE:
                        if( !processDispatchers( ancestors.slice().reverse(), true ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = Event.AT_TARGET;
                    break;
                    case Event.AT_TARGET:
                        privateVars.currentTarget = dispatcher.target;
                        if( !processDispatcher( dispatcher, true ) || !processDispatcher( dispatcher, false ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = privateVars.bubbles ? Event.BUBBLING_PHASE : Event.NONE;
                    break;
                    case Event.BUBBLING_PHASE:
                        if( !processDispatchers( ancestors, false ) ) {
                            break do_while_label;
                        }
                        privateVars.currentTarget = null;
                        privateVars.eventPhase = Event.NONE;
                    break;
                    default:
                        // we should never be able to reach this
                        throw new Error( 'This event encountered an inconsistent internal state' );
                    break do_while_label; // break out of the do...while loop.
                }

            } while( !privateVars.propagationStopped && privateVars.eventPhase !== Event.NONE );

            privateVars.currentTarget = null;
            privateVars.eventPhase = Event.NONE;

            return !privateVars.defaultPrevented;
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'detail': {
                configurable: false,
                enumerable: true,
                get: function() {
                    return privateVars.detail;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();
编目器.event.EventDispatcher(仅限最相关的部分):


你对如何做这项工作有什么建议吗?当然,我并不期望有成熟的解决方案;只要几个通用的指针就好了。

我想我已经找到了一个(可能是临时的)解决方案,将实际的调度例程移动到
事件本身。我不喜欢这个解决方案,因为我不认为
事件
应该负责实际的调度过程,但目前我想不出其他任何东西

所以,如果你有其他的解决方案,我还是很想听听


编辑:使用最终(-ish)实现进行更新/edit

重构(未完成的)实现,就其现状而言(可能有相当多的bug,比以前的bug要少):

编目器.事件.事件

codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable );
        }

        let privateVars = {
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) || null !== privateVars.target ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.target = value;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.currentTarget = value;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.eventPhase = value;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            },
            'propagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.propagationStopped;
                }
            },
            'immediatePropagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.immediatePropagationStopped;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();
codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable, detail ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable, detail );
        }

        let privateVars = {
            instance: this,
            dispatched: false,
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            detail: undefined == detail ? null : detail,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        let processListeners = function( listeners ) {
            for( let listener of listeners ) {
                if( privateVars.immediatePropagationStopped ) {
                    return false;
                }
                listener.call( privateVars.currentTarget, privateVars.instance );
            }
            return true;
        }

        let processDispatcher = function( dispatcher, useCapture ) {
            privateVars.currentTarget = dispatcher.target;
            return processListeners( dispatcher.getEventListenersForEvent( privateVars.type, useCapture ) );
        }

        let processDispatchers = function( dispatchers, useCapture ) {
            for( let i = 0, l = dispatchers.length; !privateVars.propagationStopped && i < l; i++ ) {
                let dispatcher = dispatchers[ i ];
                if( !processDispatcher( dispatcher, useCapture ) ) {
                    return false;
                }
            }

            return true;
        }

        this.dispatch = function( dispatcher ) {
            if( privateVars.dispatched ) {
                throw new Error( 'This event has already been dispatched.' );
                return false;
            }

            if( !( dispatcher instanceof codifier.event.EventDispatcher ) ) {
                throw new Error( 'Only EventDispatchers are allowed to dispatch an event.' );
                return false;
            }

            privateVars.dispatched = true;
            let ancestors = dispatcher.getAncestors();
            do_while_label: // javascript needs a label to reference to break out of outer loops
            do {
                switch( privateVars.eventPhase ) {
                    case Event.NONE:
                        privateVars.target = dispatcher.target;
                        privateVars.currentTarget = dispatcher.target;
                        privateVars.eventPhase = Event.CAPTURING_PHASE;
                    break;
                    case Event.CAPTURING_PHASE:
                        if( !processDispatchers( ancestors.slice().reverse(), true ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = Event.AT_TARGET;
                    break;
                    case Event.AT_TARGET:
                        privateVars.currentTarget = dispatcher.target;
                        if( !processDispatcher( dispatcher, true ) || !processDispatcher( dispatcher, false ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = privateVars.bubbles ? Event.BUBBLING_PHASE : Event.NONE;
                    break;
                    case Event.BUBBLING_PHASE:
                        if( !processDispatchers( ancestors, false ) ) {
                            break do_while_label;
                        }
                        privateVars.currentTarget = null;
                        privateVars.eventPhase = Event.NONE;
                    break;
                    default:
                        // we should never be able to reach this
                        throw new Error( 'This event encountered an inconsistent internal state' );
                    break do_while_label; // break out of the do...while loop.
                }

            } while( !privateVars.propagationStopped && privateVars.eventPhase !== Event.NONE );

            privateVars.currentTarget = null;
            privateVars.eventPhase = Event.NONE;

            return !privateVars.defaultPrevented;
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'detail': {
                configurable: false,
                enumerable: true,
                get: function() {
                    return privateVars.detail;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();

我不确定在
instanceof
方法之外是否有任何预防机制。我对JS也有同样的抱怨。谢谢你们让我能够回答我自己的问题。这是最好的方式。您将所有事件内容封装在一个闭包中。也许一个更准确的名字会适合你。@Guskrawford谢谢你让Gus放心。事实上,我很喜欢它现在也发生在一个闭包中,但我仍然不能完全确定它是否发生在
事件中。但是,我越来越喜欢它了。:-)不过我不确定我是否理解你的名字。英语不是我的母语,但我认为这个名字的意思对于一个程序员来说是非常合适的。事实上,我只是觉得这听起来很酷。你能详细说明一下吗?“我失踪”这个词是否有内部人士的意思?或者你是在谈论一些完全不同的事情吗?你说你觉得让每件事都成为“事件”的一部分是不舒服的。所以,也许现在的事件不仅仅是一个事件。这就是我的意思;)
codifier.event.EventDispatcher = ( function() {

    function EventDispatcher( target, ancestors ) {

        if( !( this instanceof EventDispatcher ) ) {
            return new EventDispatcher( target, ancestors );
        }

        let privateVars = {
            instance: this,
            target: target === Object( target ) ? target : this,
            ancestors: [],
            eventListeners: {}
        }

        this.clearAncestors = function() {
            privateVars.ancestors = [];
        }

        this.setAncestors = function( ancestors ) {
            this.clearAncestors();
            if( Array.isArray( ancestors ) ) {
                ancestors.forEach( function( ancestor ) {
                    if( ancestor instanceof EventDispatcher ) {
                        privateVars.ancestors.push( ancestor );
                    }
                } );
            }
        }

        this.getAncestors = function() {
            return privateVars.ancestors;
        }

        this.getEventListenersForEvent = function( type, useCapture ) {
            if( !this.hasEventListenersForEvent( type, useCapture ) ) {
                return [];
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            return privateVars.eventListeners[ type ][ eventPhase ].values();
        }

        this.hasEventListenersForEvent = function( type, useCapture ) {
            if( !privateVars.eventListeners.hasOwnProperty( type ) ) {
                return false;
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            if( !privateVars.eventListeners[ type ].hasOwnProperty( eventPhase ) ) {
                return false;
            }

            return privateVars.eventListeners[ type ][ eventPhase ].size > 0;
        }

        this.hasEventListener = function( type, listener, useCapture ) {
            if( !this.hasEventListenersForEvent( type, useCapture ) ) {
                return false;
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            return privateVars.eventListeners[ type ][ eventPhase ].has( listener );
        }

        this.addEventListener = function( type, listener, useCapture ) {
            if( !this.hasEventListener( type, listener, useCapture ) ) {
                if( !privateVars.eventListeners.hasOwnProperty( type ) ) {
                    privateVars.eventListeners[ type ] = {};
                }

                let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
                if( !privateVars.eventListeners[ type ].hasOwnProperty( eventPhase ) ) {
                    privateVars.eventListeners[ type ][ eventPhase ] = new Map();
                }
                privateVars.eventListeners[ type ][ eventPhase ].set( listener, listener );
            }
        }

        this.removeEventListener = function( type, listener, useCapture ) {
            if( this.hasEventListener( type, listener, useCapture ) ) {
                let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
                privateVars.eventListeners[ type ][ eventPhase ].delete( listener );
            }
        }

        this.dispatchEvent = function( event ) {
            if( event instanceof codifier.event.Event ) {
                return event.dispatch( privateVars.instance );
            }

            return false;
        }

        this.clear = function() {
            Object.getOwnPropertyNames( privateVars.eventListeners ).forEach( function( type ) {
                Object.getOwnPropertyNames( privateVars.eventListeners[ type ] ).forEach( function( eventPhase ) {
                    privateVars.eventListeners[ type ][ eventPhase ].clear();
                    delete privateVars.eventListeners[ type ][ eventPhase ];
                } );
                delete privateVars.eventListeners[ type ];
            } );
        }

        Object.defineProperties( this, {
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                }
            }
        } );

        Object.freeze( this );

        this.setAncestors( ancestors );
    }

    Object.freeze( EventDispatcher );
    Object.freeze( EventDispatcher.prototype );

    return EventDispatcher;

} )();