AngularJSのスコープのプロトタイプ/プロトタイプの継承のニュアンスは何ですか?


1028

APIリファレンスScopeページは言います:

スコープ親スコープから継承できます。

開発者ガイドScopeページは言います:

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

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

回答:


1741

簡単な答え
子スコープは通常、親スコープからプロトタイプを継承しますが、常に継承するわけではありません。このルールの1つの例外は、を伴うディレクティブです。scope: { ... }これにより、プロトタイプでは継承されない「分離」スコープが作成されます。この構成は、「再利用可能なコンポーネント」ディレクティブを作成するときによく使用されます。

ニュアンスについては、スコープの継承は通常、子スコープで双方向のデータバインディング(つまり、フォーム要素、ng-model)が必要になるまで、単純明快です。子スコープの内側から親スコープのプリミティブ(たとえば、数値、文字列、ブール値)にバインドしようとすると、ng-repeat、ng-switch、およびng-includeがトリップする可能性があります。ほとんどの人が期待するようには動作しません。子スコープは、同じ名前の親プロパティを非表示/シャドウにする独自のプロパティを取得します。あなたの回避策は

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

新AngularJSの開発者は、多くの場合、それを実現していないng-repeatng-switchng-viewng-include及びng-ifこれらのディレクティブが関与しているときに問題が頻繁に現れるので、すべての新しい子のスコープを作成します。(問題の簡単な説明については、この例を参照してください。)

プリミティブに関するこの問題は、常に「。」を持つ「ベストプラクティス」に従うことで簡単に回避できますng-modelsで – 3分の価値を見てください。Miskoは、での基本的なバインディングの問題を示していng-switchます。

「。」モデル内で、プロトタイプの継承が機能していることを確認します。だから、使用

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

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


長い答え

JavaScriptプロトタイプの継承

AngularJS wikiにも配置されています: https : //github.com/angular/angular.js/wiki/Understanding-Scopes

最初にプロトタイプの継承をしっかりと理解することが重要です。サーバーサイドのバックグラウンドから来ており、クラスの継承に慣れている場合は特にそうです。それでは、最初にそれを確認しましょう。

parentScopeにプロパティaString、aNumber、anArray、anObject、およびaFunctionがあるとします。childScopeが親スコープからプロトタイプを継承する場合、次のようになります。

プロトタイプの継承

(スペースを節約するために、anArrayオブジェクトを3つの個別の灰色のリテラルを持つ単一の青いオブジェクトではなく、3つの値を持つ単一の青いオブジェクトとして表示していることに注意してください。)

parentScopeで定義されたプロパティに子スコープからアクセスしようとすると、JavaScriptはまずプロパティを見つけるのではなく子スコープを調べ、次に継承されたスコープを調べてプロパティを見つけます。(parentScopeでプロパティが見つからなかった場合は、プロトタイプチェーンまで続きます...ルートスコープまで続きます)。したがって、これらはすべて真実です。

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プロパティを削除してから、そのプロパティに再度アクセスしようとすると、プロトタイプチェーンが参照されます。

子プロパティを削除した後


角度スコープの継承

候補者:

  • 次は新しいスコープを作成し、プロトタイプを継承します:ng-repeat、ng-include、ng-switch、ng-controller、withwithディレクティブscope: true、withwithディレクティブtransclude: true
  • 次は、プロトタイプを継承しない新しいスコープを作成しscope: { ... }ます。これにより、代わりに「分離」スコープが作成されます。

デフォルトでは、ディレクティブは新しいスコープを作成しないことに注意してください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は、新しい子スコープを生成します。これは、典型的には親スコープから継承します。

ng-include子スコープ

最初の入力テキストボックスに入力(たとえば「77」)すると、子スコープmyPrimitiveは、同じ名前の親スコーププロパティを非表示/シャドウにする新しいスコーププロパティを取得します。これはおそらくあなたが望む/期待するものではありません。

プリミティブ付きのng-include

2番目の入力テキストボックスに入力(たとえば、「99」)しても、新しい子プロパティは生成されません。tpl2.htmlはモデルをオブジェクトプロパティにバインドするため、ngModelがオブジェクトmyObjectを検索すると、プロトタイプの継承が開始され、親スコープでそれが検出されます。

オブジェクトを含むng-include

モデルをプリミティブからオブジェクトに変更したくない場合は、$ parentを使用するように最初のテンプレートを書き直すことができます。

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

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

$ parentを含むng-include

