16 May 2011

避けなければいけない JavaScript の失敗

しばらくブログを書いていなかったので, 息抜きに "Javascript Mistakes You Must Avoid" という記事を訳してみました.

Ifadey.com

初心者向けの記事かなと思ったんですが今まであまりきにしたことのないトピックもあったので勉強になりました.

Intro

もしあなたが JavaScript 初心者であれば, 生の JavaScript か jQuery などのフレームワークを使うかに関わらず, JavaScript を書く際の失敗は避けたいものです. ここでは私が JavaScript を学んでくる過程で体験したいくつかの失敗について説明します.

イコール演算子

知っているかもしれませんが, JavaScript では2つのオペランドが等しいかどうかを調べる演算子が2種類あります. ひとつは "==" です. これは2つのオペランドを比較しますが, その型までは調べません. たとえば以下の条件式は真になり, ブロックの中が実行されます.

if( 1 == true ) {
    //this code will run
}

ほかにもいろいろな例があります.

1 == "1"        //true
"true" == true  //false
1 == true       //true
"0" == 0        //true
"" == 0         //true
" " == 0        //true
"Str" == false  //false
"Str" == true   //false

JavaScript の "==" 演算子の挙動に詳しくないひとにとっては驚くような例もあるでしょう. "==" 演算子の2つのオペランドは (それがどんなデータ型であるかに関わらず) Number 型に変換されたあとに比較されます. (指摘を受け以下に補足を追記しました)

最初の 1 == "1" という例を考えてみましょう. 左側のオペランドはすでに Number 型なのでなにも起こりません. 右側のオペランドは String 型なので Number へ変換/パースされます. 結果, 右側のオペランドは "1" (String) から 1 (Number) へ変換されます.

2つめの "true" == true という例の結果は false です. なぜなら, String が数字以外の文字を含んでいた場合, それを Number へ変換しようとすると NaN (Not A Number) が返されるからです. NaN はどんなものと比較しても, かならず false を返します.

ある値を Number へ変換した場合どんな値が返されるかを調べるには Number コンストラクタが便利です. 以下は Firebug でテストしてみた結果です.

f:id:cou929_la:20110515230246p:image

さて, ここまでで "===" 演算子の挙動が気になってきた頃だと思います. "=" が三つの演算子はオペランドの値だけではなく型もチェックします. 値が同じでも型が違えば false を返し, 両方が同じならば true になります.

4 === 4         //true
"2" === 2       //false
1 === true       //false
"true" === true //false
[追記] "==" 演算子の型変換について

fflo さんにコメント欄にて指摘をいただきました. 次の内容が誤解を招きやすいため, "==" 演算子の型変換のアルゴリズムについて補足します.

演算子の2つのオペランドは (それがどんなデータ型であるかに関わらず) Number 型に変換されたあとに比較されます

(原文)

Actually every operand (no matter what data type it has) is converted to Number data type before comparison.

これだといかなる時も "==" の被演算子が Number 型に変換されるように読めますが, 実際にはそうではありません. 以下が反例です

// null や undefined は Number() をかけると 0 だが, 0 と比較しても false

null == 0       // false
Number(null)  // 0


// Number(文字列) (数値を含まないような文字列の場合) の結果は NaN であるため
// 文字列同士を比較すると必ず false になってしまうことになる
// (実際はもちろんそうではない)

'foo' == 'foo'    // true
Number('foo')   // NaN
Number('foo') == Number('foo')  // false

実際には次のようなアルゴリズムで比較されます. "x == y" を考えた時,

  1. x と y が同じ型の場合, strict equal ("===" 演算子) と同様の比較を行う
  2. x と y が違う型の場合
    • null と undefined の比較の場合は true
    • number と string の比較の場合, string を Number に変換してから比較
    • 一方が boolean の場合, それを Number に変換してから比較.
Number(true) == 1   // true
    • 一方が object, もう一方が number が string の場合, object をプリミティブ値に変換してから比較
      • JavaScript のビルトインクラスのうち, Data 以外は valueOf() を試みてから toString() で変換する. Data の場合は逆
      • そうでない場合は実装依存
    • 上記に当てはまらないケースは false

詳しくは ECMA-262 の ”11.9.3 The Abstract Equality Comparison Algorithm”サイ本の "4.9.1 Equality and Inequality Operators" などを参照してください.

