アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

JavaScriptでうっかりやってしまいそうなこと色々

こんにちは、中川です。

今回はJavaScriptで開発していると、うっかりハマってしまうちょっとした罠たちを紹介したいと思います。
JavaScriptでの開発経験者であれば、どれか一度はひっかかったことがあるのではないでしょうか?


String



●String#replace()は文字列指定では全部置き換えない



対象文字列を一括して置き換えたいなどでString#replace()を使いますが、
検索対象を文字列で指定してしまうと最初に一致した部分しか置換しません。



'aaaaa'.replace('a', 'b') // => baaaa


全体を置換する場合、正規表現でgオプションで指定しましょう。



'aaaaa'.replace(/a/g, 'b') // => bbbbb



Number



●parseInt()は基数を指定しないと危険



数字文字列を整数に変換してくれるparseInt()ですが、第二引数は必ず指定しましょう。
指定しない場合、先頭が0の場合に思わぬ値となります。



parseInt('011')  // => 9
parseInt('011', 10)  // => 11


先頭が0の場合8進数として認識されます。
なお、IE10やSafariでは「11」となり、10進数として変換されました。


●+での加算時は型に注意



PHPでは文字列連結は「.」、数値の加算は「+」を使うのでこのようなことはないと思いますが、
JSでは文字列連結も数値の加算も「+」を使います。
数値として加算したいのに思わぬ値になることがあるので注意です。



var num = '1';
num + 2            // => '12'

parseInt(num, 10) + 2  // => 3
+num + 2           // => 3



●nullとundefinedで計算



どっちもNaNでしょ。と思い込んでました…。
nullは0となり、undefinedはNaNとして扱われるようです。



var val1 = null;
val1 + 1 // => 1

var val2 = undefined;
val2 + 1 // => NaN



Array



●Array#sort()は文字列比較による辞書順でソートする





var list = [3, 2, 1, 10, 100];
list.sort();


これ「1, 2, 3, 10, 100」となると勘違いしてしまうのではないでしょうか?
結果は「1, 10, 100, 2, 3」となります。
Array#sortのデフォルトのソート順は「文字列比較による辞書順」と定義されておりこのような順番になります。

数値順でソートしたい等、比較順を変更したい場合は、第二引数に比較関数を指定すれば可能です。



list.sort(function(a, b) { return a - b;});
// => [1, 2, 3, 10, 100]



●Array#length はキーに数字以外の文字列では増えない



PHPの配列に慣れていると勘違いしがちですが、
JavaScriptの配列はキーに文字列を指定して要素を追加しても、
lengthには影響を与えません。
ただし、数字の文字列(ややこしいですが…)では、lengthが増えます。



var list = [0, 1, 2];
list[3] = 3;
list.length; // => 4 (+1 増えた)

list['4'] = 4;
list.length; // => 5 (+1 増えた)

list['key'] = 5;
list.length; // => 5 (±0 変化なし)



●Array#lengthはキーに数字を指定したら変化する



配列のキーに数字を指定して要素を代入した場合、配列のlengthが変化することがあります。
何を言っているんだみたいな説明ですので、挙動を見ていただいたほうが早いですね。



var list = [0, 1];
list.length // => 2
list[100] = 100;

list.length // => 101

list[100]   // => 100
list[50]    // => undefined


このようにキーに指定した数字未満の要素数の場合、途中の値が存在しない配列となります。


●Array#lengthはIE8では末尾カンマで増える



うっかりしてると以下のようにやってしまいがちですね。
IE8で影響がでます。IE8とか開発中は気づきませんよね…。
末尾のカンマに注意しましょうということです。



var list = [
  "aaa",
  "bbb",
];
list.length // =>通常は「2」。 IE8は「3」となる。


PHPだと最後もカンマ付ける習慣がありますよね?JS/PHPの同時開発中だと特に注意ですね。


Date



●Date#getMonth()は「月 - 1」の値を返す



Date#getMonth()が返す値は「月-1」の値の為、0〜11となります。
「11月」なら、「10」となります。



