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

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


# querySelectorAll()でボタンのリストを得る
`<button>`要素は3つ、つぎのように`id`属性を定めた`<div>`要素に入れます。

```html
<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()`メソッド](https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll)で得られます。戻り値は、[`NodeList`オブジェクト](https://developer.mozilla.org/docs/Web/API/NodeList)です。配列のように角かっこ`[]`にインデックスを与えた構文で要素が取り出せます。また、要素数を調べるのは[`NodeList.length`プロパティ](https://developer.mozilla.org/docs/Web/API/NodeList/length)です。

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

```js
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()`メソッド](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every)が使いたいとき、これまでの構文では[`Function.call()`メソッド](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/call)を呼び出さなければなりませんでした。

```js
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()`メソッド](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/from)を使えば、`Array`のような(array-like)オブジェクトを`Array`に変えられます。さらにお手軽なのが[スプレッド構文](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax)です。配列をカンマ区切りの引数として扱えるので、`Array`リテラルの角かっこ`[]`に入れれば新たな配列がつくれます。

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

```js
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進数とビット演算](http://www.fumiononaka.com/Business/html5/FN1509001.html)」参照)。はじめの値は2進数`00000`として、各桁をボタンに割り振るのです([2進数数値構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Lexical_grammar#Numeric_リテラル)はECMAScript 2015で採り入れられました)。クリックされたらその桁の値を`1`にすれば、`0b111`つまり10進数`7`になったとき、ボタンがすべて押されたことになります。

ビット演算を用いると、前項のコードはつぎのように書き替えられます。実は、[`forEach()`メソッド](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/forEach)は、ECMAScript 2015では配列のようなオブジェクトにも備わっているのです。また、[べき乗演算子`**`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#べき乗_(**))はECMAScript 2016で加わりました。なお、ビット演算の結果がわかるように、押したボタンと3つのボタンの2進数の値をコンソールに示しています。

```js
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](https://qiita-image-store.s3.amazonaws.com/0/128949/bb04ad02-43ca-6077-9ee5-6693f163e917.png)
\>> jsdo.itへ](http://jsdo.it/FumioNonaka/WnWh)


# Promise.all()メソッドを使う
ECMAScript 2015に備わったPromiseは非同期の処理を扱うためのオブジェクトです。処理が成功したか、失敗したかによるコールバックがそれぞれ定められます。Promiseオブジェクトの基本的な使い方については「[ES6: Promiseオブジェクトを使う](http://fumiononaka.com/Business/html5/FN1710003.html)」をお読みください。

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

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

さらに、[`Promise.all()`メソッド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)を使えば、複数の`Promise`がすべて完了したとき、`Promise.prototype.then()`メソッドを呼び出せます。つまり、3つのボタンの`Promise`オブジェクトをこのメソッドに渡せば、すべてのボタンが押されたときの処理が定められます。

```js
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さん](https://qiita.com/culage)のコメントにもとづいて「Promise.all()メソッドを使う」を加えました。