Backbone.jsをRailsで使った際の、初期設定とルール

こないだまでRailsとBackbone.jsを使ったWEBサービスを作ってました。折角なので、その際の初期設定とちょっとしたルールをまとめておきます。ちなみに、規模感は以下のような感じです。

・ModelとCollection 各約10個
・ViewとTemplate 各約30個
・Routerは使わない(SinglePageApplicationではないので)

バージョンは

・backbone.js: 1.1.0
rails: 3.2.13

です

ライブラリの配置

依存ライブラリは以下のように配置した

// vendor/assets/javascripts/配下
.
|-- backbone/
|   |-- backbone-min.js
|   `-- backbone-min.map
|-- json2/
|   `-- json2.js
|-- underscore/
|   |-- underscore-min.js
|   `-- underscore-min.map
...

ディレクトリ構成

jsコードのディレクトリ構成は以下のようにした。collectionsとmodelsは直下にファイルをおき、viewsとtemplatesはモデルに対してディレクトリを作ってその下にファイルを置いて行った(その構造に合わないファイルも沢山出てきたので、ある程度自由にディレクトリを切っていった)

// app/assets/javascripts/配下
.
|-- collections/
|   `-- users.js
|-- models/
|   `-- user.js
|-- templates/
|   `-- users/
|       `-- xxx.hbs
|-- views/
|   `-- users/
|       `-- xxx.js

テンプレート

テンプレートにはhandlebarを使った。rails用のgemでhandlebars_assetsというのがあったので、これを利用

https://github.com/leshill/handlebars_assets

gemをインストール

group :assets do
  gem 'handlebars_assets'
end

テンプレートはこんな感じになる

// app/assets/javascripts/templates/users/user-detail.hbs
<span>名前:</span><span>{{name}}</span>
<span>メール:</span><span>{{mail}}</span>

テンプレート関数のデフォルト名は「HandlebarsTemplates」だが、気に入らなければ変えられる。例えば「JST」という名前にしたければ、config/initializers/handlebars.rbというファイルを新規作成し、その中身を以下のようにする

HandlebarsAssets::Config.template_namespace = 'JST'

するとViewからはこのようにテンプレートを使えるようになる

// app/assets/javascripts/views/users/user_detail.js
MYAPP.UserDetailView = Backbone.View.extend({
  template: JST['users/user-detail'],

  render: function() {
    this.$el.html(this.template({
      name: this.model.get('name'),
      mail: this.model.get('mail')
    }));

    return this;
  }
});

application.js

以上を動かすためのapplication.jsは以下のようになる。require_treeでディレクトリを再起的に読み込むようにする

//= require jquery
//= require jquery_ujs
//= require json2/json2
//= require underscore/underscore-min
//= require backbone/backbone-min
//= require handlebars.runtime
//= require_tree ./templates
//= require application
//= require_tree ./models
//= require_tree ./collections
//= require_tree ./views
//= require_tree .

名前空間

名前の衝突を避けるために、アプリケーションに関する名前空間を作る(といってもグローバルなオブジェクトを1つ作るだけ)。application.jsに以下を記載。

window.MYAPP = window.MYAPP || {};

クラス名規則

上記の名前空間の下にクラスとなるオブジェクトを作っていくのだが、その際のクラス名は以下のようにした。全て大文字ではじめる。CollectionはModelの複数形。ViewはModel/Collectionと1対1であれば頭にModel名をつける(そのような関係にならない場合も多いので、その場合は自由に命名する)。

// Model
MYAPP.User = Backbone.Model.extend({ /* ... */ });

// Collection
MYAPP.Users = Backbone.Collection.extend({ /* ... */ });

// View
MYAPP.UserListView = Backbone.View.extend({ /* ... */ });

アプリケーション全体での共通前処理

アプリケーション全体で必ず実行したい共通の前処理が存在したので、Backboneオブジェクト自体のイベントを利用した。application.jsに共通前処理を書き、それが終わったらBackbone.triggerでカスタムイベント(以下の例ではinit)を発火する。各ページのjavascriptではこのイベントを購読しておくという形。

application.js

$(function() {
  // 共通前処理
  // ...
  Backbone.trigger('init');
});

各ページ

Backbone.on('init', function() {
  // 各ページの処理
});

サーバーサイドで生成するjsonのハンドリング

サーバーサイドで生成するjsonのハンドリングは、基本的にはas_jsonメソッドを用いた。使い方はこちらのブログが詳しい

http://d.hatena.ne.jp/gutskun/20130409/1365518684

もっと細かいハンドリングをしたい場合にはgemを使った方がよいと思う。Rails4からはデフォルトで組み込まれているJbuilderかRABLあたりが有名

http://railscasts.com/episodes/320-jbuilder?language=ja&view=asciicast
http://railscasts.com/episodes/322-rabl?language=ja&view=asciicast

初期データ投入

大きく2つの方法があるように思う。1つはデータが空のオブジェクトを作ってからサーバーにfetchするもの。もう1つはhtmlレンダリング時にデータも入れてしまうもの。

1つ目の方法
<div id="user-list-container"></div>

<%= javascript_tag do %>
  Backbone.on('init', function() {
    var users = new MYAPP.Users();
    var userListView = new MYAPP.UserListView({collection: users});
    $('#user-list-container').html(userListView.render().el);
    users.fetch({reset: true});
  });
<% end %>

この場合の流れは
・データが空の状態でViewレンダリング
・ model/collectionがサーバーへデータをfetch
・ model/collectionのresetイベント発火
・ viewがresetイベントキャッチして、再度レンダリング

2つ目の方法
<div id="user-list-container"></div>

<%= javascript_tag do %>
  Backbone.on('init', function() {
    var users = new MYAPP.Users(JSON.parse('<%= j @users.to_json.html_safe %>'));
    var userListView = new MYAPP.UserListView({collection: users});
    $('#user-list-container').html(userListView.render().el);
  });
<% end %>

この場合の流れは
・モデルのjsonデータをサーバー側でjsコードとして出力
・データがある状態でViewがレンダリング

2つ目の方法の方がレンダリングが速いので、特に理由がなければ2つ目の方法を使う。

クライアントサイドでレンダリングするか、サーバーサイドでレンダリングするか

前述コードは、backbone管理下の要素をクライアントサイドで生成するようなコードだった。けれど、そうするとその部分の要素に関してはSEOが死ぬ。この問題は別にbackboneに限った話ではなく、angularのような他のライブラリを使っていても、クライアントサイドでレンダリングする限り起こる問題。対策法はこちらのサイトなどが詳しい

http://info.appdirect.com/blog/solving-the-javascript-seo-conundrum-part-one
http://www.ng-newsletter.com/posts/serious-angular-seo.html

けれど、今回はそこまで大掛かりなことをせずに局所的に対策すればよい程度だったので、SEO対策な必要なページはサーバーサイドでレンダリングし、クライアントサイドではサーバーサイドでレンダリングされたhtmlに対してbackbone.jsのオブジェクトをアタッチするようにした。参考にしたのはこちらのstackoverflowのポスト

http://stackoverflow.com/questions/7549306/single-page-js-websites-and-seo

コードはこんな感じ。

<div id="user-list-container">
  <% @users.each do |user| %>
    <div class="user-detail" data-id="<%= user.id %>">
      <span>名前:</span><span><%= user.name %></span>
      <span>メール:</span><span><%= user.mail %></span>
    </div>
  <% end %>
</div>

<%= javascript_tag do %>
  Backbone.on('init', function() {
    var users = new MYAPP.Users(JSON.parse('<%= j @users.to_json.html_safe %>'));
    var userListView = new MYAPP.UserListView({el: $('#user-list-container'), collection: users});
    userListView.$('div.user-detail').each(function(i, el) {
      var el = $(el);
      var id = el.attr('data-id');
      var user = users.get(id);
      new MYAPP.UserDetailView({el: el, model: user});
    });
  });
<% end %>

このアプローチだと、テンプレートをクライアント側でも使いたい場合にコードの重複が発生するが、今回はそのような箇所がそれほど多くなかったので、よしとした。

参考になったソース

https://github.com/documentcloud/documentcloud
Backbone.jsの本家本元、documentcloud。そのソースコードGithubで公開されてる。サーバーサイドはRails。ちょっとバージョンが古いので、書き方もレガシーなメソッドを使っていたりするけど、Todoアプリみたいなサンプルとは違って実運用されている大きなアプリケーションなので多いに参考になった。

https://github.com/samuelclay/NewsBlur
SinglePageApplicationのRSSリーダーNewsBlur。こちらもdocumentcloudのメンバーが作っているっぽい。サーバーサイドはDjango。 media/js/newsblur/配下にbackboneを使ったjsコードが沢山ある。

終わり

いわゆるSinglePageApplicationといったような、ほとんど画面遷移をしないアプリケーションにする場合、またちょっとポイントが変わってくるとは思います(Routerの役割が大きくなるので)。けれどそこまでいかないアプリケーションであれば、上記のような構成+ルールで破綻せずにいけそうな感じでしたー

RubyのIntegerを拡張してFizzBuzzを問題をやってみた

FizzBuzz問題というのをやったことが無かったので、Rubyでやってみた。「ある整数がある整数で割り切れるか?」という部分を抽象化すると、少し汎用的になってよいかと思った。

class Integer
  def dividable?(n)
    self % n == 0
  end                                                                                                                                                                                                       
end

(1..100).each do |n|
  puts n.dividable?(15) ? 'FizzBuzz' : n.dividable?(5) ? 'Buzz' : n.dividable?(3) ? 'Fizz' : n
end

Rubyだと0はtrueになるので、== 0の結果を返してあげる

ネーミングに困ったとき、RailsのAPIが参考になった

最近ちょっとしたORMを書いていて、SQLプレースホルダに「紐付ける値」をなんと命名したらよいか悩んでいたのだけど、こういう定石的な命名は、自分でウンウン悩むよりフレームワークAPIを参照するのが早いと思った

RubyのORMをざっと調べてみたところ、やはりRailsActiveRecord)はシンプルで分かりやすいネーミングをしている思った。ということで、bindsというネーミングをいただきました。

Rails(ActiveRecord)

find_by_sql(sql, binds = [])

Sequel

with_sql(sql, *args)

DataMapper

query(statement, *bind_values)

Perlいつやるの?今でしょ!

ゆーすけべー先生のPerl入門書がKindle本で出ました。詳細はこちら

Perlについて語ろう
Perlについて語ろう
posted with amazlet at 13.03.17
和田裕介 (2013-03-13)

僕はRuby, PHP, JavaScriptしかまともに触った事が無かったのですが、
心の中からこんな声が聞こえました↓

はいぃぃぃぃ!ということで、Perl入門してみることにしましたです。ちなみにゆーすけべー先生のKindle本には、Perlの基礎文法〜Webアプリの作り方まで、結構広範囲についてサクッと書かれていたのですが、各章の「キリ」がよくて、つまみ食いしながら読み進めることができる感じ。なので自分はまず基礎文法だけやってみたです(細かいところはすっとばして、頻出しそうなところだけつまみ食い)。

※ 以下のコードはKindle本に出てくるサンプルコードとは全く異なり、自分で書いたコードですー
※ ちなみに自分はKindle持ってないので、iphoneのKindleアプリ使って読みますた

配列、ハッシュ

配列、ハッシュの扱い方は色々な方法があるようだけど、自分は以下のように無名リファレンスを使う方法が使いやすかった。javascriptの配列リテラルやオブジェクトリテラルに似ている!

use strict;                                                                                                                                                                                                 
use warnings;

my $users = [                                                                                                                                                                                            
  {id => '001', name => 'hoge', sex => 'man'},
  {id => '002', name => 'fuge', sex => 'woman'}
];

# 要素にアクセス
print $users->[0]->{name}; #hoge

# ループでまわす
foreach my $user (@$users) {
  print $user->{name}; #hogefuge
}

関数

javascriptのように無名関数を引数に渡して、コールバックできるんすね!引数の扱い方がちょっと独特だけど、そのうち慣れるでしょー

use strict;                                                                                                                                                                                                 
use warnings;

sub foo {
  my $callback = shift;
  my $y = 'yyy';

  $callback->($y);
}

# レキシカル変数
my $x = 'xxx';

# 無名関数を関数に渡す
foo(sub {
  my $yy = shift;

  print "$x\n"; # xxx
  print "$yy\n"; # yyy
});

cpan

cpanRubyのgemみたいなもんかな。Kindle本でも紹介されてたAcme::Oppaiというジョークモジュールを使ってみた。Acme::ってのはジョークモジュールにつける名前空間らしい。::で名前空間を繋げて行くあたりはRubyに似てる。ちなみにperlとcpanmの環境設定は、Kindle本の「一歩進んだPerlの環境つくり」という章に書いてあったー

$ cpanm Acme::Oppai
use strict;
use warnings;
use Acme::Oppai;

print Acme::Oppai->oppai;
#  _  ∩
#( ゜∀゜)彡 おっぱい!おっぱい!
# ⊂彡  

OOP

OOPの勉強がてらAcme::Oppaiの拡張的なものを作ってみた。コマンドラインからoppaiコマンドを叩くと、引数によって表示されるAAが変わる代物。

クラス概要


ディレクトリ構成
.
├── Acme/
│   ├── OppaiEx/
│   │   ├── KnightsOfRound.pm
│   │   ├── Kuma.pm
│   │   └── Manager.pm
│   └── OppaiEx.pm
└── oppai*
oppai

コマンドとなるプログラム。こいつに実行権限をつけて、パスを通す

#!/usr/bin/env perl                                                                                                                                                                                         

use strict;
use warnings;
use Acme::Oppai;
use Acme::OppaiEx;

my $key = @ARGV ? shift : '';

my $oppai;
if ($key eq "kuma") {
  $oppai = Acme::OppaiEx::Kuma->new();
} elsif ($key eq "knights") {
  $oppai = Acme::OppaiEx::KnightsOfRound->new();
} else {
  $oppai = Acme::Oppai->new();
}

my $manager = Acme::OppaiEx::Manager->new($oppai);
$manager->show;
Acme/OppaiEx.pm

各パッケージを読み込むプログラム

package Acme::OppaiEx; 

use strict;                                                                                                                                                                                                 
use warnings;
use Acme::OppaiEx::Manager;
use Acme::OppaiEx::Kuma;
use Acme::OppaiEx::KnightsOfRound;

1;
Acme/OppaiEx/Manager.pm

oppaiオブジェクトを管理するパッケージ。oppaiプロパティにoppaiオブジェクトをセットし、showメソッドでそのオブジェクトのoppaiメソッドを呼び出す。

package Acme::OppaiEx::Manager;                                                                                                                                                                             

use strict;
use warnings;

sub new {
  my($class, $oppai) = @_;

  bless {
    oppai => $oppai
  }, $class;
}

sub show {
  my $self = shift;
  print $self->{oppai}->oppai;
}

1;
Acme/OppaiEx/Kuma.pm

クマおっぱいを返すパッケージ

package Acme::OppaiEx::Kuma;                                                                                                                                                                                

use strict;
use warnings;

sub new {
  bless {};
}

sub oppai {
<<'EOS';
      ∩___∩
      | ノ       ヽ (⌒ヽ
     /⌒) ●   ● | / / ミ
  / /   ( _●_)  ミ  /   おっぱい!おっぱい!
 .(  ヽ  |∪|  ` 彡
  \   _ヽノ_  彡
   /  (__ ___/
    |       /
    |  /\ \
    | /    )  )
    ∪    (  \
          \_)
