ループについて ~whileとforと参照と~

2014.01.20kickbaseJavaScript


先日非常に興味深い発見があったのでポスト。いや、よくよく考えれば非常にシンプルな話なんですが、ハマったら中々気づきにくい穴だと思うので記事にしました。

事の発端は「任意の文字列を渡したら、逆から読んだ文字列を返す関数を実装する」というもの。一番最初に考えたのがこれ。

var original = 'abcdef';

function reverse(input) {
    var str = '';
    var arr = input.split("");
    for (var i = 0; i < arr.length; i++) {
        str += arr.pop();
    }
    return str;
}

console.log(reverse(original)); //fed

実装としては簡単なもので、

1. 文字列をoriginalとして宣言する
2. 引数にoriginalを渡し、内部で一文字づつ分解したものを配列として保持する
3. ローカル変数のstrに、配列のメンバを後ろから抜き出して足していく(Array.popメソッド)
4. ループが終わったらstrを返す

おかしい。出力結果が「fed」と、半分の文字列になっている…

ここでfor文のまま考えてればいずれ答えにたどり着いたのでしょうが、while文でトライしたのが底なし沼の始まり。

var original = 'abcdef';

function reverse(input) {
    var str = '';
    var arr = input.split("");
    while (arr.length) {
        str += arr.pop();
    }
    return str;
}
console.log(reverse(original)); //fedcba

なん…だと… whileでは問題なく出力されている…

散々悩んだあげく、エンジニアの友人に答えを教えてもらいました。

var original = 'abcdef';

function reverse(input) {
    var str = '';
    var arr = input.split("");
    for (var i = 0; i < arr.length; i++) {
        console.log(i,arr.length); //ここのi,arr.lengthを見ると答えがわかる。
        str += arr.pop();
    }
    return str;
}

console.log(reverse(original)); //fed

出力結果は下記の通り

0 6 
1 5 
2 4 

つまり、iがひとつ増えたと同時にarr.lengthもひとつ減るため、2倍の速度でカウントされ、for文を抜けてしまっていたのです!
よって、for文の外でarr.length(の大元の値)を参照しておき、それと比較すると期待通りに動作します。

var original = 'abcdef';

function reverse(input) {
    var str = '';
    var arr = input.split("");
    var l = arr.length; //for文の外で定義
    for (var i = 0; i < l; i++) {
        // console.log(i,arr.length);
        str += arr.pop();
    }
    return str;
}
console.log(reverse(original)); //fedcba

for文の条件式において、リミット(今回の例ではarr.length)を参照にするとその分アクセスが走り処理が重くなるため、外側で宣言するのはよくやることですが、forスコープ内で配列を操作する場合は余計注意が必要ですね。

[ 追記 ]

String.charAtメソッドを使う方法もあるとアドバイス頂いたので、記載しておきます。
「ループ内で直接配列を触らない」という考え方は大切ですね。

var original = 'abcdef';

function reverse(input) {
    var str = '';
    for (var i = input.length - 1; i >= 0; i--) {
        str += input.charAt(i);
    }
    return str;
}

console.log(reverse(original));

上記を全部まとめたものは下記の通り

(function () {
    var original = 'abcdef';

    function reverseWhile(input) {
        var str = '';
        var arr = input.split("");
        while (arr.length) {
            // console.log(arr.length);
            str += arr.pop();
        }
        return str;
    }
    console.log('while : ' + reverseWhile(original)); //fedcba

    function reverseForA(input) {
        var str = '';
        var arr = input.split("");
        for (var i = 0; i < arr.length; i++) {
            // console.log(i,arr.length);
            str += arr.pop();
        }
        return str;
    }
    console.log('for A : ' + reverseForA(original)); //fed

    function reverseForB(input) {
        var str = '';
        var arr = input.split("");
        var l = arr.length; //for文の外で定義
        for (var i = 0; i < l; i++) {
            // console.log(i,arr.length);
            str += arr.pop();
        }
        return str;
    }
    console.log('for B : ' + reverseForB(original)); //fedcba

    function reverseCharAt(input) {
        var str = '';
        for (var i = input.length - 1; i >= 0; i--) {
            str += input.charAt(i);
        }
        return str;
    }
    console.log('charAt : '+reverseCharAt(original));
})();

「forとwhileは同じもの」という思い込みが、中々真実を見つけにくくしている問題でした。種を明かせば簡単な事ですが、みなさんも参照にはお気をつけください。


ページトップへ