結論としては, Good Parts でも述べられていますが*1, "==" のややこしい挙動を覚えるのは面倒なので, 常に strict equal ("===") を使うのが良いと思います.

参照型に null を代入

よくある失敗ですが, 多くの js developer は参照型 (object や Array) を使い終わったあとにそこに null を代入しません. この例を見てください.

var arr = [1, 2, 3];

// arr に対して何か操作をする

// arr を使い終わったあと null を代入する
arr = null;

このように null を代入する利点は GC が自動で変数を回収, メモリを開放してくれることです. これはグローバル変数のようなスコープの広い変数ではより重要です. なぜなら, ローカル変数はそのスコープが切れると GC の対象になるからです (Mark and Sweep GC のエンジンでは特に).


参照変数の初期化

複数の参照変数 (object や Array) に対して 1つの文で代入をしてはいけません. この例を見てください.

var arr1 = [1, 2, 3]
  , arr2 = ['a', 'b', 'c'];

//reset both arrays
arr1 = arr2 = [];

//add a single item in arr2 and arr1
arr2.push( 32 );
arr1.push( 10 );

//print both arrays and you will see same result
//OUTPUT: 10, 32
alert( arr1.join() );
alert( arr2.join() );

1, 2 行目で2つの配列が作られています. その後5行目でから配列で一度に初期化されています. この書き方の問題は arr1, arr2 の両方共がこの時点でメモリ上の同じ配列を指していることです. よって片方への変更はそのままもう片方へも影響します.

例では arr2 へ 32, arr1 へ 10 を push したあと, それぞれを join() して出力しています. 結果, 全く同じ出力になります.

var キーワードを忘れてはいけない

JavaScript では変数宣言時 var をつけることも, 逆に付けずに宣言もできます. しかしこれら2つの間には大きな違いがあります. 次の例を考えます.

function createVar() {
var myVar = 'local';
};

alert( myVar ); //output: undefined

このように, var 付きで宣言された変数は, そのスコープの外からはあくせすできません. もし var なしで宣言した場合,

function createVar() {
myVar = 'local';
};

alert( myVar ); //output: local

変数はグローバルスコープからアクセスできるようになります. 言い換えると var をつけると変数をローカルにすることができます. よって変数の扱いには十分に気をつけてください. 常に var を付けて変数宣言をしてください.

Event delegation

JavaScript でイベントハンドラを扱うのは簡単です. 次のコードは "myLink" という id 属性を持つアンカータグに click ハンドラを付加する例です.

document.getElementById('myLink').addEventListener( 'click', function() {
   //you code goes here...
}, false );

ここで, 以下の html のすべての td 要素にクリックハンドラをつけることを考えます. いちいちすべての td にイベントをつけていきますか?

"myTable">
   
1, 1 1, 2
2, 1 2, 2

このような時に役に立つのが event delegate です. 今回のケースではひとつのクリックイベントハンドラを myTable に付け, そのなかで td がクリックされたかどうかをチェックします. こうすればすべての td 要素にイベントを付ける必要はありません. このようなハンドラは event delegate と呼ばれます. 次がコード例です.

document.getElementById( 'myTable' ).addEventListener( 'click', function( e ) {
      if( e.target && e.target.nodeName == 'TD' ) {
         console.log( e.target.innerHTML );

         //to access id
         //console.log( e.target.id );

         //to access className
         //console.log( e.target.className );
      }
   }, false );

innerText vs innerHTML

新しい js 開発者は innerHTML と innerText を混同しがちです. 両方 element object とともに使うものです. innerHTML は要素の中の html, innerText は要素の中のテキストにアクセスできます.

このような html を考えます.

<div id="myDiv">
     This text is in Div.
     <p>A para in div element.p>
div>

innerHTML では,

document.getElementById('myDiv').innerHTML;

以下のように, html タグ (この場合は p タグ) を含めて出力されます.

This text is in DIV.

A para in div element.

innerText の場合は,

document.getElementById('myDiv').innerText;

html タグを除き, 中のテキストだけを取得します.

This text is in DIV. A para in div element.

大量のノード追加

JavaScript ではノードのリストを DOM のある要素へ追加するような処理がよくあります. 例えば ajax を用いてサーバから名前のリストを受け取り, それを ul のリストとしてドキュメントに追加するような場合です. コードでは次のようにします.

