addEventListener & removeEventListener polyfill


## 概要

本投稿は、teratailの質問「[IE8以下でのaddEventListener、removeEventListenerへの対応について](https://teratail.com/questions/4715)」に対する`addEventListener`と`removeEventListener`のpolyfillコードに関する意見収集が目的である。

## 元のコード

[addEventListener polyfill for IE6+](https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e)

### 特徴、及びコメントの指摘

* 対応しているのは`addEventListener`メソッドのみ
* `Event.target`プロパティと`Event.currentTarget`プロパティが未実装 [^1]
* jQueryで問題が発生する [^2]  
  原因はjQueryが`removeEventListener`メソッドの対応判定に`document.addEventListener`を利用するため。
* 8行目に無駄な`var`宣言が存在する [^3]  
  (直後のコメントに、無駄では無いと書かれている…?)
* 引数`fn`に関数以外の値が指定された場合の挙動が想定されていない [^4]

※著者はChromeの翻訳機能に頼っているため、訳は不正確の可能性あり

## コード

### 1.1

* `listeners_get`,`listeners_set`関数を`listener_get`,`listener_set`に改名
* 変数`listeners`に保持していたリスナーと内部無名関数(リスナーを内包し、`Event`オブジェクトを補完する関数)の参照を、リスナー自身の`x-ms-event-listeners`プロパティに代入するよう変更。  
(JavaScriptでは関数もオブジェクトなため、関数に連想配列のように値を設定できる)

```js:v1.1
/**
 * @license addEventListener polyfill 1.0 / Eirik Backer / MIT Licence
 * https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e
 */
(function (window, document, listeners_prop_name) {
    if ((!window.addEventListener || !window.removeEventListener) && window.attachEvent && window.detachEvent) {
        /**
         * @param {*} value
         * @return {boolean}
         */
        var is_callable = function (value) {
            return typeof value === 'function';
        };
        /**
         * @param {!Window|HTMLDocument|Node} self
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @return {!function(Event)|undefined}
         */
        var listener_get = function (self, listener) {
            var listeners = listener[listeners_prop_name];
            if (listeners) {
                var lis;
                var i = listeners.length;
                while (i--) {
                    lis = listeners[i];
                    if (lis[0] === self) {
                        return lis[1];
                    }
                }
            }
        };
        /**
         * @param {!Window|HTMLDocument|Node} self
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {!function(Event)} callback
         * @return {!function(Event)}
         */
        var listener_set = function (self, listener, callback) {
            var listeners = listener[listeners_prop_name] || (listener[listeners_prop_name] = []);
            return listener_get(self, listener) || (listeners[listeners.length] = [self, callback], callback);
        };
        /**
         * @param {string} methodName
         */
        var docHijack = function (methodName) {
            var old = document[methodName];
            document[methodName] = function (v) {
                return addListen(old(v));
            };
        };
        /**
         * @this {!Window|HTMLDocument|Node}
         * @param {string} type
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {boolean=} useCapture
         */
        var addEvent = function (type, listener, useCapture) {
            if (is_callable(listener)) {
                var self = this;
                self.attachEvent(
                    'on' + type,
                    listener_set(self, listener, function (e) {
                        e = e || window.event;
                        e.preventDefault = e.preventDefault || function () { e.returnValue = false };
                        e.stopPropagation = e.stopPropagation || function () { e.cancelBubble = true };
                        e.target = e.target || e.srcElement || document.documentElement;
                        e.currentTarget = e.currentTarget || self;
                        e.timeStamp = e.timeStamp || (new Date()).getTime();
                        listener.call(self, e);
                    })
                );
            }
        };
        /**
         * @this {!Window|HTMLDocument|Node}
         * @param {string} type
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {boolean=} useCapture
         */
        var removeEvent = function (type, listener, useCapture) {
            if (is_callable(listener)) {
                var self = this;
                var lis = listener_get(self, listener);
                if (lis) {
                    self.detachEvent('on' + type, lis);
                }
            }
        };
        /**
         * @param {!Node|NodeList|Array} obj
         * @return {!Node|NodeList|Array}
         */
        var addListen = function (obj) {
            var i = obj.length;
            if (i) {
                while (i--) {
                    obj[i].addEventListener = addEvent;
                    obj[i].removeEventListener = removeEvent;
                }
            } else {
                obj.addEventListener = addEvent;
                obj.removeEventListener = removeEvent;
            }
            return obj;
        };

        addListen([document, window]);
        if ('Element' in window) {
            /**
             * IE8
             */
            var element = window.Element;
            element.prototype.addEventListener = addEvent;
            element.prototype.removeEventListener = removeEvent;
        } else {
            /**
             * IE < 8
             */
            //Make sure we also init at domReady
            document.attachEvent('onreadystatechange', function () { addListen(document.all) });
            docHijack('getElementsByTagName');
            docHijack('getElementById');
            docHijack('createElement');
            addListen(document.all);
        }
    }
})(window, document, 'x-ms-event-listeners');
```

### 1.0

* `removeEventListener`メソッドの対応
* `attachEvent`メソッドおよび`detachEvent`メソッドの対応チェックを追加
* 関数の参照の維持
* 第二引数が関数かどうか判定し、関数の場合のみ実行
* [DOM Level 2 Eventsの仕様](http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Registration-interfaces)に従い、`addEventListener`メソッドが返り値を返さないよう変更
* `Event.target`,`Event.currentTarget`,`Event.timeStamp`プロパティに対応
* Closure Compiler用のJsDocコメントを追加

```js:v1.0
/**
 * @license addEventListener polyfill 1.0 / Eirik Backer / MIT Licence
 * https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e
 */
(function (window, document) {
    if ((!window.addEventListener || !window.removeEventListener) && window.attachEvent && window.detachEvent) {
        /**
         * @type {Array}
         */
        var listeners = [];
        /**
         * @param {*} value
         * @return {boolean}
         */
        var is_callable = function (value) {
            return typeof value === 'function';
        };
        /**
         * @param {!Window|HTMLDocument|Node} self
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @return {!function(Event)|undefined}
         */
        var listeners_get = function (self, listener) {
            var lis;
            var i = listeners.length;
            while (i--) {
                lis = listeners[i];
                if (lis[0] === self && lis[1] === listener) {
                    return lis[2];
                }
            }
        };
        /**
         * @param {!Window|HTMLDocument|Node} self
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {!function(Event)} callback
         * @return {!function(Event)}
         */
        var listeners_set = function (self, listener, callback) {
            return listeners_get(self, listener) || (listeners[listeners.length] = [
                self,
                listener,
                callback
            ], callback);
        };
        /**
         * @param {string} methodName
         */
        var docHijack = function (methodName) {
            var old = document[methodName];
            document[methodName] = function (v) {
                return addListen(old(v));
            };
        };
        /**
         * @this {!Window|HTMLDocument|Node}
         * @param {string} type
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {boolean=} useCapture
         */
        var addEvent = function (type, listener, useCapture) {
            if (is_callable(listener)) {
                var self = this;
                self.attachEvent(
                    'on' + type,
                    listeners_set(self, listener, function (e) {
                        e = e || window.event;
                        e.preventDefault = e.preventDefault || function () { e.returnValue = false };
                        e.stopPropagation = e.stopPropagation || function () { e.cancelBubble = true };
                        e.target = e.target || e.srcElement || document.documentElement;
                        e.currentTarget = e.currentTarget || self;
                        e.timeStamp = e.timeStamp || (new Date()).getTime();
                        listener.call(self, e);
                    })
                );
            }
        };
        /**
         * @this {!Window|HTMLDocument|Node}
         * @param {string} type
         * @param {EventListener|function(!Event):(boolean|undefined)} listener
         * @param {boolean=} useCapture
         */
        var removeEvent = function (type, listener, useCapture) {
            if (is_callable(listener)) {
                var self = this;
                var lis = listeners_get(self, listener);
                if (lis) {
                    self.detachEvent('on' + type, lis);
                }
            }
        };
        /**
         * @param {!Node|NodeList|Array} obj
         * @return {!Node|NodeList|Array}
         */
        var addListen = function (obj) {
            var i = obj.length;
            if (i) {
                while (i--) {
                    obj[i].addEventListener = addEvent;
                    obj[i].removeEventListener = removeEvent;
                }
            } else {
                obj.addEventListener = addEvent;
                obj.removeEventListener = removeEvent;
            }
            return obj;
        };

        addListen([document, window]);
        if ('Element' in window) {
            /**
             * IE8
             */
            var element = window.Element;
            element.prototype.addEventListener = addEvent;
            element.prototype.removeEventListener = removeEvent;
        } else {
            /**
             * IE < 8
             */
            //Make sure we also init at domReady
            document.attachEvent('onreadystatechange', function () { addListen(document.all) });
            docHijack('getElementsByTagName');
            docHijack('getElementById');
            docHijack('createElement');
            addListen(document.all);
        }
    }
})(window, document);
```

## 脚注

[^1]: [addEventListener polyfill for IE6+](https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e#comment-342256)
[^2]: [addEventListener polyfill for IE6+](https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e#comment-1003920)
[^3]: [addEventListener polyfill for IE6+](https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e#comment-1180112)
[^4]: [addEventListener polyfill for IE6+](https://gist.github.com/2864711/946225eb3822c203e8d6218095d888aac5e1748e#comment-1260342)