Hatena::ブログ(Diary)

JP::HSJ::Junknews::HatenaSide このページをアンテナに追加 RSSフィード

2011年01月24日

JavaScript: The Good PartsからTitanium Mobileを触れるのに知っておいたほうがいいことアレコレ(超下書きバージョン)

JavaScriptのスキルをステップアップさせていく中で必読なのがオライリージャパンからリリースされているJavaScript: The Good Parts ―「良いパーツ」によるベストプラクティスです。

もちろん必読といっている以上ぜひとも読んでいただきたいのですが、その中でも特にTitanium Mobileを触るためにJavaScriptはじめました!的な方に基本構文以外で知っておいて頂きたいこと*1をいくつかまとめたいと思います。

ちなみにprototypeチェーンとか扱いません。

なお、ぼく自身JavaScriptのエキスパートでもなんでもないので間違ってる/勘違いしている点も多々あるかもしれません。詳しい方はビシバシご指導お願い致します><

関数リテラルと無名関数

一昔前までのJavaScriptの教本には関数定義は次のように記載されていました。

// 関数定義
function hogehoge(foo, bar){
  // hogehogeの処理
}
// 実行する
hogehoge('1st', '2nd');

もちろんこれでも間違っていないのですが、次のようにも記述できます。

// 別表現
var hogehoge = function(foo, bar){
  // この関数の処理
};
// 実行する
hogehoge('1st', '2nd');

実際にはfunction hogehoge...の記述は後者となるこの表現に置き換えられ、変数スコープの先頭に勝手に配置される糖衣文法(シンタックスシュガー)です。

また、「var 変数名 = 変数の値」と同じ表現をしていることから分かりますが、JavaScriptの関数はその他の値同様にオブジェクトとして扱われます。つまり変数名が割り当たる直前まで名前がありません。この名前が無い関数無名関数(匿名関数とも)といいます。

モダンなJavaScriptを記述していくなかでこの無名関数の活用がカギとなってきます。

オブジェクトリテラルとJSON

ハッシュテーブル(連想配列)は識別子となるキー(数値や文字列)と値を対としたプロパティで管理するデータ構造です。

// Person変数にハッシュテーブル(連想配列)を定義します。
var person = {
  name:     'donayama',
  age:      33
};
// 値を参照するときはキーとなる文字列を['key']として表現してもよいし
// .で連結して表現するシンタックスシュガーもある。
Ti.API.info(person['name']);
Ti.API.info(person.age);

実際のところJavaScriptにおける「オブジェクト」とはこのハッシュテーブルと何も変わりません。

前述の通りJavaScriptの世界では関数オブジェクトです。関数メソッド)と値(プロパティ)はどちらもオブジェクトですので、次のような表現ができるわけです。

var person = {
  name:     'donayama',
  age:      33,
  // 配列も持てる(要素もオブジェクト)
  accounts: [
    {site:   'http://twitter.com/',
     acount: '@donayama'},
    {site:   'http://www.hatena.ne.jp/',
     acount: 'id:donayama'},
    {site:   'http://mixi.jp/',
     acount: '102587'}
  ],
  // 関数もオブジェクトなのでキーのペアとなる値として扱える
  say:      function(message){
     alert(message);
  }
};
// 同様にアクセスできる
for(var i = 0; i < person.accounts.length; i++){
  Ti.API.info(person.accounts[i].site);
}
person.say('こんにちはこんにちは');

継承プロトタイプチェーン)などを使わない限り、この生のオブジェクトを活用していくのが手っ取り早いです。

ちなみにJSONはここから関数を持つことができなくなったものと、とりあえず考えておけばいいでしょう。

非同期処理とコールバック関数

ネットワーク経由でデータを取得する場合など同期処理(処理Xが終わるのを受けて次の処理Yを行う)にしてしまうと、コーディングする側には処理順序が分かりやすく書きやすいのですが、そのプログラムを使う側にとっては待ち時間が掛かってしまってストレスとなります。

例えばネットワーク経由でデータ取得した内容を画面表示するケースでは、(1)データ取得する処理→(待ち時間)→(2)取得結果を画面表示するとなり、待ち時間がダイレクトにユーザに跳ね返ってきます。

そこで処理Xが終わるのを待たずに次の処理Zを実行し、処理Xが終わり次第処理Yを実行するというスタイルが非同期処理です。これにより続きの処理が先に実行され、ユーザの利便性が確保されます。

