はじめに
このブログに記載されている、AngularJSのController As記法をめぐる考察が興味深い。
Alex Ford氏という方が著者なのだが、原文を読むと、 元々は http://www.johnpapa.net/do-you-like-your-angular-controllers-with-or-without-sugar/ にて、「AngularJSのController As記法は最近のトレンドだけど、一種のSyntax Sugarだし、最終的には好みで決めたら?」的なエントリに対して、「いやいや、Controller Asは只のSyntax Sugarじゃないぜ」とAlex氏がController Asの優位性を示している流れ。
Controller As記法
AngularJS 1.1.xでは、Controllerで、scopeの設定を行い、Viewとなるhtmlからは、scopeを参照する記法の一択だった。
<div ng-controller="MainCtrl">
<input ng-model="foo" />
<button ng-click="hoge();">click me!</button>
</div>
function MainCtrl($scope){
$scope.foo = 'bar';
$scope.hoge = function(){
...
}
}
AngularJS 1.2以降では、 ng-contoller
ディレクティブにて as 参照名
を記載することで、
Controllerへの参照をViewから行えるようになり、 $scope
をわざわざ書かなくても良くなるよ、という機能。
<div ng-controller="MainCtrl as main">
<input ng-model="main.foo" />
<button ng-click="main.hoge();">click me!</button>
</div>
function MainCtrl(){
this.foo = 'bar';
this.hoge = function(){
...
}
}
詳細は、 http://qiita.com/soundTricker/items/9a2a27281246ca7ce0b7 や https://docs.angularjs.org/api/ng/directive/ngController を参考のこと(ぶっちゃけ、僕もつい最近まで、この記法のことは全然知りませんでした...)。
$scopeで発生するprototypeチェーン問題
Alex氏はConroller Asのメリットについて語るために、$scopeの階層構造について触れている。
ng-controller
をネストするケースは多々あると思うが、 $scope
の仕組み上、問題が発生する事がある。以下のコードを例にする:
<div ng-controller="MainCtrl">
<div>
<input type="text" ng-model="foo" />
</div>
<div ng-controller="ChildCtrl">
<input type="text" ng-model="foo" />
</div>
</div>
function MainCtrl($scope){
$scope.foo = 'bar';
}
function ChildCtrl($scope){
}
親コントローラ MainCtrl
の $scope.foo
の値に対して、子コントローラ ChildCtrl
側でも変更を行っている。
こいつを実行すると、下記の挙動をとる.
- 親コントローラ側のinput要素の値を
"bar"
から"barbar"
に変更
→ 子コントローラ側のinput要素の値も"barbar"
となる - 子コントローラ側のinput要素の値を
"bar"
から"barbar"
に変更
→ 親コントローラ側のinput要素の値は"bar"
のまま → あれあれー?
ユーザの操作順序によって、$scopeの値の一致・不一致が起きるのだ。
こんな挙動となる原因は、子コントローラ側の $scope.foo
は 親スコープ側の $scope.foo
に対して、プロトタイプチェーンで解決されているため。
「プロトタイプのプロパティを変更すると、子オブジェクトのプロパティも( hasOwnProperty
が偽である限り)変更されるが、子オブジェクトのプロパティを変更しても、プロトタイプのプロパティは変更されない」というJavaScriptの性質ですわな。
var mainScope = {
foo: 'bar'
};
var childScope = Object.create(mainScope);
console.log('mainScope.foo:', mainScope.foo); // -> bar
console.log('childScope.foo:', childScope.foo); // -> bar
mainScope.foo = 'barbar';
console.log('mainScope.foo:', mainScope.foo); // -> barbar
console.log('childScope.foo:', childScope.foo); // -> barbar
childScope.foo = 'barbarbar';
console.log('mainScope.foo:', mainScope.foo); // -> barbar !
console.log('childScope.foo:', childScope.foo); // -> barbarbar
子オブジェクトが同名プロパティをセットされると同時に、プロトタイプへの参照が行われなくなるのが原因なのであれば、子オブジェクト側からのプロトタイプは参照のみにしちゃえばよいんじゃね?
var mainScope = {
obj: {foo: 'bar'}
};
var childScope = Object.create(mainScope);
childScope.obj.foo = 'barbarbar';
console.log('mainScope.obj.foo:', mainScope.obj.foo); // -> barbarbar
console.log('childScope.obj.foo:', childScope.obj.foo); // -> barbarbar
childScope.obj.foo
として、 プロトタイプの参照保持オブジェクト obj
を間に噛ますことで、常に foo
の値を共有することが出来る。
このテクニックをそのまま流用すれば、さっきのネストしたコントローラのパターンも解決できて嬉しいわけだ。
<div ng-controller="MainCtrl">
<div>
<input type="text" ng-model="obj.foo" />
</div>
<div ng-controller="ChildCtrl">
<input type="text" ng-model="obj.foo" />
</div>
</div>
function MainCtrl($scope){
$scope.obj = {foo: 'bar'};
}
function ChildCtrl($scope){
}
Alex氏が言っているのは、「ネストしたコントローラ間の値を同期させる目的だけのために、中間オブジェクト(上記でいうところの obj
)が登場するのはスマートではない」という点だ。
ここで、ようやく話を元々のController As記法に戻すことが出来る。冒頭で紹介したController As記法で書き換えると下記のようになる。
<div ng-controller="MainCtrl as main">
<div>
<input type="text" ng-model="main.foo" />
</div>
<div ng-controller="ChildCtrl as child">
<input type="text" ng-model="main.foo" />
</div>
</div>
function MainCtrl(){
this.foo = 'bar';
}
function ChildCtrl(){
}
上記のコードは、親コントローラ側/子コントローラ側のどちらのinput要素の値を変更した場合もきちんと同期して、 foo
の値が変更される。
$scope.obj
の役割を Controller As記法で指定した main
が果たしているため、プロトタイプチェーンの予期しない切断も起きないようになっているし、 $scope.obj
自体には意味がなかったが、これがコントローラオブジェクトとなることで、コーディングとしても自然である。
Alex氏はこのメリットを称して、「Controller As記法は只のSyntax Sugarではない」と主張していた訳ですね。
なお、以下は僕の個人的な意見であるし、Alex氏のブログコメント欄でも議論となっていたが、やはりController As記法自体は、Syntax Sugarであると思う。実際、 ng-controller
に asの指定が出来なかったとしても、下記のように書けばView側はまったく同じ事が実現出来るし、 ng-controller
がやっていることもこれと一緒である。
function MainCtrl($scope){
$scope.main = this;
this.foo = 'bar';
}
重要なのは、Controller As記法がSyntax Sugarであるかどうかではなく、この記法を用いた場合に、自然な形で参照を保持できる点だろう。
うん、これから使うように心がけます。
まとめ
- Controller As記法がSyntax Sugarかどうかはどうでもよい。
- Controller As記法を用いた場合、意図しない子 -> 親間のプロトタイプチェーンの切断を防ぐことができる。
補足
Alex氏の原文エントリでは、実際に親・子スコープの動きもデモで動かすことが出来る.
どうでもいいことだが、 ngRouteモジュールの
$routeProvider
でも、Controller As相当が使えます。
angular
.module('myApp', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl as main'
});
});
下記も可:
angular
.module('myApp', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl',
controllerAs: 'main'
});
});