ばかおもちゃ本店:Youtube twitter:@sashimizakana Amazon.co.jpアソシエイト

2013年12月21日土曜日

AngularJSのscopeの継承とかを理解する

まえおき

本稿はAngularJS Startup AdventCalendar2013の21日目です。
20日目:AngularJSをTypeScriptで書くときのあれこれ - Qiita [キータ]

scopeについて

scopeはAngularJSでビュー側に値を渡すときに利用するサービス。
たぶん知ってると思う。これがビューとコントローラーがデータをやりとりする。
で、今回はちょっとAngularJSをさわり始めると公式ドキュメントなんかあちこちに出てくる、新しいscopeが作られるっていう部分について書く。
ディレクティブなんかのAPIを読んでると出てくる。このディレクティブは新しいscopeを作りますって、あれってなんなのか。

scopeの生成と継承

scopeは一番上位に位置するrootScopeの下に階層化されて作られていく。
scopeはJavaScriptのレキシカルスコープみたいにブロックを持ち、親子関係を持つ。
なのだけど、少しだけjavaScriptのスコープとは違う部分がある。ここがちょっとだけわかりにくい。

一般的にディレクティブを作るときにscopeを作ることが多いので、それを例に取って説明する。
ディレクティブの作成時にscopeの生成には、false(作らない:デフォルト)、true(作る)、オブジェクト(親を継承せず作る)ってのがある。
一応比較対象としてfalseの作らないやつからfiddleに例を作った。

AngularJS Scope-1 - JSFiddle

これは見たまんまでディレクティブ内でいきなり親スコープのプロパティを読んでるけど、問題なく表示されているし更新も出来る。念のために書いておくと、これはサンプルなのでそういうことをしているけど、普通にこんなことをするとモジュール間の結合度が上がりすぎて最終的に動かないプログラムを作ることになるのでやってはいけない。
ダメ絶対。

普通にやる場合はlinkの第三引数(attrs)からタグに設定された属性の中身を読みとってscope.$watchで受け取ったりする。
なんでそう書いてないのかというとコードを短くしたいってことと、この後のサンプルの意味が分かりづらくなるから。

というわけで、trueを設定した場合。

AngularJS Scope-2 - JSFiddle

trueとfalseが変わっただけだけど、子+を押すと親+は変更されず、以後はまったく独立してカウントされるようになる(なので先に親+から押してください)。これがAngularJSのscopeがJavaScriptのスコープと違う部分で、つまり親を継承して子スコープを作った場合は、親の値は参照可能だけど上書きすることはできないということ。
上書きしようとすると変数名などはそのままに、別の値として作成されてしまう。
これを知らないで居ると全く意味不明な挙動に見えるときがある。

たとえばng-includeなどでもこんな風にscopeは作成される。そのため、たとえば親のscopeの値をそのまま利用して、かつng-include内部でちょこっと数値を変えるようなものを書いてしまうと、親の方で値を操作しているうちは良いけど、ng-include以下で操作すると急に親から値が読めないなどということが起きたりする。

で、最後がscopeにobjectを指定した場合。

AngularJS Scope-3 - JSFiddle

これは親を継承しないので、親の値は全く参照することが出来ない。
つまり動きません。お使いのPCは正常です。

ただこのobjectに親から継承したいものを設定することができる。ここではすべては詳しく書かないけど、親のscopeの変数を変更可能な状態で受け取ることとか、子のDOM内にのみ入れることとか、親のcontextで実行させるとか(ng-click="hoge()"みたいなやつのこと)、ができる。

値を受け取る場合はプロパティ名が子scopeでプロパティ名にしたい名前にして、その値を"=自分のDOMの属性名"にする。つまり<div hoge="***">みたいな場合、scope:{myValue:"=hoge"}というようなこと。
ちなみに子scope内で属性名と同じ名前を使うときは属性名は省略可能。
{hoge:"="}ってな感じ。

いちおうサンプル。

AngularJS Scope-4 - JSFiddle

まとめ

具体的にどういうときに使うんだって言う例を作っておきたかったけど、時間がなかった。実際にバリバリ書いてると絶対に分かってないとダメな局面があったはずなんだけど、なかなかちゃんと再現するコードを書くのはむずかしい。
でもjQuery拡張とかをディレクティブに放り込んでAngularJS対応にしてたりとか、複数のディレクティブをタグに付けまくったりしたりすると、なんか値が上手く変わらんとか思うことがあった。でもこのあたりを理解してからは問題なくなった。

scope作るときに実際どれ使えば良いかっていうので言うと、普通に使うだけならオブジェクトを渡して必要なプロパティだけ共有するのがおすすめ。安全だしミスを起こさないので良いと思う。

余談

$watchで値を受け取るとき、受け取った値がオブジェクトとか配列とかだと参照渡しされているのでscope:trueに設定してあってもそのオブジェクトのプロパティへの変更とかは通常通り行われて親scope側の値に影響する。
まあでも関数の引数と同じ。