処理がシーケンシャルに並ばないため、なかなか概念として掴みにくいと思いますが、よくあるonclickで呼び出される処理もいわば非同期です。ユーザのアクション(ここではclick)がされるまで他の処理をしないのではなく、ユーザのアクションがあり次第onclickで指定された処理を実行しているわけです。

(図をそのうち書く)

非同期処理を受けて呼び出される処理をコールバックといい、大抵関数として表現されます。これをコールバック関数といいます。電話番号を伝えておいて電話を掛けなおしてもらうイメージです。

関数スコープ(notブロックスコープ)

変数のスコープはブロックではなく、関数の定義範囲です。

HTML+JSですが、ありがちな間違いを以下に示します。

<html>
  <body>
    <button id="btnButton0">太郎</button>
    <button id="btnButton1">次郎</button>
    <button id="btnButton2">三郎</button>
  </body>
</html>
<script>
var ar = [{name:'太郎'},{name:'次郎'},{name:'三郎'}];
for(var i = 0; i < ar.length; i++){
  var btnButton = document.getElementById('btnButton' + i);
  var name = ar[i].name;
  btnButton.addEventListener('click', function(e){
    alert(name);
  },false);
}
</script>

本来、それぞれの名前に対応する動作になって欲しいところですが、どのボタンを押しても結果は「三郎」としかでないはずです。

というのもこのnameという変数自体が関数スコープであるため、定義箇所としてはarなどと変わりません。(forを回しているiの宣言位置も同様の扱いになります)

そのためforで3回処理した最後の評価結果(三郎)がnameにセットされているため、どのボタンを押してもname="三郎"という状態なのです。

それではと次のようにすると別の罠にひっかかります。

var ar = [{name:'太郎'},{name:'次郎'},{name:'三郎'}];
for(var i = 0; i < ar.length; i++){
  var btnButton = document.getElementById('btnButton' + i);
  btnButton.addEventListener('click', function(e){
    // nameの定義位置を変えたが…
    var name = ar[i].name;
    alert(name);
  },false);
}

エラーになるはずです。たとえ関数スコープとはいえ、ar[i]自体が大外で定義されているため、評価されるタイミングではiが3になっており、ar[3]が存在しないという状態になるからです。

ではどうすればいいのかというと、こうなります。

var ar = [{name:'太郎'},{name:'次郎'},{name:'三郎'}];
for(var i = 0; i < ar.length; i++){
  (function(){
    var btnButton = document.getElementById('btnButton' + i);
    var name = ar[i].name;
    btnButton.addEventListener('click', function(e){
      alert(name);
    },false);
  })();
}

該当部分だけを抜き出します。

(function(){
  // 処理
})();

これは「(function(){〜})」という無名関数に対して空の引数を引き渡して実行(評価)している、という状態です。

関数の外側にも当然スコープが届くため、そのときのar[i].nameがキチンとnameに対して代入されていることになります。

ちなみにclickの評価タイミングが非同期であるため、次のようにするとエラーになりますので、気をつけましょう。

  (function(){
    var btnButton = document.getElementById('btnButton' + i);
    //var name = ar[i].name;
    btnButton.addEventListener('click', function(e){
      alert(ar[i].name);
    },false);
  })();

また、ループしている添え字や他の変数を使いたい場合は無名関数に対して引数として引き渡すことも可能です。*2

for(var i = 0; i < ar.length; i++){
  (function(j){
    var btnButton = document.getElementById('btnButton' + j);
    var name = ar[j].name;
    btnButton.addEventListener('click', function(e){
      alert("[" + j + "] " + name);
    },false);
  })(i);
}
thisの取り扱い(書くネタだけ先においとく)
var ar = [
  {
    name:'太郎',
    show: function(){
      alert(this.name);
    }
  },
  {
    name:'次郎',
    show: function(){
      alert(this.name);
    }
  },
  {
    name:'三郎',
    show: function(){
      alert(this.name);
    }
  }
];
for(var i = 0; i < ar.length; i++){
  (function(){
    var btnButton = document.getElementById('btnButton' + i);
    var item      = ar[i];

    // パターンA) ×
    // このときのitem.show内のthisはそれぞれのボタンエレメントを指すため、this.nameはundefinedとなる
    btnButton.addEventListener('click', item.show);
    
    // パターンB) ○
    // この場合ののitem.show内のthisはitem自身を指すため、個々のnameが出力される
    btnButton.addEventListener('click', function(e){
      item.show();
    });
  })();
}

*1:といいつつ触れていることはまったくもって基本構文の枠内なんですがね…

*2:@cherenkovさんありがとうございます