【AngularJS】スコープの継承で地味にハマりがちなこと

More than 1 year has passed since last update.

これはAngularJS Advent Calendar 2014の23日目の記事です。
前回はcan_i_do_webさんのAngularJS 2.0 のご様子というお話でした。
先日出てきたこの話……。ツラタン。

気を取り直して。

私の本日のネタは、表題の通り。
AngularJSに慣れ親しんだ方々には当たり前の内容かもしれませんが、そうではない人には「なんだこれ!?」的な話になるかと思います。

それではスタート。

スコープの継承

コントローラーが階層化された際には、子スコープは親スコープを継承して派生します。

<div ng-controller="hogeCtrl">
  <p>{{text}}</p>
  <div ng-controller="fugaCtrl">
    <p>{{text}}</p>
  </div>
</div>
hogeCtrl = [
  '$scope',
  function($scope){
    $scope.text = 'Lorem ipsum';
  }
];

この場合、どちらの{{text}}もLorem ipsumが表示されます。

ここで、hogeCtrl直下に$scope.textを書き換えるボタンを追加してみましょう。

<div ng-controller="hogeCtrl">
  <p>{{text}}</p>
  <button ng-click="text='いろはにほへと'">button1</button>
  <div ng-controller="fugaCtrl">
    <p>{{text}}</p>
  </div>
</div>

このボタンを押すと、どちらの{{text}}もいろはにほへとに変わります。

ここまではおk?

では、今度はfugaCtrlの下に$scope.textを書き換えるボタンを追加してみましょう。

<div ng-controller="hogeCtrl">
  <p>{{text}}</p>
  <button ng-click="text='いろはにほへと'">button1</button>
  <div ng-controller="fugaCtrl">
    <p>{{text}}</p>
   <button ng-click="text='いろはにほへと'">button2</button>
  </div>
</div>

この、新しく追加されたボタンを押すと 下の{{text}}はいろはにほへとに変わりますが、上の{{text}}はLorem ipsumのママ です。

実際に動くコードはこちら

ここであなたはこう思うでしょう。

「親のスコープの値の変更は子に影響するけど、子のスコープの値の変更は親のスコープの値には影響しないんだ!!」

さて、次に。

さっきのは忘れて、次にこのコードを見てください。

<div ng-controller="hogeCtrl">
  <p>{{values.text}}</p>
  <button ng-click="values.text='いろはにほへと'">button1</button>
  <div ng-controller="fugaCtrl">
    <p>{{values.text}}</p>
   <button ng-click="values.text='いろはにほへと'">button2</button>
  </div>
</div>
hogeCtrl = [
  '$scope',
  function($scope){
    $scope.values = {
        text: 'Lorem ipsum'
    };
  }
];

さきほどとの違いは、{{text}}が{{values.text}}のように連想配列のvalueになっている点ですね。

この状態で、上のhogeCtrl直下のボタンを押すと、先ほどのように両方ともにいろはにほへとに変更されます。

そして、下のfugaCtrl直下のボタンを押すと、さっきの例と同じく親のスコープの値は変更されないで、下の{{values.text}}だけ変更され、、、

!!!!!?????

実際のコードはこちらにあります。

一度触ってみてください。

結果としては、 親のスコープの値も変更されてしまします

なんなの?これ?

結論から言うと、 この継承には、Javascriptのプロトタイプ継承が使われている ことがこの挙動の原因となっています。

最初の例と次の例の違いは何か。
対象となっている値が、 プリミティブ型(数値、文字列など)かオブジェクト型ということです。

プロトタイプ継承では、これらの継承では違う挙動を示します。
それが同じくこの、スコープの親子で発生している、というわけです。

プロトタイプ継承についてはここでは詳しくは説明しませんが、とりあえずこの「 プリミティブ型とオブジェクト型とで継承時の挙動が変わる 」ことだけは覚えておいたほうが良いです。ハマります。 私も昔ハマりました

同じ話で、JavaScript Advent Calendar 2014のこちらの記事のような話もありますね。

本件とは直接は関係ないですが、ng-ifが独立スコープを作るみたいな話もあったり、このあたりは地味にハマりどころですな。

対策法

$parent.textみたいに明示的に親スコープを参照するか、2つ目の例みたいにオブジェクトにしてしまうのが早いかと思います。
大規模な開発な場合はおとなしく$emit$broadcast$onを使いましょう。

おまけ

こうやって祝日に律儀にAdvent Calendarに投稿していますが、本日は地味に私の誕生日だったりします。

ということで → Amazonウィッシュリスト

管理画面 Advent Calendarに私が本日投稿した記事、管理画面 × AngularJSも良かったら読んであげてくださいー。

おまけのおまけ。うちの猫 Advent Calendar 2014にも投稿してます。

明日の投稿は……

IMAIMIAMIさんのこちらの記事ですー。
クリスマスだし爆速でMEAN stackのWebサービスを作りながら入門する。