var date = new Date('Nov 01 2012');
date.getMonth();  // => 10


Date#setMonth()や、new Date()の月指定も同様のルールです。


●Date#getYear()は『1900年からの差分を返す』



西暦の4桁の年数(例:2012) を取得しようとして、Date#getYear()を使うと間違いですね。
Date#getYear()は『1900年からの差分を返す』となっています。



var date = new Date(2012, 0, 1); // 2012年1月1日
date.getYear(); // => 112


なお、IE8で試したところ、西暦4桁を返しました。
環境により値が違うようですので使い場合は注意が必要です。

西暦を取得したい場合は、Date#getFullYear()が用意されています。



var date = new Date(2012, 0, 1); // 2012年1月1日
date.getFullYear(); // => 2012


参考:http://kawara-tan.blogspot.jp/2009/10/javascript-getyeargetfullyear.html


●Date.parse()は『IETF 標準日付構文』を受け付ける



Date.parse()関数は、日時文字列を解析して、地方時の1970 年 1 月 1 日 00:00:00 からのミリ秒数に変換してくれますが、
解析できる形式がちょっと日本人には馴染みが薄いものだったりします。
new Date('日時文字列')も同様の動作ですね。



Date.parse('Thu, 1 Nov 2012 01:23:45 GMT+0900');
// => 1351700625000


データベースでよく見る「YYYY-MM-DD HH:II:SS」形式は、
たいていのブラウザではNaNとなって解析してくれませんが、
ChromeやNode.jsでは正常に動作しました。



Date.parse('2012-11-01 00:00:00'); 
//=> NaN。環境によっては正常に値を返す


また、「YYYY-MM-DD」形式だと解析してくれる環境がありますが、
時差の分、思った結果とずれたりNaNとなる環境もあるみたいなので注意が必要です。



new Date('2012-11-01')
// => Thu Nov 01 2012 09:00:00 GMT+0900 (JST)


参考: http://d.hatena.ne.jp/naoyes/20101107/1289105967


Boolean



●ifの判定条件



ifや三項演算子などでのtrue/falseの判定条件もハマりやすいところですね。



var res = null ? true : false
// => false
var res = undefined ? true : false
// => false
var res = '' ? true : false
// => false
var res = 0 ? true : false
// => false

var res = {} ? true : false
// => true
var res = '0' ? true : false
// => true
var res = [] ? true : false
// => true


最後の2個のtrueとなる場合は、PHPだとfalseになるので、勘違いしやすいですよね。


Function



●条件付きで関数を定義する



条件付きで関数を定義する時には注意が必要です。



if (true) {
  function foo() {
    return 'AAA';
  }  
} else {
  function foo() {
    return 'BBB';
  }  
}

foo() // => 'BBB'


ifの条件がtrueなので、'AAA'となるかと思いがちですが罠です。
'BBB'となります。
firefoxでは'AAA'となります。

無名関数として変数に代入してあげれば想定通りとなります。



var bar;
if (true) {
  bar = function() {
    return 'AAA';
  }
} else {
  bar = function() {
    return 'BBB';
  }
}

bar() // => 'AAA'


関数宣言と関数式の違いと、環境によって動作が違うので注意が必要ですね。

参考:https://developer.mozilla.org/ja/docs/JavaScript/Reference/Functions_and_function_scope


●ループを使った関数定義



DOMのイベントの登録なんかでよく起こりがちな罠ですね。



var funcs = [];                        
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i;
  }; 
}

funcs[0]() // => 3
funcs[1]() // => 3
funcs[2]() // => 3


変数のスコープの勘違いですね。全て最終的なiの値を返してしまいます。

クロージャを使いましょう。



var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function(i) {
    return function() {
      return i;
    }; 
  }(i);

  /*
  // この方法もよく見かけますね。
  (function(i) {
    funcs[i] = function() {
      return i;
    };
  })(i);
  */
}

funcs[0]() // => 0
funcs[1]() // => 1
funcs[2]() // => 2