Angularは、すべてのスコープ(プロトタイプかどうかに関係なく)のスコーププロパティ$ parent、$$ childHead、および$$ childTailを介して、常に親子関係(つまり、階層)を追跡します。通常、これらのスコーププロパティは図に表示しません。

フォーム要素が関係しないシナリオの場合、別の解決策は、親スコープに関数を定義してプリミティブを変更することです。次に、子が常にこの関数を呼び出すことを確認します。この関数は、プロトタイプの継承により子スコープで使用できます。例えば、

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

この「親関数」アプローチを使用するフィドル例を次に示します。(フィドルはこの回答の一部として書かれました:https : //stackoverflow.com/a/14104318/215945

https://stackoverflow.com/a/13782671/215945およびhttps://github.com/angular/angular.js/issues/1267参照してください

ng-switch

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

AngularJSも参照してください、switch-caseのスコープをバインドしますか?

ng-repeat

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

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

プリミティブを使用したng-repeat

このng-repeatは機能しません(望みどおりに)。テキストボックスに入力すると、灰色のボックスの値が変更されます。これは、子スコープでのみ表示されます。ここで必要なのは、入力が子スコープのプリミティブプロパティではなく、myArrayOfPrimitives配列に影響を与えることです。これを行うには、モデルをオブジェクトの配列に変更する必要があります。

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

オブジェクトのng-repeat

(どこに行くのか明確にするために、1本の線を灰色に着色しました。)

これは期待どおりに機能します。テキストボックスに入力すると、灰色のボックスの値が変更され、子スコープと親スコープの両方に表示されます。

参照してくださいNG-モデルの難しさ、NGリピート、および入力をして 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要素上の)複数のディレクティブが新しい​​スコープを要求する場合、新しい子スコープは1つだけ作成されます。「通常の」プロトタイプ継承があるため、これはng-includeやng-switchに似ているため、親スコーププリミティブへの双方向データバインディング、および親スコーププロパティの子スコープ非表示/シャドウイングに注意してください。
  3. scope: { ... }-ディレクティブは新しい分離/分離スコープを作成します。それはプロトタイプ的に継承しません。ディレクティブが誤って親スコープを読み取ったり変更したりすることがないため、これは通常、再利用可能なコンポーネントを作成するときに最良の選択です。ただし、このようなディレクティブは多くの場合、いくつかの親スコーププロパティにアクセスする必要があります。オブジェクトハッシュは、親スコープと分離スコープ間の双方向バインディング( '='を使用)または一方向バインディング( '@'を使用)を設定するために使用されます。親スコープ式にバインドする '&'もあります。したがって、これらはすべて、親スコープから派生したローカルスコーププロパティを作成します。属性はバインディングのセットアップを支援するために使用されることに注意してください。オブジェクトハッシュで親スコープのプロパティ名を参照するだけではなく、属性を使用する必要があります。たとえば、親プロパティにバインドする場合、これは機能しませんparentProp分離スコープ内:<div my-directive>およびscope: { localProp: '@parentProp' }。ディレクティブがバインドする各親プロパティを指定するには、属性を使用する必要があります:<div my-directive the-Parent-Prop=parentProp>およびscope: { localProp: '@theParentProp' }
    スコープの__proto__参照オブジェクトを分離します。分離スコープの$ 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-ディレクティブは、新しい「変換された」子スコープを作成します。子スコープは、典型的には親スコープから継承します。トランスクルードされたスコープと分離されたスコープ(存在する場合)は兄弟です。各スコープの$ parentプロパティは同じ親スコープを参照します。トランスクルードスコープと分離スコープの両方が存在する場合、分離スコーププロパティ$$ nextSiblingはトランスクルードスコープを参照します。トランスクルードされたスコープのニュアンスは知りません。
    以下の図では、上記と同じディレクティブを追加したものとします。transclude: true
    隠されたスコープ

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


概要

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

  1. 通常のプロトタイプのスコープ継承-ng-include、ng-switch、ng-controller、ディレクティブ scope: true
  2. コピー/割り当てによる通常のプロトタイプのスコープ継承-ng-repeat。ng-repeatを繰り返すたびに新しい子スコープが作成され、その新しい子スコープは常に新しいプロパティを取得します。
  3. スコープを分離する-ディレクティブscope: {...}。これはプロトタイプではありませんが、「=」、「@」、および「&」は、属性を介して親スコープのプロパティにアクセスするためのメカニズムを提供します。
  4. トランスクルードされたスコープ-ディレクティブtransclude: true。これも通常のプロトタイプスコープの継承ですが、分離スコープの兄弟でもあります。

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

