「Webアプリエンジニア養成読本」を献本してもらった!読んだ!
「Webアプリエンジニア養成読本」を献本してもらいました。あざす!
「点」ではなく「線」
私自身は「28歳の時に未経験からWebエンジニアに転職する」という無謀な試みをして、今年3年目になります。日々開発に勤しむ日々を過ごしておりますが、Webアプリを作るために必要な技術要素って広い!と日々痛感しております。
たまたま自分は本書の著者の1人であるゆーすけべーさんはじめ、先輩諸氏から色々教わることができましたが、教えてくれる人が身近にいて一番よかったのは
何を勉強すればよいのかが分かった
ことです。本書にはその「何を勉強すればよいのか」がコンパクトにまとまっていると思いました。技術書って「点」で書かれているものが多いと思います(PHP、Ruby、MySQLみたいに特定技術ピンポイントで深堀されてる本)。一方、本書は「Webアプリを作るための技術要素」という「線」で書かれているように思いました。
この本をざっと読んで手を動かせば、脳内でWebアプリを作るために必要な技術要素という「点」が繋がって「線」になっていくように思います。Webアプリエンジニア先輩諸氏がconnecting the dotsしてくれた感じで素敵です。
未経験の人は「索引作り」に
まだWebアプリを作ったことが無い方は、きっと「Webアプリを作るために必要な技術的要素の索引」がまだ頭の中に出来上がっていない状態だと思うので、頭の中に索引を作るイメージで読み進めるとよいかと思います。
まずは、この本に書いてあることを、ざっと手を動かしてやってみる(全体を見失わないようにとにかく先に進める)。すると「何を勉強すればよいのか」当たりがつけられるようになる。その後は自分でアプリの企画を考えて実際に作ってみたり、より深く勉強したい技術的要素があればさらに勉強していくみたいな感じで進めるとよいかと。
あ、ただ本書ではクライアントサイドJavaScriptとデータベースについて本当にさらっとしか触れられていないので、その部分はちょい踏み込んで自分で調べる必要が出てくると思います。
2-3年の経験がある人は「穴埋め」に
自分のように2, 3年の経験がある人にとっては、「穴を埋める」という視点で読むとよいかと思いました。もちろん「この本に書かれていることは全て知り尽くしている」という方もいらっしゃるでしょうが、自分は結構穴がありました。例えば、自分は「開発」に携わることが多く「運用」の経験が浅いです。なので、やはりその「運用」を扱っている3章,4章は穴が多かったです。
また、PHPの章はいわゆる「モダン」な開発手法が用いられているので、「レガシーなPHPの保守に手一杯でモダンなPHPの学習が追いついていない」なんて方は結構新しい発見があるのではないでしょうか。具体的にはComposerでライブラリ管理して、WAFとしてSlim(RubyのSinatraライクなWAF)、テンプレートにTwig、ORMにEloquent(Laravelで使われているORM)を使ってサンプルアプリを開発していくという構成になっているので、その辺の経験がなければ新しい発見があるかと。
まとめ
未経験だった頃の自分には無条件でオススメしたい。今の自分にとっても穴が把握できてよかったです!
Backbone.jsでViewからModel・DOMを操作する時の流れ
自分の頭の中の整理のために。
はじめに
Backbone.jsガイドブックを読んでいたら、このような事が書いてあった。
Backbone流MVCでは、ビューとコントローラは両方ともViewが担当します。(略)これらはそれぞれViewのメソッドとして実装しますが、本書では区別のために前者をビューメソッド、後者をコントローラメソッドと呼ぶことにします。(略)重要な点は、DOMイベントに応じてコントローラメソッドが実行されることで処理が始まり、その中でmodelやcollectionを操作し、その結果生じるイベントがビューメソッドを呼ぶ、という流れを意識することです。そのためにも、コントローラメソッドには処理の起点になる以上の仕事を与えず、DOM操作はすべてビューメソッドに集めるよう明確に区別しましょう。
図にするとこんな感じになる(点線がイベントで実線が直操作)
しかし、なぜこのような書き方にするとよいのか?2つの疑問をもった
1. なぜコントローラメソッドとビューメソッドを切り分けるのか?
2. なぜコントローラメソッドがビューメソッドを直接呼び出さないのか?
以降、それぞれ整理してみる
1. なぜコントローラメソッドとビューメソッドを切り分けるのか?
まず、なぜわざわざコントローラメソッドとビューメソッドを切り分ける必要があるのか?逆に以下のように一緒にすると何が困るのか?
思うに、この書き方の問題は、DOMの更新を担当する部分のコードが再利用できないことにあると思う。例えば以下のように複数のイベントハンドラが同じDOM更新を行う場合、上記の書き方だとコードが重複する。
DOM更新の部分を切り出して別メソッドにすれば再利用できる
役割が分離されているので、テストもしやすくなるし、理解もしやすくなる。
2. なぜコントローラメソッドがビューメソッドを直接呼び出さないのか?
では次に、なぜコントローラメソッドが直接ビューメソッドを呼び出さない方がよいのか?つまり以下のように書くと、なぜよくないのか?
恐らく、問題はModelとViewが1対多になった時に起こる。自分が作ったWebサービス(音楽制作のためのWebサービス)でModelとViewが1対多になる部分が多くあったので例にあげる。例えばこんな部分。
この部分では音源を表す1つのModelに対して、再生ボタンを管理するPlayButtonViewと波形を管理するWaveformViewという2つのViewが存在している。例えば音源が停止している状態から再生ボタンと波形それぞれをクリックすると、以下のような動きをする。
・再生ボタンをクリック
- > PlayButtonViewがModelを再生
- > PlayButtonViewが再生ボタンを停止ボタンにする + WaveformViewが波形を動かす
・波形部分をクリック
- > WaveformViewがModelを再生
- > PlayButtonViewが再生ボタンを停止ボタンにする + WaveformViewが波形を動かす
まとめ
もちろん、コントローラとなるViewのメソッドが必ずModelとDOMを更新するわけではない(ただDOMを更新するだけの時や、イベントを発火するだけの時など様々なケースがある)し、ViewとModelが複雑でイベント仲介者(Mediator)を置かないと厳しいこともあるが、「Model更新->DOM更新の基本的ケース」においては、上記のような書き方をするとだいたい幸せになれるように思った。
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が参考になった
Perlいつやるの?今でしょ!
ゆーすけべー先生のPerl入門書がKindle本で出ました。詳細はこちら
僕は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
cpanはRubyの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
以上
とりあえず基礎文法はざっと入門した!こんどはWebアプリの作り方に入門してみよー
参考サイト
細かい文法とかは以下のサイトが参考になりましたー
基礎文法
http://d.hatena.ne.jp/perlcodesample/20091226/1264257759
配列、ハッシュ
http://d.hatena.ne.jp/perlcodesample/20100930/1278596435
関数
http://d.hatena.ne.jp/marsonic/20090302/1235984068
無名関数、クロージャ
http://d.hatena.ne.jp/bingo_nakanishi_perl/20090211/1234317567
OOP
http://d.hatena.ne.jp/perlcodesample/20090317/1237125522
や...やっと理解できた!(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