どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ

UI開発者 宇賀

みなさんこんにちは!UI開発者の宇賀です。

今回は、先日掲題のようなお便りをいただきましたので記事にしてみました。

社内外問わずjQueryからJavaScriptの世界へ入ってきた方々の中にも、脱jQueryとまでは言わなくともVanilla(純粋なJavaScript)でコードを書く練習をしているデベロッパが多くなってきたと思います。

今でも幅広く世の中で利用されているjQueryが持つメリットの中でも、特に高い恩恵をもたらす仕様の1つに「暗黙的にforEachのようなループ処理が実行されること」があげられると私は考えているのですが、同時にこの「暗黙的に」という部分が厄介な点でもあります。

これはVanilla入門者の壁になりがちです。今回は「イベントハンドラの登録」にフォーカスしてjQueryとの違いを見ていきましょう。

「on」と「addEventListener」の使い方

jQueryでイベントハンドラを登録するためには「on」というメソッドを利用しますが、Vanillaでは「addEventListener」というメソッドを利用します。

まずjQueryで、全てのbutton要素に「クリックされた時にアラートダイアログを表示する機能」を実装する例を見てみましょう。

var handler = function () {
    window.alert('クリックされました');
};
 
$('button').on('click', handler);

Vanillaでは次のように書くことで同様の機能が実現できます。

var handler = function () {
    window.alert('クリックされました');
};
 
document.querySelectorAll('button').forEach(function (button) {
    button.addEventListener('click', handler);
});

要素を取得してからイベントハンドラを登録する間に「forEach」というメソッドを実行しています。

jQueryでは要素を取得したすぐあとに「on」を利用してイベントハンドラを登録できたのに、なぜ同じ要素を取得する機能である「document.querySelectorAll」ではそれができないのでしょうか?

ヒント:オブジェクトの種類によって持っている機能は異なる

次の例を見てみましょう。

var hoge = [
    0,
    1,
    2
];
 
hoge.forEach(function (item) {
    console.log(item);
});
var hoge = {
    a: 0,
    b: 1,
    c: 2
};
 
hoge.forEach(function (item) { // > Uncaught TypeError
    console.log(item);
});

2つのコードうち、後者は「hoge.forEach is not a function」と言われて実行できないはずです。

これは「forEach」メソッドのピリオドの直前にある「hoge」に何が入っているかの違いがポイントです。前者の変数hogeにはArrayオブジェクト(配列)が入っていますが、後者にはObjectオブジェクト(連想配列)が入っています。

Arrayオブジェクトは「forEach」を持っていますが、Objectオブジェクトは「forEach」を持っていないため実行することができません。

このように、オブジェクトの種類によって持ち合わせている機能は異なるという点が今回のポイントです。

では「addEventListener」を持っているのは誰か

結論から申し上げますと、「addEventListener」を持っているのは「要素そのもの」です。JavaScriptでは「Elementオブジェクト」と呼ばれます。

それを踏まえて、一度jQueryの「on」メソッドを見てみましょう。

この「on」メソッドは、jQueryオブジェクトが持つ機能です。jQueryオブジェクトとは「$()」の実行結果で返されるObjectオブジェクトのようなものであり、Arrayオブジェクトのように連番で要素を管理しています。

var $target = $('div'); // ページ内にdiv要素は3つの時に実行したとする

/*
    $targetの中身を簡略化したイメージ

    {
        0: div要素,
        1: div要素,
        2: div要素,
        length: 3, // 取れた要素の数
        on: function (eventType, handler) {
            var i = 0;
            var max = this.length; // 取れた要素の数

            for (i; i < max; i++) {
                // このオブジェクトで管理している要素1つ1つにイベントハンドラを登録する
                this[i].addEventListener(eventType, handler);
            }
        },

        ... // その他のメソッドやプロパティ
    };
*/

上記のコードはあくまでもイメージですが、実際のjQueryでも「on」メソッドは内部的にjQueryオブジェクトで管理している要素1つ1つに「addEventListener」を実行しています。

ところが、「document.querySelectorAll」で取得できる値は「NodeListオブジェクト」であり、このNodeListオブジェクトは「addEventListener」を持ちません。持っているのはArrayオブジェクトのように連番管理された0個以上のElementオブジェクト(取得できた要素)と、いくつかのNodeListオブジェクト用メソッドやプロパティです。

以上のような理由から、「document.querySelectorAll(selector).addEventListener()」は実行できないのでした。

var targetList = document.querySelectorAll('div'); // ページ内にdiv要素は3つの時に実行したとする
var handler = function () {
    window.alert('クリックされました');
};

/*
    targetListの中身を簡略化したイメージ

    {
        0: div要素, // div要素はaddEventListenerを持っている
        1: div要素, // div要素はaddEventListenerを持っている
        2: div要素, // div要素はaddEventListenerを持っている
        length: 3, // 取れた要素の数

        ... // その他のメソッドやプロパティ
    };
*/

// targetListの中にある要素の数だけループする
targetList.forEach(function (target) {
    // 引数targetにはdiv要素が1つずつ渡されている
    target.addEventListener('click', handler);
});

まとめ

「document.querySelectorAll」で取得した要素群にイベントハンドラを登録するには、NodeListオブジェクトの中に格納されている要素1つ1つに「addEventListener」を実行する必要があり、そのためにはループ文や「forEach」などを利用します。

こうしてみるVanillaは複数のイベントタイプ(clickやkeydown、DOMContentLoadedなど)に対応するためには、イベントタイプの数×要素の数だけ「addEventListener」を実行する必要があり、名前空間を利用するためにはそのための仕組みを自分で用意しなければならないなど、jQueryに比べると不便な印象を受けるかもしれません。

しかしJavaScriptに限らず、Vanillaの状態で1つの機能が行う処理は可能な限りシンプルであるべきですから、ただ不便というわけでもありません。元々の機能がシンプルだからこそ様々なフレームワークやライブラリが誕生していく可能性も広がっていくのだと思います。(とはいえ「NodeList.prototype.addEventListenerAll」みたいなものが実装される日が確実に来ないと言い切れるわけでもありません。)

巷ではVue.jsが人気ですが、自分が知らないライブラリやフレームワークを触ることになったり、環境によってVanillaを余儀なくされたりした場合にも対応できるよう、自分が利用するライブラリがどのようなロジックで備わっている機能を実現しているのかを考えながら触れてみるのも楽しいかもしれないですね。

今回も記事が面白かった!と思っていただけましたら、TwitterやFacebookでぜひぜひシェアしてください!

次回もお楽しみに!