図は githubにある「* .dot」ファイル。Tim Caswellの「オブジェクトグラフを使用したJavaScriptの学習」は、図にGraphVizを使用するきっかけになりました。


48
素晴らしい記事です。SOの回答には長すぎますが、とにかく非常に便利です。編集者がそれをサイズに切り詰める前に、あなたのブログにそれを置いてください。
iwein

43
AngularJS wikiにコピーを置きました。
Mark Rajcok

3
修正:「スコープの__proto__参照オブジェクトを分離する」。代わりに「スコープの__proto__参照を分離するScopeオブジェクト」である必要があります。したがって、最後の2つの画像では、オレンジ色の「オブジェクト」ボックスは「スコープ」ボックスである必要があります。
Mark Rajcok 2013年

15
このasnwerはangularjsガイドに含まれるべきです。これははるかに教訓的です...
Marcelo De Zen

2
wikiは私を困惑させます、最初にそれを読みます:「オブジェクトがchildScopeに見つからないのでプロトタイプチェーンが調べられます。」「childScope.propertyXを設定すると、プロトタイプチェーンは参照されません。」2番目のものは状態を意味しますが、最初のものはそうではありません。
ステファン2014

140

私はマークの答えと決して競合したくありませんが、Javascriptの継承とそのプロトタイプチェーンの初心者としてすべてが最後にすべてをクリックするようにした作品を強調したかっただけです

プロパティの読み取りのみがプロトタイプチェーンを検索し、書き込みは行いません。だからあなたが設定したとき

myObject.prop = '123';

チェーンを調べませんが、設定すると

myObject.myThing.prop = '123';

プロップに書き込む前にmyThingをルックアップしようとする、その書き込み操作内で行われている微妙な読み取りがあります。そのため、子からobject.propertiesへの書き込みが親のオブジェクトに到達します。


12
これは非常に単純な概念ですが、多くの人が見落としているため、あまり明白ではないかもしれません。よく置きます。
moljac024 2014年

3
素晴らしい発言。取り上げますが、非オブジェクトプロパティの解決には読み取りは含まれませんが、オブジェクトプロパティの解決には含まれます。
ステファン2014

1
どうして?プロパティの書き込みがプロトタイプチェーンを上らない理由は何ですか?クレイジーに見える...
ジョナサン。

1
本当に簡単な例を追加すれば素晴らしいでしょう。
tylik 2014年

2
それがあることに注意してくださいないため、プロトタイプチェーンを検索セッター。何も見つからない場合は、レシーバーにプロパティを作成します。
ベルギ2017年

21

@Scott Driscollの回答に、JavaScriptを使用したプロトタイプの継承の例を追加したいと思います。EcmaScript 5仕様の一部であるObject.create()で従来の継承パターンを使用します。

まず、「親」オブジェクト関数を作成します

function Parent(){

}

次に、「親」オブジェクト関数にプロトタイプを追加します

 Parent.prototype = {
 primitive : 1,
 object : {
    one : 1
   }
}

「子」オブジェクト関数を作成する

function Child(){

}

子プロトタイプの割り当て(子プロトタイプに親プロトタイプを継承させる)

Child.prototype = Object.create(Parent.prototype);

適切な「子」プロトタイプコンストラクターを割り当てる

Child.prototype.constructor = Child;

メソッド "changeProps"を子プロトタイプに追加します。これにより、子オブジェクトの "primitive"プロパティ値が書き換えられ、子オブジェクトと親オブジェクトの両方で "object.one"値が変更されます

Child.prototype.changeProps = function(){
    this.primitive = 2;
    this.object.one = 2;
};

親(父)および子(息子)オブジェクトを開始します。

var dad = new Parent();
var son = new Child();

子(息子)のchangePropsメソッドを呼び出す

son.changeProps();

結果を確認してください。

親プリミティブプロパティは変更されませんでした

console.log(dad.primitive); /* 1 */

子プリミティブプロパティが変更されました(書き換えられました)

console.log(son.primitive); /* 2 */

親と子のobject.oneプロパティが変更されました

console.log(dad.object.one); /* 2 */
console.log(son.object.one); /* 2 */

ここで作業例http://jsbin.com/xexurukiso/1/edit/

Object.createの詳細については、https: //developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/createをご覧ください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.