JavaScript
ECMAScript
ECMAScript6
ECMAScript2015

JavaScript: ボタンをすべて押したらつぎに進む ー ECMAScriptの新しめの構文を織り交ぜて

今回のお題は、ボタンをすべて押したらつぎの処理に進む、というものです。組み立て方はいろいろ考えられましょう。ここで試すのは、配列とビット演算、それに Promiseオブジェクトを用いたやり方です。そのとき、ECMAScript 2015(ES6)以降の新しめの構文も織り交ぜてゆきます。

querySelectorAll()でボタンのリストを得る

<button>要素は3つ、つぎのようにid属性を定めた<div>要素に入れます。

<div id="buttons">
    <button type="button">button 1</button>
    <button type="button">button 2</button>
    <button type="button">button 3</button>
</div>

3つの<button>要素は、querySelectorAll()メソッドで得られます。戻り値は、NodeListオブジェクトです。配列のように角かっこ[]にインデックスを与えた構文で要素が取り出せます。また、要素数を調べるのはNodeList.lengthプロパティです。

Living StandardにはNodeList.prototype.forEach()メソッドが備わっています。このメソッドで3つのボタンにクリックイベントのリスナー関数を定めるのが次のコードです。ボタンを押すと、コンソールにそれぞれのインデックス番号が示されます。

function init() {
    const buttons = [...document.querySelectorAll('#buttons button')];
    buttons.forEach((button, id) => {
        button.addEventListener('click', (event) => {
            console.log(id);
        });
    });
}
document.addEventListener('DOMContentLoaded', init);

もっとも、NodeListオブジェクトはArrayクラスと同じメソッドを備えているわけではありません。たとえば、ECMAScript 5.1のArray.every()メソッドが使いたいとき、これまでの構文ではFunction.call()メソッドを呼び出さなければなりませんでした。

const buttons = document.querySelectorAll('#buttons button');
Array.prototype.every.call(buttons, function(button, id) {
    // 要素ごとの処理
});

ArrayのようなオブジェクトにArray.every()メソッドを使う

3つのボタンがすべて押されたら処理をするお題に進みましょう。そのとき使いたいのがArray.every()メソッドです。引数のコールバック関数がすべてtrueを返したときのみ、メソッドはtrueを返します(他の場合はfalse)。

ECMAScript 2015(ES6)に備わったArray.from()メソッドを使えば、Arrayのような(array-like)オブジェクトをArrayに変えられます。さらにお手軽なのがスプレッド構文です。配列をカンマ区切りの引数として扱えるので、Arrayリテラルの角かっこ[]に入れれば新たな配列がつくれます。

ボタンがクリックされたら、リスナー関数で自分のオブジェクトのプロパティ(clicked)に値trueを与えることにしましょう。すると、ボタンの配列(buttons)に対してArray.every()メソッドを呼び出して、すべてクリックされたかどうか調べられます。

function init() {
    const buttons = [...document.querySelectorAll('#buttons button')];
    buttons.forEach((button, id) => {
        button.addEventListener('click', (event) => {
            button.clicked = true;
            const allDone = buttons.every((button) => button.clicked);
            if (allDone) {
                console.log('completed');
            }
        });
    });
}

ビット演算でフラグを扱う

ボタンを押したかどうかは2値です。これを0/1で扱うことにすると、ビット演算が使えます(「2進数・16進数とビット演算」参照)。はじめの値は2進数0b000として、各桁をボタンに割り振るのです(2進数数値構文はECMAScript 2015で採り入れられました)。クリックされたらその桁の値を1にすれば、0b111つまり10進数7になったとき、ボタンがすべて押されたことになります。

ビット演算を用いると、前項のコードはつぎのように書き替えられます。実は、forEach()メソッドは、ECMAScript 2015では配列のようなオブジェクトにも備わっているのです。また、べき乗演算子**はECMAScript 2016で加わりました。なお、ビット演算の結果がわかるように、押したボタンと3つのボタンの2進数の値をコンソールに示しています。

function init() {
    // const buttons = [...document.querySelectorAll('#buttons button')];
    const buttons = document.querySelectorAll('#buttons button');
    let allDone = 0;
    buttons.forEach((button, id) => {
        // button.id = id;
        button.addEventListener('click', (event) => {
            // button.clicked = true;
            // const allDone = buttons.every((button) => button.clicked;
            allDone |= 1 << id;  // button.id;
            console.log('button', (1 << id).toString(2));  // 押したボタンの値
            console.log('allDone', allDone.toString(2));  // 3つのボタンの値
            if (allDone >= 2 ** buttons.length - 1) {
                console.log('completed');
            }
        });
    });
}

jsdo.itにサンプルを掲げました。

サンプル

three_buttons_.png
>> judo.itへ

Promise.all()メソッドを使う

ECMAScript 2015に備わったPromiseは非同期の処理を扱うためのオブジェクトです。処理が成功したか、失敗したかによるコールバックがそれぞれ定められます。Promiseオブジェクトの基本的な使い方については「ES6: Promiseオブジェクトを使う」をお読みください。

clickイベントのリスナー関数が定められたボタンひとつをPromiseで扱うコードはつぎのとおりです。

const button = document.querySelector('#buttons button');
const buttonPromise = new Promise((resolve) =>
    button.addEventListener('click', () => resolve())
);
buttonPromise.then(() => console.log('clicked'));

さらに、Promise.all()メソッドを使えば、複数のPromiseがすべて完了したとき、Promise.prototype.then()メソッドを呼び出せます。つまり、3つのボタンのPromiseオブジェクトをこのメソッドに渡せば、すべてのボタンが押されたときの処理が定められます。

function init() {
    const buttonPromises = [...document.querySelectorAll('#buttons button')]
    .map((button) =>
        new Promise((resolve) =>
            button.addEventListener('click', () => resolve())
        )
    );
    Promise.all(buttonPromises)
    .then(() => {
        console.log('completed');
    });
}

なお、3つのボタンを押せばPromiseがすべて完了しますので、それ以降のクリックはコールバックを呼び出しません。

[追記: 2018年10月1日] culageさんのコメントにもとづいて「Promise.all()メソッドを使う」を加えました。