JSHintを使っていると、ループ内で関数作るなって怒られますので、
関数を返す関数を作ってもいいと思います。




function createfunc(i) {
  return function() {return i;};
}

var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

funcs[0]() // => 0
funcs[1]() // => 1
funcs[2]() // => 2


参考:http://dev.classmethod.jp/series/javascript-closure/


URLエンコード



●encodeURI & encodeURIComponentの違い



encodeURIでは不十分な場合がありますので、encodeURIComponentとの違いもおさえておきましょう。



var url = 'http://example.com/test?page=1 &key=あ';

encodeURI(url)
// => 'http://example.com/test?page=1 &key=%E3%81%82'

encodeURIComponent(url)
// => 'http%3A%2F%2Fexample.com%2Ftest%3Fpage%3D1%26key%3D%E3%81%82'


エンコード対象の文字の詳細は以下のURLで。
参考:https://developer.mozilla.org/ja/docs/JavaScript/Reference/Global_Objects/encodeURI
参考:https://developer.mozilla.org/ja/docs/JavaScript/Reference/Global_Objects/encodeURIComponent


最後に



以上、ついつい思い込みやうっかりでやってしまいがちな罠たちでした。
結構気をつけたつもりですが、この記事の内容自体にうっかりがある可能性もありますので、
気づかれた方はやさしくご指摘いただければと思いますm(_ _)m。

なお、今回は以下のブラウザにて検証を行いました。
IE 10.0
IE 8.0
Chrome 22.0
Firefox 16.0
Safari 6.0
・Mobile Safari 6.0 (iOSシミュレータ)
・PhantomJS 1.7
・Node@v0.8.14(jasmine-node@1.0.26)
※ブラウザの種類による違いだけでなく、バージョンによっても挙動が違ったりしますのでご注意ください。

今回の検証で使ったjasmineによるテストコードは以下になります。



var node = false;
if (typeof window === 'undefined') {
  window = {navigator: {userAgent: ''}};
  node = true;
}

var ie8 = window.navigator.userAgent.match('MSIE 8.0');
var ie10 = window.navigator.userAgent.match('MSIE 10.0');
var firefox = window.navigator.userAgent.match('Firefox');
var chrome = window.navigator.userAgent.match('Chrome');
var phantomjs = window.navigator.userAgent.match('PhantomJS');
var safari = window.navigator.userAgent.match('Safari')  & & !chrome  & & !phantomjs;


describe('String', function() {
  it('replace()は文字列指定では全部置き換えない', function() {
    expect('aaaaa'.replace('a', 'b')).toEqual('baaaa');
    expect('aaaaa'.replace(/a/g, 'b')).toEqual('bbbbb');
  });
});

describe('Number', function() {
  it('parseInt()は基数を指定しないと危険', function() {
    expect(parseInt('011')).toEqual(ie10 || safari ? 11 : 9);
    expect(parseInt('011', 10)).toEqual(11);
  });

  it('+での加算時は型に注意', function() {
    var num = '1';
    expect(num + 2).toEqual('12');
    expect(parseInt(num, 10) + 2).toEqual(3);
    expect(+num + 2).toEqual(3);
  });

  it('nullとundefinedで計算', function() {
    var val1 = null;
    expect(val1 + 1).toEqual(1);

    var val2 = undefined;
    expect(isNaN(val2 + 1)).toBeTruthy();
  });
});

describe('Array', function() {
  it('sort()は文字列比較による辞書順', function() {
    var list = [3, 2, 1, 10, 100];
    list.sort();
    expect(list).toEqual([1, 10, 100, 2, 3]);

    // 数値比較
    list.sort(function(a, b) { return a - b;});
    expect(list).toEqual([1, 2, 3, 10, 100]);
  });

  it('length はキーに数字以外の文字列では増えない', function() {
    var list = [0, 1, 2];
    list[3] = 3;
    expect(list.length).toEqual(4);

    list['4'] = 4;
    expect(list.length).toEqual(5);

    list['key'] = 5;
    expect(list.length).toEqual(5, 'length変化なし');
  });

  it('lengthはキーに数字を指定したら変化する', function() {
    var list = [0, 1];
    list[100] = 100;
    expect(list.length).toEqual(101);
    expect(list[100]).toEqual(100);
    expect(list[50]).toBeUndefined();
  });

  it('lengthはIE8では末尾カンマで増える', function() {
    var list = [
      'aaa',
      'bbb',
    ];
    expect(list.length).toEqual(ie8 ? 3 : 2);
  });
});

