入れ子構造になっている、二重ループの内側から抜け出したい時ってありますよね。
そんな時に使える「ラベル構文」というのをJavaScriptの勉強中に知りましたので、書いておきます。
今回は、12×12のかけ算をしていく中で、100を越えたらループを抜けるというコードを書いてみます。
コード
<!DOCTYPE html>
<html>
<head>
<title>JavaScriptの勉強</title>
</head>
<body onload="proc();">
<p id="test-box"></p>
<script>
function proc(){
var testBox = document.getElementById("test-box");
timesTable : for(var i = 1; i <= 12; i++){
for (var j = 1; j <= 12; j++){
var k = i * j;
if( k > 100){
break timesTable;
}
testBox.innerHTML += k + ',';
}
testBox.innerHTML += '<br>';
}
}
</script>
</body>
</html>
普通にループを抜けようとすると・・・
function proc(){
var testBox = document.getElementById("test-box");
for(var i = 1; i <= 12; i++){
for (var j = 1; j <= 12; j++){
var k = i * j;
if( k > 100){
break;
}
testBox.innerHTML += k + ',';
}
testBox.innerHTML += '<br>';
}
}
こういうふうに書くと、jのループから抜け出すことはできます。
しかし、その外側にあるiのループまで一緒に抜けだしたいときには、この書き方では不十分です。
こんな感じになってしまい、ループが続いていきます。
1,2,3,4,5,6,7,8,9,10,11,12,
2,4,6,8,10,12,14,16,18,20,22,24,
3,6,9,12,15,18,21,24,27,30,33,36,
4,8,12,16,20,24,28,32,36,40,44,48,
5,10,15,20,25,30,35,40,45,50,55,60,
6,12,18,24,30,36,42,48,54,60,66,72,
7,14,21,28,35,42,49,56,63,70,77,84,
8,16,24,32,40,48,56,64,72,80,88,96,
9,18,27,36,45,54,63,72,81,90,99,
10,20,30,40,50,60,70,80,90,100,
11,22,33,44,55,66,77,88,99,
12,24,36,48,60,72,84,96,
ラベル構文をつかったループ
こんな時に使うのがラベル構文というものです。
function proc(){
var testBox = document.getElementById("test-box");
// 文頭にラベルを付ける(ラベル名: )
timesTable : for(var i = 1; i <= 12; i++){
for (var j = 1; j <= 12; j++){
var k = i * j;
if( k > 100){
break timesTable; // どのループを抜け出すか指定する
}
testBox.innerHTML += k + ',';
}
testBox.innerHTML += '<br>';
}
}
最初のfor文の文頭に、"timesTable : "というコードを追加し、breakのところにも"timesTable"を追加しました。
forやwhile,ifなどの頭にこのようなラベルをつけてやることで、どこをbreakするか指定してやることができます。
結果こうなりました。
1,2,3,4,5,6,7,8,9,10,11,12,
2,4,6,8,10,12,14,16,18,20,22,24,
3,6,9,12,15,18,21,24,27,30,33,36,
4,8,12,16,20,24,28,32,36,40,44,48,
5,10,15,20,25,30,35,40,45,50,55,60,
6,12,18,24,30,36,42,48,54,60,66,72,
7,14,21,28,35,42,49,56,63,70,77,84,
8,16,24,32,40,48,56,64,72,80,88,96,
9,18,27,36,45,54,63,72,81,90,99,
100以上の数字が出た瞬間にすべてのループから抜け出せていることがわかりますね。
whileでも試してみましたが、同じようにできていました。
とはいえ、そもそも多重ループを使うのがわかりづらくなるうえ、ラベルでそのループを指定するとなると、なおさらわかりづらくなってしまうと思うので、できればラベルなど使わずに入れ子構造以外での処理を考えたほうが良いかもしれませんね。
ちなみに、英語での九九はtimes tableといって、12×12まで覚えたりするらしいですね。初めて知りました。
追記
2020-07-24 : ループを使わない方法で12×12をする方法
教えていただいたのでご紹介します。
function proc(){
var testBox = document.getElementById("test-box");
const sequence = [...Array(12).keys()].map(i => i + 1);
sequence.some((ov) => {
const step = sequence.map(iv => iv * ov).filter(iv => iv < 100);
testBox.innerHTML += step + ',';
if(step.length < sequence.length){
return true;
}
testBox.innerHTML += '<br>';
});
}
解説
const sequence = [...Array(12).keys()].map(i => i + 1);
わかりやすいところから書くと、Array(12)というのは、要素が12個ある配列を作るということ。
この配列に".keys()"をつけてやることで、配列のインデックス情報(0番目から11番目まで、配列の中での位置情報)を持つイテレータを取得します。
"..."というドット三つをつけてやることでイテレータを展開しています(スプレッド構文)。
ちなみに、この時Array(12)に対して先にスプレッドされるのではなく、keys()が優先されて演算されていますね。
なお、map関数までのところではこのような結果が出ます。
console.log([...Array(12).keys()]);
// [0,1,2,3,4,5,6,7,8,9,10,11]]
スプレッド構文について
最初、「スプレッド構文で展開したのをまた[ ]でくくって配列にしているなら、省略しちゃえば良いんじゃないの?」て思いました。
[...Array(12).keys()].map(i => i + 1);
ではなく
Array(12).keys().map(i => i +1);
ではダメ?
しかし、これはエラーに。
map関数は配列オブジェクトに対して使うものですが、keysで出力されるのがイテレータオブジェクトだからダメだったようです。
あくまでも今回スプレッド構文で展開しているのはイテレータオブジェクト。
配列に変換してあげなければいけなかったから、[ ]でくくってスプレッド構文を使っているんですね。
map関数
次に、map関数について。
これは、配列の一つ一つの要素をいじって配列に入れなおしてくれるというものです。
なので、配列の一要素をiという変数として引数に与え、+1しているわけです。
ということで、以下のようになります。
console.log([...Array(12).keys()]);
// [0,1,2,3,4,5,6,7,8,9,10,11]]
console.log([...Array(12).keys()].map(i => i + 1);
// [1,2,3,4,5,6,7,8,9,10,11,12]]
疑似二次元ループ
sequence.some((ov) => {
const step = sequence.map(iv => iv * ov).filter(iv => iv < 100);
testBox.innerHTML += step + ',';
if(step.length < sequence.length){
return true;
}
testBox.innerHTML += '<br>';
});
sequenceは先ほど抽出した1から12までの配列ですね。
これに対してsome()メソッドを使ってやります。
some()は、trueを返す要素が見つかるまで要素を一つずつ見ていくというもの。
trueが返ってきた時点でループが終わります。
どうやって二次元ループにしているかというと、
sequence.some((ov) => {...} というので、1から12までの数字を一つずつ関数の中身に入れていき、
その関数内で、sequence.map(iv => iv * ov) というのを出し、もう一つの1から12までのループを作っています。
そして、filterメソッドを使って、条件に合うものだけ配列の中身に残るようにし、元の配列の長さと残った配列の長さを見比べて、長さが違っていたらtrueを返してsomeを抜けるという形になっています。
ループの中でconstって良いの?
constって再定義・再代入不可と最近学んだばかり。JavaScriptだと、ループの中で定義した変数は、ループ外でも使えてしまうというらしいし。だったら、ループの中にconstって使ったらループが二回目に来た時に再代入不可になるべきじゃないの?って思いました。
が、letやconstをループの中で使うと、varとは違ってブロックスコープ(ループの中でだけ使える変数)になるそうですね。
うーん、、なんだかもやっとする。
ともかく、ループの外でも使うというのでなければ、むしろconstにするのが良いみたいです。
ループ内変数として使うconstとletの違いってなんだ?
ループ内で定義する分には、constもletも問題なく使えます。
しかし、ループ条件にする場合には違うよう。
for (let i = 0; i < 3; i++) {
const x = i * i;
console.log(x);
}
とすると、問題なくいきます。letは再定義不可、再代入可ですからね。
しかし、以下のようにするとエラーが出ます。
for (const i = 0; i < 3; i++) {
const x = i * i;
console.log(x);
}
constの値は変えられませんよ!って怒られてしまいます。ループ内で使う分にはよくても、ループの条件文で使うとダメなんですね。
え、てことは、ここでつかった変数ってループ外で使える??
for (let i = 0; i < 3; i++) {
const x = i * i;
console.log(x);
}
console.log(i);
ということでやってみましたが、これもエラー。
ループの条件文で定義したものは、ループ内でしか使えないものの、ループ内ではループを回していても保持されるよう。
そりゃそうか、変数情報保持できなかったらiの値を変えていくみたいなことできないですもんね。
結論
- 再代入しないループ内変数はconstが良い。
- 再代入するループ内変数はletが良い。
- ループ条件文に入れる変数はletが良い。
- ループ内で定義した変数はvar以外はループ外で使えない。
ということで、二重ループを抜ける方法と一口に言っても、JavaScriptの奥深さが味わえましたね。
ぜひ他のやり方も教えていただけると!