1. ホーム
  2. ジャバスクリプト

[解決済み】AngularJSのスコーププロトタイピング/プロトタイピング継承のニュアンスとは?

2022-03-23 22:24:35

質問

その APIリファレンススコープのページ は言う。

スコープ できる は親スコープを継承する。

デベロッパーガイドのスコープページ と書かれています。

スコープは(プロトタイプ的に)親スコープからプロパティを継承します。

  • では、子スコープは常に親スコープからプロトタイプ的に継承されるのでしょうか?
  • 例外はありますか?
  • 継承する場合、常に通常のJavaScriptのプロトタイプ継承なのでしょうか?

どのように解決するのですか?

クイックアンサー :

子スコープは通常、親スコープをプロトタイプ的に継承しますが、常にそうとは限りません。 この規則の例外は、ディレクティブが scope: { ... } -- これは、プロトタイプ的に継承されないアイソレート(分離)されたスコープを作成します。 この構造は、再利用可能なコンポーネントを作成するときによく使われます。

ニュアンスとしては、スコープ継承は通常、簡単なものです。 2ウェイデータバインディング (つまり、フォーム要素、ng-model) を子スコープで使用します。 にバインドしようとすると、ng-repeat、ng-switch、および ng-include でつまずく可能性があります。 プリミティブ (例: 数値、文字列、ブーリアン) を子スコープの中から親スコープで指定することができます。 これは、多くの人が期待するような動作ではありません。 子スコープは独自のプロパティを取得し、同じ名前の親プロパティを隠したり、影をつけたりします。 回避策としては

  1. モデルの親にオブジェクトを定義し、子でそのオブジェクトのプロパティを参照します: parentObj.someProp
  2. $parent.parentScopeProperty を使用する(常に可能とは限りませんが、可能な場合は1.よりも簡単です)。
  3. 親スコープに関数を定義し、子スコープからそれを呼び出す (常に可能というわけではありません)

AngularJSの新規開発者は、しばしば以下のことに気づきません。 ng-repeat , ng-switch , ng-view , ng-includeng-if はすべて新しい子スコープを作成するため、これらのディレクティブが関与している場合に問題が発生することがよくあります。(参照 この例 を使うと、簡単に問題を説明できます)。

このプリミティブの問題は、以下のベストプラクティスに従うことで簡単に回避することができます。 ng-modelには常に'.'を入れてください。 - 3分程度をご覧ください。ミスコがプリミティブのバインディングの問題を ng-switch .

モデルの中に'.'があることで、プロトタイプ継承が確実に行われます。そのため、以下のように使用します。

<input type="text" ng-model="someObj.prop1">

<!--rather than
<input type="text" ng-model="prop1">`
-->


L-o-n-gの回答 :

JavaScript プロトタイプの継承

AngularJSのwikiにも掲載されています。 https://github.com/angular/angular.js/wiki/Understanding-Scopes

特にサーバーサイド出身でクラス的な継承に慣れている場合は、まずプロトタイピング継承をしっかり理解することが大切です。 そこで、まずはそのおさらいをしましょう。

parentScope が aString, aNumber, anArray, anObject, aFunction の各プロパティを持っているとします。 childScopeがparentScopeをプロトタイプ的に継承すると、次のようになります。

(なお、スペースを節約するため、私は anArray オブジェクトは、3つの値を持つ1つの青いオブジェクトであり、3つの別々の灰色のリテラルを持つ1つの青いオブジェクトではありません)。

parentScope で定義されたプロパティに子スコープからアクセスしようとすると、JavaScript はまず子スコープを探し、プロパティが見つからず、次に継承されたスコープを探し、プロパティが見つかります。 (親スコープでプロパティが見つからなければ、プロトタイプの連鎖を辿って......ルートスコープまでたどり着くのです)。つまり、これらはすべて真実なのです。

childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'

次にこうするとします。

childScope.aString = 'child string'

プロトタイプチェーンは参照されず、新しい aString プロパティが childScope に追加されます。 この新しいプロパティは、同じ名前の parentScope プロパティを隠したり、影をつけたりします。 これは、以下でng-repeatとng-includeについて説明するときに非常に重要になります。

次に、このようにするとします。

childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'

オブジェクト (anArray と anObject) が childScope に見つからないため、プロトタイプ チェーンが参照されます。 オブジェクトは parentScope で見つかり、プロパティ値は元のオブジェクトで更新されます。 childScope には新しいプロパティは追加されず、新しいオブジェクトも作成されません。 (JavaScript では、配列と関数もオブジェクトであることに注意してください)。

次に、このようにするとします。

childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }

プロトタイプチェーンは参照されず、子スコープは、同じ名前の parentScope オブジェクトプロパティを隠したり、シャドウしたりする 2 つの新しいオブジェクトプロパティを取得します。

テイクアウェイ

  • childScope.propertyXを読み、childScopeがpropertyXを持つ場合、プロトタイプチェーンは参照されない。
  • childScope.propertyXを設定しても、プロトタイプチェーンは参照されません。

最後にシナリオを一つ。

delete childScope.anArray
childScope.anArray[1] === 22  // true

最初に childScope プロパティを削除し、次にそのプロパティに再度アクセスしようとすると、プロトタイプチェーンが参照されます。


Angular スコープの継承

競合他社は

  • 以下は新しいスコープを作成し、プロトタイプを継承します:ng-repeat、ng-include、ng-switch、ng-controller、ディレクティブに scope: true ディレクティブは transclude: true .
  • 以下は、プロトタイプを継承しない新しいスコープを作成します: ディレクティブに scope: { ... } . これは、代わりに "isolate" スコープを作成します。

なお、デフォルトでは、ディレクティブは新しいスコープを作成しません -- つまり、デフォルトは scope: false .

ng-include

コントローラの中にあるとします。

$scope.myPrimitive = 50;
$scope.myObject    = {aNumber: 11};

そして、HTMLでは

<script type="text/ng-template" id="/tpl1.html">
<input ng-model="myPrimitive">
</script>
<div ng-include src="'/tpl1.html'"></div>

<script type="text/ng-template" id="/tpl2.html">
<input ng-model="myObject.aNumber">
</script>
<div ng-include src="'/tpl2.html'"></div>

各 ng-include は新しい子スコープを生成し、それはプロトタイプ的に親スコープから継承します。

最初の入力テキストボックスに (たとえば "77") を入力すると、子スコープに新しい myPrimitive スコーププロパティは、同じ名前の親スコーププロパティを隠したり、影をつけたりします。 これはおそらく、あなたが望んでいること/期待していることではありません。

2つ目の入力テキストボックスに(例えば、"99")と入力しても、新しい子プロパティは生成されません。 tpl2.html はモデルをオブジェクトのプロパティにバインドしているので、ngModel がオブジェクト myObject を探すときにプロトタイプ継承が働き、親スコープでそれを見つけます。

モデルをプリミティブからオブジェクトに変更しないのであれば、最初のテンプレートを$parentを使うように書き換えることができます。

<input ng-model="$parent.myPrimitive">

この入力テキストボックスに(例えば、"22")と入力しても、新しい子プロパティは生成されません。 モデルは現在、親スコープのプロパティにバインドされています ($parent は親スコープを参照する子スコープのプロパティであるため)。

プロトタイプであってもなくても、すべてのスコープについて、Angularはスコーププロパティ$parent、$$childHead、$$childTailによって、常に親子関係(つまり階層構造)を追跡しています。 通常、これらのスコーププロパティをダイアグラムに表示することはありません。

フォーム要素が関与しないシナリオでは、プリミティブを修正する関数を親スコープに定義するのも解決策のひとつです。 この関数は、プロトタイプ継承により、子スコープでも使用できるようになります。例えば

// in the parent scope
$scope.setMyPrimitive = function(value) {
     $scope.myPrimitive = value;
}

以下は サンプル フィドル この親機能を使っているのは、quot;parent function"です。 (このフィドルは、この解答の一部として書かれたものです。 https://stackoverflow.com/a/14104318/215945 .)

参照 https://stackoverflow.com/a/13782671/215945 https://github.com/angular/angular.js/issues/1267 .

ng-スイッチ

ng-switchのスコープ継承は、ng-includeと同じように動作します。 したがって、親スコープにあるプリミティブに対して双方向のデータバインディングが必要な場合は、$parent を使用するか、モデルをオブジェクトに変更してからそのオブジェクトのプロパティにバインドします。 これにより、親スコープのプロパティを子スコープが隠したり、シャドーイングしたりすることを避けることができます。

こちらもご覧ください AngularJS、switch-caseのバインドスコープ?

ng-リピート

Ng-repeatは少し違った動きをします。 例えば、コントローラに

$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

そして、HTMLでは

<ul><li ng-repeat="num in myArrayOfPrimitives">
       <input ng-model="num">
    </li>
<ul>
<ul><li ng-repeat="obj in myArrayOfObjects">
       <input ng-model="obj.num">
    </li>
<ul>

各アイテム/繰り返しについて、ng-repeat は新しいスコープを作成し、それはプロトタイプ的に親スコープから継承されます。 の新しいプロパティに割り当てます。 . (ng-repeatのAngularのソースコードは以下の通りです。

childScope = scope.$new();  // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value;  // creates a new childScope property

itemがプリミティブの場合(myArrayOfPrimitivesのように)、基本的に値のコピーが新しい子スコーププロパティに割り当てられます。 子スコーププロパティの値を変更する(つまり、ng-model を使用し、そのために子スコープ num を実行します。 ではない は、親スコープが参照する配列を変更します。 そのため、上記の最初の ng-repeat では、各子スコープに num プロパティは、myArrayOfPrimitives 配列から独立しています。

<イグ

このng-repeatは(あなたが望む/期待するように)機能しません。 テキストボックスに入力すると、グレーのボックスの値が変更されますが、これは子スコープにのみ表示されます。 私たちが望むのは、入力が子スコープのプリミティブプロパティではなく、myArrayOfPrimitivesの配列に影響を与えることなのです。 これを実現するためには、モデルをオブジェクトの配列に変更する必要があります。

つまり、itemがオブジェクトの場合、元のオブジェクトへの参照(コピーではない)が新しい子スコーププロパティに代入されるわけです。 子スコーププロパティの値を変更する(つまり、ng-model を使用する、したがって obj.num ) する は、親スコープが参照しているオブジェクトを変更します。 つまり、上記の2番目のng-repeatでは、次のようになります。

(行き先がはっきりするように、1行だけグレーに着色しました)

これは期待通りに動作します。 テキストボックスに入力すると、グレーのボックスの値が変更され、子スコープと親スコープの両方から見えるようになります。

参照 ng-model、ng-repeat、およびinputsの難しさ https://stackoverflow.com/a/13782671/215945

ng-controller

ng-controllerを使用したコントローラのネストは、ng-includeやng-switchと同様に通常のプロトタイプ継承となり、同じテクニックが適用されます。 しかし、2つのコントローラが$scope継承で情報を共有するのは悪い形式とされています。 http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ コントローラ間のデータ共有には、サービスを使用する必要があります。

(どうしてもコントローラ・スコープの継承でデータを共有したい場合は、何もする必要はありません。 子スコープは、親スコープのすべてのプロパティにアクセスできるようになります。 以下もご参照ください。 ロード時とナビゲート時でコントローラのロード順が異なる )

ディレクティブ

  1. デフォルト( scope: false ) - このディレクティブは新しいスコープを作成しないので、ここでは継承はありません。 これは簡単ですが、危険でもあります。例えば、ディレクティブはスコープに新しいプロパティを作成したつもりが、実際には既存のプロパティを破壊している可能性があるからです。 これは、再利用可能なコンポーネントとしてのディレクティブを書くには、良い選択ではありません。
  2. scope: true - ディレクティブは、親スコープをプロトタイプ的に継承した新しい子スコープを作成します。 複数のディレクティブが (同じ DOM 要素に対して) 新しいスコープを要求した場合、新しい子スコープが作られるのはひとつだけです。 プロトタイプの継承があるので、これは ng-include や ng-switch のようなもので、親スコーププリミティブへの2方向のデータバインディングや、親スコーププロパティの子スコープによる非表示やシャドウに注意する必要があります。
  3. scope: { ... } - ディレクティブは、新しい分離/隔離されたスコープを作成します。 これはプロトタイプ的には継承されません。 ディレクティブが誤って親スコープを読んだり変更したりすることがないため、再利用可能なコンポーネントを作成する場合には、通常、これが最良の選択となります。 しかし、このようなディレクティブは、しばしばいくつかの親スコープのプロパティにアクセスする必要があります。 オブジェクトハッシュは、親スコープと分離スコープの間の双方向バインディング('='を使用)または一方向バインディング('@'を使用)を設定するために使用されます。 また、親スコープ式にバインドするための '&' もあります。 つまり、これらはすべて親スコープから派生したローカルスコープのプロパティを作成します。 オブジェクトハッシュで親スコープのプロパティ名を参照するだけではだめで、アトリビュートを使用しなければならないのです。 たとえば、親プロパティにバインドしたい場合は、次のようにします。 parentProp を孤立したスコープで表示します。 <div my-directive>scope: { localProp: '@parentProp' } . ディレクティブがバインドしたいそれぞれの親プロパティを指定するために、属性を使用しなければなりません。 <div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' } .

    スコープを分離する __proto__ はObjectを参照しています。 孤立したスコープの $parent は親スコープを参照しているので、孤立していて親スコープからプロトタイプ的に継承していなくても、子スコープであることに変わりはありません。

    下の図では

    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> そして

    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }

    また、ディレクティブがリンク機能でこのようなことをすると仮定します。 scope.someIsolateProp = "I'm isolated"



    アイソレートスコープの詳細については、以下を参照してください。 http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true - このディレクティブは、新しい "transcluded" の子スコープを作成し、プロトタイプ的に親スコープから継承します。 transcluded と分離されたスコープ (もしあれば) は兄弟です -- それぞれのスコープの $parent プロパティは、同じ親スコープを参照しています。 トランスクルーデッドとアイソレートスコープが両方存在する場合、アイソレートスコープのプロパティ $$nextSibling は、トランスクルーデッドスコープを参照します。 transcluded スコープに関するニュアンスの違いはわかりません。

    下の図は、上記と同じディレクティブを想定し、このように追加したものです。 transclude: true

この フィドル には showScope() 関数は、分離されたスコープと包含されたスコープを調べるために使用することができます。 fiddleのコメントにある説明を参照してください。


概要

スコープには4つのタイプがあります。

  1. 通常のプロトタイプのスコープ継承 -- ng-include、ng-switch、ng-controller、ディレクティブを使用した scope: true
  2. コピー/割り当てを伴う通常のプロトタイプのスコープ継承 -- ng-repeat。 ng-repeat の各反復は新しい子スコープを作成し、その新しい子スコープは常に新しいプロパティを取得します。
  3. スコープを分離する -- ディレクティブで scope: {...} . こちらはプロトタイプではありませんが、'='、'@'、'&'は、属性を通じて、親スコープのプロパティにアクセスするメカニズムを提供します。
  4. 被包括スコープ -- ディレクティブに transclude: true . こちらも通常のプロトタイプのスコープ継承ですが、任意のアイソレートスコープの兄弟になります。

プロトタイプであろうとなかろうと、すべてのスコープについて、Angularは常に親子関係(つまり階層)を追跡し、$parentと$$childHeadと$$childTailプロパティを通じて、親子関係を追跡します。

ダイアグラムは グラフビズ にある "*.dot" ファイル。 ギズーブ . ティム・キャスウェルの" オブジェクトグラフで学ぶJavaScript GraphVizを図に使用するきっかけとなったのは、"です。