describe('Date', function() {
  it('getMonth()は『月 - 1』の値を返す', function() {
    var date = new Date('Nov 01 2012');
    expect(date.getMonth()).toEqual(10);
  });

  it('getYear()は『1900年からの差分を返す』', function() {
    var date = new Date(2012, 0, 1);
    expect(date.getYear()).toEqual(ie8 ? 2012 : 112, 'IE8は西暦を返すみたいね');
    expect(date.getFullYear()).toEqual(2012, 'ちなみに西暦はgetFUllYear');
  });

  it('parse()は『IETF 標準日付構文』を受け付ける', function() {
    var date = new Date(2012, 10, 1, 1, 23, 45);
    expect(Date.parse('Thu, 1 Nov 2012 01:23:45 GMT+0900')).toEqual(date.getTime());

    // データベースでよく見る、この形式でもパースできる環境もある
    expect(isNaN(Date.parse('2012-11-01 00:00:00'))).toEqual(chrome || node ? false : true);
  });
});

describe('Boolean', function() {
  it('ifの判定条件', function() {
    expect(null ? true : false).toBeFalsy();
    expect(undefined ? true : false).toBeFalsy();
    expect('' ? true : false).toBeFalsy();
    expect(0 ? true : false).toBeFalsy();

    expect({} ? true : false).toBeTruthy();
    expect('0' ? true : false).toBeTruthy();
    expect([] ? true : false).toBeTruthy();
  });
});

describe('Function', function() {
  it('条件付きで関数を定義する', function() {
    if (true) {
      function foo() {
        return 'AAA';
      }
    } else {
      function foo() {
        return 'BBB';
      }
    }
    expect(foo()).toEqual(firefox ? 'AAA' : 'BBB');

    var bar;
    if (true) {
      bar = function() {
        return 'AAA';
      }
    } else {
      bar = function() {
        return 'BBB';
      }
    }

    expect(bar()).toEqual('AAA');
  });

  it('ループを使った関数定義', function() {
    var funcs = [];
    for (var i = 0; i < 3; i++) {
      funcs[i] = function() {
        return i;
      };
    }
    expect(funcs[0]()).toEqual(3);
    expect(funcs[1]()).toEqual(3);
    expect(funcs[2]()).toEqual(3);

    var funcs = [];
    for (var i = 0; i < 3; i++) {
      funcs[i] = function(i) {
        return function() {
          return i;
        };
      }(i);
      /*
      // この方法もよく見かけますね。
      (function(i) {
        funcs[i] = function() {
          return i;
        };
      })(i);
      */
    }
    expect(funcs[0]()).toEqual(0);
    expect(funcs[1]()).toEqual(1);
    expect(funcs[2]()).toEqual(2);

    function createfunc(i) {
      return function() {return i;};
    }
    var funcs = [];
    for (var i = 0; i < 3; i++) {
      funcs[i] = createfunc(i);
    }
    expect(funcs[0]()).toEqual(0);
    expect(funcs[1]()).toEqual(1);
    expect(funcs[2]()).toEqual(2);
  });
});

describe('url encode', function() {
  it('encodeURI  & encodeURIComponentの違い', function() {
    var url = 'http://example.com/test?page=1 &key=あ';
    expect(encodeURI(url)).toEqual('http://example.com/test?page=1 &key=%E3%81%82');
    expect(encodeURIComponent(url)).toEqual('http%3A%2F%2Fexample.com%2Ftest%3Fpage%3D1%26key%3D%E3%81%82');
  });
});