on = function (events, fn) {
        var eventsList = events.split(/\s+/).map(splitNs),
            i;

        if (!fn) {
            throw new Error("EventListener.on() called with no listener fn for event '" + events + "'");
        }

        // Check for deprecation warnings
        if (this._deprecatedEvents) {
            for (i = 0; i < eventsList.length; i++) {
                var deprecation = this._deprecatedEvents[eventsList[i].eventName];
                if (deprecation) {
                    var message = "Registering for deprecated event '" + eventsList[i].eventName + "'.";
                    if (typeof deprecation === "string") {
                        message += " Instead, use " + deprecation + ".";
                    }
                    console.warn(message, new Error().stack);
                }
            }
        }

        // Attach listener for each event clause
        for (i = 0; i < eventsList.length; i++) {
            var eventName = eventsList[i].eventName;
            if (!this._eventHandlers) {
                this._eventHandlers = {};
            }
            if (!this._eventHandlers[eventName]) {
                this._eventHandlers[eventName] = [];
            }
            eventsList[i].handler = fn;
            this._eventHandlers[eventName].push(eventsList[i]);

            // Check for suspicious number of listeners being added to one object-event pair
            if (this._eventHandlers[eventName].length > LEAK_WARNING_THRESHOLD) {
                console.error("Possible memory leak: " + this._eventHandlers[eventName].length + " '" + eventName + "' listeners attached to", this);
            }
        }

        return this;  // for chaining
    }
Adds the given handler function to 'events': a space-separated list of one or more event names, each
with an optional ".namespace" (used by off() - see below). If the handler is already listening to this
event, a duplicate copy is added.
@param {string} events
@param {!function(!{type:string, target:!Object}, ...)} fn