EOS
}

1;
Acme/OppaiEx/KnightsOfRound.pm

円卓の騎士的なおっぱいを返すパッケージ

package Acme::OppaiEx::KnightsOfRound;                                                                                                                                                                      

use strict;
use warnings;

sub new {
  bless {};
}

sub oppai {
<<'EOS';
 \\                                                      //
   \\   お    っ    ぱ    い.                                //
    \\                                                //
      \\                     お    っ    ぱ    い     //
        _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.
      ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡
      (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡
                 _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.
               ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡
               (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.
                |   |     |   |     |   |     |   |    |   |     |   |     |   |
                し ⌒J.    し ⌒J.    し ⌒J.    し ⌒J.   し ⌒J.    し ⌒J.    し ⌒J
EOS
}

1;
実行!
$ oppai

  _  ∩
( ゜∀゜)彡 おっぱい!おっぱい!
 ⊂彡
$ oppai kuma

      ∩___∩
      | ノ       ヽ (⌒ヽ
     /⌒) ●   ● | / / ミ
  / /   ( _●_)  ミ  /   おっぱい!おっぱい!
 .(  ヽ  |∪|  ` 彡
  \   _ヽノ_  彡
   /  (__ ___/
    |       /
    |  /\ \
    | /    )  )
    ∪    (  \
          \_)
$ oppai knights

 \\                                                      //
   \\   お    っ    ぱ    い.                                //
    \\                                                //
      \\                     お    っ    ぱ    い     //
        _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.
      ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡
      (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡
                 _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.     _ _∩.
               ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡  ( ゚∀゚)彡
               (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.   (  ⊂彡.
                |   |     |   |     |   |     |   |    |   |     |   |     |   |
                し ⌒J.    し ⌒J.    し ⌒J.    し ⌒J.   し ⌒J.    し ⌒J.    し ⌒J
できた!

_人人人人人人人人人人人人人人人人人人人人人人_
>まさに、おっぱいのポリモーフィズム多態性)や!<
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

以上

とりあえず基礎文法はざっと入門した!こんどはWebアプリの作り方に入門してみよー

や...やっと理解できた!(2) JavaScriptのスコープチェーン

前回はJavaScriptのプロトタイプチェーンについて、図解を用いることでなんとか理解できました。今回はスコープチェーンに挑戦してみます。前回と同じく「1. 図解を用いる」「2. 用語を明確に定義する」「3. Standard ECMA-262 3rd editionを情報ソースとする」というアプローチで紐解いて行きます。

用語の定義

・本エントリの文章における表記は、以下の表の「ECMA-262 3rd」に統一する
・本エントリの図における表記は、以下の表の「本エントリの略称」に統一する
・本エントリ内におけるES3とは、Standard ECMA-262 3rd editionを指す

ECMA-262 3rd 本エントリの略称 JavaScript(サイ本)第5版(日本語)
Execution Contexts EC 実行コンテキスト
Variable Object VO 変数定義のために使われるオブジェクト
Activation Object AO Callオブジェクト
Global Object GO グローバルオブジェクト
Function Object FO Functionオブジェクト
Scope Chain SC スコープチェーン
[[Scope]] [[Scope]] 言及なし?

※表中の「JavaScript(サイ本)第5版(日本語版)」は比較のための参考情報

前置き

Execution Contextとは?

JavaScriptのスコープチェーンを理解するためには、まずExecution Contextについて理解する必要がある。Execution Contextとは「論理的にスタックの構造をしたもの」であり、コントロールが実行可能コード(Global Code、Eval Code、Function Codeの3つ)に移ると新しいExecution Contextに入る(ES3 10)。つまり、以下の図のように、コントロールが実行可能コードに移ると新しいExecution Contextがスタックにpushされ、そのコードから戻るとスタックからpopされると捉えることができる。


Execution Contextに入ると何が起きるか?

では、新しいExecution Contextに入ると何が起きるのだろうか?ES3では以下3つの処理が行われると定義されている(ES3 10.1.6)。

(1) Scope Chainの生成・初期化
(2) Variable Instantiationの実行
(3) thisの値の決定

上記それぞれにおいて、具体的にはどのような処理が行われるのだろうか?これは実行可能コードの種類によって異なるとされている。そして実行可能コードには3つの種類のコードが存在し、それぞれ以下の通りである(ES3 10.2)。なお、本エントリでは、このうち(1) Global Codeと(3) Function Codeについて扱う。

(1) Global Code
(2) Eval Code
(3) Function Code

サンプルコード

本エントリでは、以下のサンプルコードについて図解を用いて紐解いていくことにする。

var x = 'xxx';                                                                                                                                                                                              

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z); //xxxyyyzzz
  }

  bar();
}

foo();
最初にGlobal Objectが存在する

まず、いかなるExecution Contextに入るよりも前に、唯一のGlobal Objectが生成される(ES3 10.1.5)。初期状態のGlobal ObjectにはMathやStringといったBuilt-in objectsとブラウザ環境におけるwindowオブジェクトのようなホストが定義するプロパティがセットされる(この点について今回は割愛する)。


Global Code

◆ 新しいExecution Contextに入る

Global Objectが生成された後、Global Codeにコントロールが移る。すると、新しいExecution Contextがスタックに積まれる。そして冒頭記載したようにこのExecution Contextを元に(1) Scope Chainの生成・初期化 (2) Variable Instantiationの実行 (3) thisの値の決定が行われる。詳細は後述するが、これらの処理はExecution Contextに属するScope Chain(図中SC)、Variable Object(図中VO)、thisの値を決定するものと捉えることができる。

◆ Scope Chainの生成・初期化

新しいExecution Contextに入ると、まずScope Chainの生成・初期化が行われる。では、Scope Chainとは何か?ES3によるとScope Chainとは識別子の検索に使われるオブジェクトの「リスト」のことであり、そのExecution Contextの実行中はwith文かcatchを使わない限り不変である(ES3 10.1.4)。なお「リスト」という概念をどう実装するかはES3では定義されていない。そのため、ここでは純粋な「配列」であると考える。では、Global CodeにおけるScope Chainの生成・初期化はどのようになるのか?これはいたってシンプルで、Global CodeにおけるScope ChainはGlobal Objectのみを含むリストとなる(ES3 10.2.1)。

◆ Variable Instantiationの実行

Scope Chainの生成・初期化が行われると、次にVariable Instantiationが行われる。Variable InstantiationとはVariable Objectという特殊なオブジェクトにプロパティとその値を追加する処理のことである。全てのExecution ContextにはVariable Objectという特殊なオブジェクトが存在し、そのExecution Contextのコードにおける変数と関数の宣言はそのVariable Objectのプロパティとして追加されることになる。なお、どのオブジェクトがVariable Objectになるかはコードの種類によって異なり、Global Codeの場合はGlobal ObjectがVariable Objectとなる(ES3 10.1.3, 10.2.1)。

そしてVariable Instantiationでは以下の順番でVariable Objectにプロパティとその値が追加されていく(ES3 10.1.3)

(1) (Function Codeの場合)仮引数がVariable Objectのプロパティとして追加され、その値として実引数がセットされる。
(2) 対象コード内のFunctionDeclaration(関数宣言。関数式は除く)全てに対して、その宣言における関数名がVariable Objectのプロパティとして追加され、その値として新たに生成されたFunction Objectがセットされる。
(3) 対象コード内のVariableDeclaration(変数宣言)全てに対して、その宣言における変数名がVariable Objectのプロパティとして追加され、その値としてundefinedがセットされる。
※ 必ず1→2→3の順番で実行される

今回のサンプルコードにおいて、Global Codeでは変数xの宣言(VariableDeclaration)と、関数fooの宣言(FunctionDeclaration。仮引数はなし)が行われている。上記のルールに従うと、まず関数fooの宣言が処理され(今回はFunction CodeではなくGlobal Codeのため(1)の仮引数の処理は行われない)、その後変数xの宣言が処理されることになる。では、まず関数fooの宣言の方はどのように処理されるのだろうか?

FunctionDeclarationにおいては、前述したようにその宣言における関数名(ここではfoo)がVariable Object(ここではGlobal Object)のプロパティとして追加され、その値として新たに生成されたFunction Objectがセットされる。新しく生成されたFunction Objectは[[Scope]]プロパティという内部的なプロパティを持ち、その値として現在のExecution ContextのScope Chainが参照しているオブジェクトと同じオブジェクト(ここではGlobal Objectのみ)を参照するリストがセットされる(ES3 13.2)。

これでFunctionDeclarationの処理が済んだので、次にVariable Declarationが処理される。VariableDeclarationにおいては、前述したようにその宣言における変数名がVariable Objectのプロパティとして追加され、その値としてundefinedがセットされる。つまり、今回の例ではxがGlobal Objectのプロパティに追加され、その値としてundefinedがセットされる(この段階ではまだ値が'xxx'にならない)。

◆ thisの値の決定

上記のようにしてVariable Instantiationの実行が済むと、次はthisの値が決定される。thisとは全てのアクティブなExecution Contextに関連づいており、どのような値になるかは呼び出し元のオブジェクトと実行されるコードの種類によって異なり、その値は一度設定された後は不変である(ES3 10.1.7)。そしてGlobal Codeにおいてthisの値は常にGlobal Objectとなる(ES3 10.2.1)。そのためこの時点でthisの値はGlobal Objectとなる。

◆ コードを実行

ここまで来てはじめてコードの実行が行われる。今回のサンプルコードにおいては、プロパティxへの文字列'xxx'の代入。およびfoo()が実行されることになる。ここでxに'xxx'を代入する際には、識別子の探索(ES3 10.1.4)が行われる。具体的には現在のExecution ContextのScope Chainが参照しているVariable Objectを先頭から(図では0から)検索していき、識別子に該当するプロパティが存在したらそこで解決する。今回のサンプルではGlobal CodeのExecution ContextにおけるScope Chainが参照しているのはGlobal Objectのみであり、そこにプロパティxが存在するので、そこへ代入が行われる。

foo() - Function Code

◆ 新しいExecution Contextに入る

Global Codeにおいてfoo()が実行されると、新しいExecution Contextに入る。新しいfoo()のExecution Contextに入ると、Global Codeの時と同じようにScope Chainの生成・初期化やVariable Instantiationが行われる。ただし、Global Codeと異なり今回実行されるコードはFunction Codeである。「Scope Chainの生成・初期化、Variable Instantiation、thisの値の決定」はGlobal Codeのルールではなく、Function Codeのルールが適用される。

◆ Scope Chainの生成・初期化

Function CodeにおけるScope Chainの生成・初期化では、まずScope Chainの先頭にActivation Objectという特殊なオブジェクトがセットされる。なお、Activation Objectとは、Execution CodeがFunction Codeに入った時にVariable Objectとして使われるオブジェクトのことである。Activation Objectはまず関数の引数を表すargumentsプロパティの初期化を行う(argumentsに関する詳細については別の機会に言及する)その後Activation ObjectをVariable ObjectとしてVariable Instantiationが行われる。なお、Activation Objectとは仕様上の概念であり、プログラムからActivation Object自体には直接アクセスできない(Activation Objectのプロパティにはアクセスできる)(ES3 10.1.6)。

その後、呼び出した関数の[[Scope]]プロパティが参照しているオブジェクトがScope Chainにpushされる(ES3 10.2.3)。従って、今回のサンプルコードにおいて、foo()を実行した直後におけるExecution ContextのScope ChainはActivation Object(foo()の実行によって作られたもの。図ではAO_1とする)とGlobal Objectを参照していることになる。なお、ここでのポイントは、あくまでも呼び出した関数の[[Scope]]プロパティが使われるのであって、決してGlobal CodeのExecution ContextにおけるScopt Chainが使われる訳では無いということだ。このことは、より複雑な例になってきた際に重要となる(次回以降に扱う)

◆ Variable Instantiationの実行

先ほど触れたように、Function Codeでは、Activation ObjectをVariable ObjectとしてVariable Instantiationが行われる(ES3 10.2.3)。その点を除いては、Global Codeの時と同じ処理が行われるので、ここでのVariable Instantiation以下のような処理になる。

Function Declarationで宣言された関数をVariable Object(AO_1)にセットする(プロパティはbar、値は新たに生成されたFunction Object。そのFunction Objectの[[Scope]]プロパティの値は、AO_1とGlobal Objectを参照するリスト)

Variable Declarationで宣言された変数をVariable Object(AO_1)にセットする(プロパティはy、値はundefined)

◆ thisの値の決定

Function codeにおいて、thisの値はその呼び出し元オブジェクトから提供される(callやapplyメソッドでthisの値を指定できる)。もし呼び出し元オブジェクトから提供されたthisの値がオブジェクトでなければ(nullの場合も含む)、thisの値はGlobal Objectとなる(ES3 10.2.3)。今回のサンプルコードにおいては特にthisの値は指定されていないので、Global Objectがthisの値となる。

◆ コードを実行

ここでfooコードの実行が行われる。今回のサンプルコードにおいては、プロパティyへの文字列'yyy'の代入。およびbar()が実行されることになる。


bar() - Function Code

◆ 新しいExecution Contextに入る

bar()が実行されると、foo()の時と同じように新しいExecution Contextに入りScope Chainの生成・初期化等が行われる。

◆ Scope Chainの生成・初期化
foo()の時と同じようにScope Chainが生成・初期化される。まず新しいActivation Object(図ではAO_2)がScope Chainの先頭にセットされる。

その後、呼び出した関数の[[Scope]]プロパティが参照しているオブジェクトがScope Chainにpushされる。この段階でbar()のExecution ContextのScope Chainは、先頭からAO_2, AO_1, Global Objectということになる。

◆ Variable Instantiationの実行
Variable Declarationで宣言された変数zがAO_2のプロパティに追加される。そしてその値はundefinedである。

◆ thisの値の決定

foo()の時と同じく、ここで特にthisの値は指定されていないので、thisの値はGlobal Objectとなる。

◆ コードを実行

ここでbarコードが実行され、プロパティzに文字列'zzz'が代入される。

この時点で現在実行中のExecution Context(barのExecution Context)は、以下の図のような構成になっている。

この段階におけるconsole.log(x + y + z);というコードにおいて、識別子x, y, zはそれぞれ以下のように解決されるため、結果はxxxyyyzzzとなる。

x: AO_2にxを探すが見つからない→AO_1にxを探すが見つからない→GOにxが見つかり、その値は'xxx'
y: AO_2にyを探すが見つからない→AO_2にyが見つかり、その値は'yyy'
z: AO_2にzが見つかり、その値は'zzz'

終わり

以上でスコープチェーンに関する基本的な内容は終わりです。次回はスコープチェーンに関するより詳細な内容(evalやwith文を使った例)を見て行きたいと思っています。その後は、クロージャやthisについても紐解いていく予定です。よろしければ是非続編もご覧くださいm(__)m

まさに忍者...JavaScriptの関数は第一級オブジェクト

JavaScriptの関数は「ファーストクラスオブジェクト(第一級オブジェクト)」である。なので、変数に代入したり、配列にセットしたり、他の関数に食わせたりできる。この変幻自在っぷりはすごい。newでコンストラクタになるところなんて変化の術のようだ。無名関数の即実行は影縫いの術みたいだし、callやapplyでthisの値を変えるとこなんて口寄せの術を彷彿とさせる。正に忍者 |--)ノシュッ==卍

変数に代入する

var foo = function() {console.log('foo');};
foo();

配列にセットする

var fnList = [
  function() {console.log('foo');},
  function() {console.log('bar');},
  function() {console.log('piyo');}
];

for(var i = 0; i < fnList.length; i++) {
  fnList[i]();
}

オブジェクトのプロパティにセットする

var obj = {
  foo: function() {console.log('foo');}
}

obj.foo();

関数に引数として渡す

function foo(callback) {
  callback();
}

foo(function() {console.log('呼び出されました!');});

関数の戻り値を関数にする

function foo() {
  return function() {console.log('fooからreturnされた関数だよ!');}
}

foo()();

無名関数を作って、即呼び出す

(function() {console.log('foo');})();

newをつけてコンストラクタ

var User = function(name, sex) {
  this.name = name;
  this.sex = sex;
  this.hello = function() {console.log("Hello! I am " + this.name);};
}

hoge = new User('hoge', 'man');
hoge.hello();

call(もしくはapply)でコールバック関数内のthisを変更

function foo(callback) {
  callback.call(document);
}

foo(function() {console.log(this);});

関数定義時のスコープを束縛

var x = 'hoge';
function foo(callback) {
  callback();
}

(function() {
    var x = 'fuge';
    foo(function() {console.log(x);});
})();

コンボは無限大...

ちょっとしたコンボが以下

オブジェクトのプロパティに、無名関数を即呼び出した結果をセットする
var obj = {
  x: (function() {return 1 + 2;})()
};

console.log(obj.x);
オブジェクトのプロパティにセットして、動的に呼び出す
var Models = {
  User: {
    save: function() {console.log('userがsaveされました');},
    delete: function() {console.log('userがdeleteされました');}
  },
  
  Entry: {
    save: function() {console.log('entryがsaveされました');},
    delete: function() {console.log('entryがdeleteされました');}
  },
      
  Comment: {
    save: function() {console.log('commentがsaveされました');},
    delete: function() {console.log('commentがdeleteされました');}
  }
};

for(var name in Models) {
  Models[name].delete();
}

Models['User'].save();

応用すれば、もっともっと色々なことができるはず。。。まさに忍者|--)ノシュッ==卍

jQueryで$(document).readyを複数実行した場合のthisやvarについて

はじめに

jQueryを使う時は、$(document).ready(handler)やその省略法である$(handler)でdom構築ができてからコードを実行することが多い。(handlerは無名関数などの関数オブジェクト)

で、たまに複数のファイルで何度も$(handler)を呼び出すことがあるのだけれど、その際$()の引数に渡しているhandler内のthisやvar宣言について混乱しがちだったので整理しておく。ちなみに検証環境はChrome ver24、jQuery1.9.1。概念はES3を基準にする。

前提

書き方色々

以下の3つは同義(真ん中は推奨されない)

$(document).ready(handler)
$().ready(handler) //this is not recommended
$(handler)

http://api.jquery.com/ready/

複数呼び出した場合

呼び出した順に全てが実行される。別々のファイルにしていても同じ。

$(function() {console.log('hoge');});
$(function() {console.log('fuge');});
$(function() {console.log('piyo');});

//結果:
//hoge
//fuge
//piyo

jQueryのtutorial

これをふまえて、handler内のthisやvarの挙動をみていく

this

handler内のthisはdocumentオブジェクトになる。なので、例えば最初のhandler内で「this.x」とすれば、後で呼び出したhandler内でも「document.x(もしくはthis.x)」で参照することができる

$(function() {
  this.x = 'xxx';
  console.log(this === document); //true
});

$(function() {
  console.log(document.x); //xxx
  console.log(this.x); //xxx
});

jQuery1.9.1のソースコードを見てみると、以下の部分でhandlerのthisにdocumentオブジェクトをセットしていることがわかる

readyList.resolveWith( document, [ jQuery ] );

(略)

// 最終的には以下の部分でapplyの第1引数にdocumentオブジェクトがセットされる
// これにより、handlerのthisがdocumentとなる
list[ firingIndex ].apply( data[ 0 ], data[ 1 ] )

varあり

関数コード内でvar 宣言した変数は、アクティベーションオブジェクトのプロパティになる。なので、例えば以下のようにすると、後で呼び出したhandlerからxにはアクセスできない(実行コンテキストが変わってしまうから)

$(function() {
  var x = 'xxx'; //xはアクティベーションオブジェクトのプロパティ
});

$(function() {
  console.log(x); //error
});

アクティベーションオブジェクトについては、こちらが詳しい
http://alpha.mixi.co.jp/2011/10763/#ecma-263-3-2-variable-object-in-function-context

varなし

handler内でvar宣言しなかった変数は、globalオブジェクトのプロパティになる。これはスコープチェーンを遡ってグローバルオブジェクトにたどり着くため(逆に、スコープチェーンの途中でvar xと宣言されていれば、そこで解決する)。なので、以下のようにすると後で呼び出したhandlerでもxを参照できる。

$(function() {
  x = 'xxx'; //xはglobalオブジェクトのプロパティ(スコープチェーンを遡るので)
});

$(function() {
  console.log(x); //xxx
  console.log(window.x); //xxx
});

http://stackoverflow.com/questions/1470488/difference-between-using-var-and-not-using-var-in-javascript

複数ファイルで$(handler)を使いつつ、変数を共有

handler内で定義した変数を他のファイルのhandler内と共有するにはどうすればよいか?
色々なやり方があるけれど、グローバル空間をあまり汚さない方がバグを生みにくいと思う。
なので、自分の場合はプロジェクト固有の名前空間となるオブジェクトを作ってグローバルオブジェクトにセットし、
外部に公開したいAPIはそのプロジェクト固有の名前空間のプロパティとしてセットするようなやり方を主に使おうと思う

html

<html>                                                                                                                                                                                                      
<head>
  <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
  <script src="a.js"></script>
  <script src="b.js"></script>
</head>
</html>

a.js

$(function() {                                                                                                                                                                                              
  var x = 'xxx';

  App = {
    foo: function() {console.log(x);}
  };
});

b.js

$(function() {                                                                                                                                                                                              
  App.foo();
});