window.onload = function() {
//ul element - 
    var list = document.getElementById( 'list' ); var item = null; // この json はサーバから ajax で取得したと仮定 var ajaxResponse = [ { 'name' : 'Haiku' }, { 'name' : 'Linux' }, { 'name' : 'OS X' }, { 'name' : 'Windows' } ]; // 取得したすべての name を list に追加 for( var i in ajaxResponse ) { item = document.createElement( 'li' ); item.appendChild( document.createTextNode( ajaxResponse[ i ].name ) ); list.appendChild( item ); } } //end onload /* ..:: OUTPUT ::..
    • Haiku
    • Linux
    • OS X
    • Windows
    */

    この例で問題なのは, "for in" ループの毎回 DOM への追加を行っている点です. DOM 操作は重い処理なのでパフォーマンスが劣化します.

    DocumentFragment を使って同様のことを実現できます. DocumentFragment はドキュメントの軽量版で web ページのどこにも表示されないものです. 以下に DocumentFragment を使った例を示します.

    window.onload = function() {
        // DocumentFragment を作成
        var documentFragment = document.createDocumentFragment();
    
        var list = document.getElementById( 'list' ); //
      var item = null; // この json はサーバから ajax で取得したと仮定 var ajaxResponse = [ { 'name' : 'Haiku' }, { 'name' : 'Linux' }, { 'name' : 'OS X' }, { 'name' : 'Windows' } ]; // すべての names を documentFragment に追加 for( var i in ajaxResponse ) { item = document.createElement( 'li' ); item.appendChild( document.createTextNode( ajaxResponse[ i ].name ) ); documentFragment.appendChild( item ); } // documentFragment を list に追加 list.appendChild( documentFragment ); }

      こちらの John Resig の記事 で DocumentFragment とそのパフォーマンスについて述べられています.

      innerHTML を用いた DOM 操作

      "+=" などの演算子を用いて innerHTML に新たなマークアップを追加していってはいけません. innerHTML が変更されるたびに, DOM のアップデート (ブラウザがマークアップを更新する) が起こります. よって += でマークアップを追加することはパフォーマンスの低下を招きます (特にループの中では).

      var container = document.getElementById( 'container' );
      
      for( var i = 1; i <= 10; ++i ) {
          container.innerHTML += 'Item ' + i + '
      '
      ; }

      この場合は一時変数にマークアップを格納し, 最後に追加すべきです.

      var container = document.getElementById( 'container' )
        , str = '';
      
      for( var i = 1; i <= 10; ++i ) {
          str += 'Item ' + i + '
      '
      ; } container.innerHTML += str;

      コメント欄より

      delete 演算子, innerHTML

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3194

      使い終わった配列などに null を代入するよりも delete arr とするほうがベターです. その方が意図が明確になるからです. null の場合静的解析時にパーサの速度を低下させます. ただし使い終わった配列をクリーンアップするというのは全く正しいアイデアです.

      またすべてのブラウザに innerHTML があるわけではないことにも注意してください.

      delete への反論

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3206

      次のようなコードはうまくうごきません.

      (function(){
        var arr = [1,2,3]
        alert(delete arr); // false
        alert(arr);
      }());
      

      詳しくは kangax の記事 を参照してください.

      また Array は参照型ではありません. ES3 の定義では "Reference" は base object とプロパティ名から成り立っています. 上の例での arr の base object は ES3 では "Activation Object" と呼ばれているものです.

      おすすめの書籍

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3202

      無料でオンラインで読めるこの書籍がおすすめです.

      Eloquent JavaScript

      アマゾンで書籍版を買うこともできます.

      Eloquent JavaScript: A Modern Introduction to Programming: Marijn Haverbeke: 8601419214532: Amazon.com: Books


      for in のパフォーマンス

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3216

      配列の for in は遅いので使わないほうがいいです. ベンチマーク

      innerText と textContent

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3234

      http://www.ifadey.com/2011/05/javascript-mistakes-you-must-avoid/comment-page-1/#comment-3238

      innerText は IE の独自機能が元で, サポートされていないブラウザがあります.類似のものに標準の textContent がありますが, innerText とは違うものです.

      http://clubajax.org/plain-text-vs-innertext-vs-textcontent/

      *1:"邪悪な演算子"とまで言われてしまっています