javascript - prototype原型 - AngularJS中範圍原型/原型繼承的細微差別是什麼?




prototypal inheritance javascript (2)

快速回答
子範圍通常從其父範圍通常原型繼承,但並非總是如此。 此規則的一個例外是一個指令,其scope: { ... } - 這會創建一個不會原型繼承的“隔離”範圍。 創建“可重用組件”指令時經常使用此構造。

至於細微差別,範圍繼承通常是直截了當的......直到您需要在子範圍內使用雙向數據綁定 (即表單元素,ng-model)。 如果嘗試在子範圍內綁定到父範圍中的基本元素 (例如,數字,字符串,布爾值),則Ng-repeat,ng-switch和ng-include可能會讓您失望。 它不能像大多數人所期望的那樣工作。 子作用域獲取自己的屬性,該屬性隱藏/遮蔽相同名稱的父屬性。 你的解決方法是

  1. 在模型的父級中定義對象,然後在子級中引用該對象的屬性:parentObj.someProp
  2. 使用$ parent.parentScopeProperty(並不總是可能的,但在可能的情況下比1更容易)
  3. 在父範圍上定義一個函數,並從子項調用它(並不總是可能的)

新的AngularJS開發人員通常並不知道ng-repeatng-switchng-viewng-includeng-if全部都創建新的子範圍,所以當涉及這些指令時,問題經常出現。 (請參閱此示例以便快速說明問題。)

通過遵循總是有'。'的“最佳實踐”,可以很容易地避免這個與原語有關的問題 在你的ng模型中 - 觀看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 https://github.com/angular/angular.js/wiki/Understanding-Scopes

首先要對原型繼承有一個很好的理解,尤其是當你來自服務器端的背景,並且更熟悉類繼承時,這一點很重要。 所以我們先來回顧一下。

假設parentScope具有屬性aString,aNumber,anArray,anObject和aFunction。 如果childScope原型繼承自parentScope,我們有:

(請注意,為了節省空間,我將anArray對象顯示為具有三個值的單個藍色對象,而不是具有三個獨立灰色文字的單個藍色對象。)

如果我們嘗試從子範圍訪問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'

因為在childScope中找不到對象(anArray和anObject),所以查閱原型鏈。 這些對象位於parentScope中,並且在原始對像上更新了屬性值。 沒有新的屬性添加到childScope; 沒有新的對像被創建。 (請注意,JavaScript中的數組和函數也是對象。)

假設我們這樣做:

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

未查詢原型鏈,子範圍獲取兩個新的對象屬性,用於隱藏/隱藏具有相同名稱的parentScope對象屬性。

小貼士:

  • 如果我們讀取childScope.propertyX,並且childScope具有propertyX,則不會諮詢原型鏈。
  • 如果我們設置了childScope.propertyX,則不會諮詢原型鏈。

最後一個場景:

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

我們先刪除了childScope屬性,然後當我們再次訪問屬性時,查詢了原型鏈。

角度範圍的繼承

競爭者:

  • 下面創建新的範圍,並繼承原型:ng-repeat,ng-include,ng-switch,ng-controller,指令的scope: true ,指令的transclude: true
  • 以下內容將創建一個不會繼承原型的新範圍: scope: { ... }指令。 這會創建一個“隔離”範圍。

請注意,默認情況下,指令不會創建新的作用域 - 即默認為scope: false

NG-包括

假設我們在我們的控制器中有:

$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作用域屬性,該屬性隱藏/隱藏同名的父作用域屬性。 這可能不是你想要的/期望的。

在第二個輸入文本框中鍵入(例如“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;
}

這是一個使用這種“父功能”方法的示例小提琴 。 (小提琴是作為這個答案的一部分寫的: https://stackoverflow.com/a/14104318/215945 : 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-重複

吳重複工作有點不同。 假設我們在我們的控制器中有:

$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中,每個子範圍都獲得一個獨立於myArrayOfPrimitives數組的num屬性:

這個ng-repeat不起作用(就像你想要的那樣)。 鍵入文本框會更改灰色框中的值,這些值僅在子範圍中可見。 我們想要的是輸入影響myArrayOfPrimitives數組,而不是子作用域原始屬性。 為了實現這一點,我們需要將模型改為對像數組。

因此,如果item是一個對象,則將對原始對象(而不是副本)的引用分配給新的子範圍屬性。 更改子範圍屬性的值(即,使用ng-model,因此obj.num確實會更改父範圍引用的對象。 所以在上面的第二個ng-repeat中,我們有:

(我把一條線弄成灰色,這樣就清楚了它要去的地方。)

這按預期工作。 輸入文本框會更改灰色框中的值,這些灰色框對於子範圍和父範圍都是可見的。

另請參見ng-model,ng-repeat和輸入的難點和https://stackoverflow.com/a/13782671/215945

NG-控制器

與ng-include和ng-switch一樣,使用ng-controller的嵌套控制器也會產生正常的原型繼承,所以同樣的技術也適用。 然而,“它被認為是兩個控制器通過$ scope繼承共享信息的糟糕形式” - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/應該使用服務來共享數據控制器。

(如果你真的想通過控制器作用域繼承來共享數據,你就不需要做任何事情,子作用域將可以訪問所有的父作用域屬性,參見當加載或導航時控制器的加載順序不同

指令

  1. 默認( scope: false ) - 指令不會創建新的範圍,所以在這裡沒有繼承。 這很容易,但也很危險,因為例如一條指令可能認為它在範圍上創建了一個新屬性,而實際上它正在破壞一個現有屬性。 這對編寫用作可重用組件的指令不是一個好的選擇。
  2. scope: true - 該指令創建一個新的子範圍,該範圍從父範圍原型繼承。 如果多個指令(在同一個DOM元素上)請求一個新的作用域,則只會創建一個新的子作用域。 由於我們有“正常”的原型繼承,這就像ng-include和ng-switch一樣,所以請謹慎對待父範圍基元的雙向數據綁定以及父範圍屬性的子範圍隱藏/遮蔽。
  3. scope: { ... } - 該指令創建一個新的隔離/隔離範圍。 它不是原型繼承。 這通常是創建可重用組件時的最佳選擇,因為該指令不會意外讀取或修改父範圍。 但是,這樣的指令通常需要訪問幾個父範圍屬性。 對象散列用於設置父範圍和隔離範圍之間的雙向綁定(使用'=')或單向綁定(使用'@')。 還有'&'綁定到父範圍表達式。 所以,這些都創建派生自父範圍的本地範圍屬性。 請注意,屬性用於幫助設置綁定 - 您不能僅引用對象散列中的父級範圍屬性名稱,您必須使用屬性。 例如,如果要綁定到獨立作用域中的父屬性parentProp ,則這parentProp<div my-directive>scope: { localProp: '@parentProp' } 。 必須使用屬性來指定指令要綁定到的每個父屬性: <div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' }
    隔離作用域的__proto__引用對象。 隔離範圍的$父級引用父級範圍,因此雖然它是孤立的,並且不會從父級範圍原型繼承,但它仍然是子級範圍。
    對於下面的圖片我們有
    <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屬性引用相同的父範圍。 當transcluded和isolate隔離區都存在時,隔離範圍屬性$$ nextSibling將引用transcluded作用域。 我不知道任何與transcluded範圍的細微差別。
    對於下面的圖片,假設與上述相同的指令: transclude: true

這個fiddle有一個showScope()函數,可以用來檢查一個孤立和transcluded作用域。 請參閱小提琴中的評論中的說明。

概要

有四種類型的範圍:

  1. 正常原型範圍繼承 - ng-include,ng-switch,ng-controller, scope: true指令
  2. 正常的原型範圍繼承與復制/分配 - ng重複。 ng-repeat的每次迭代都會創建一個新的子作用域,並且該新的子作用域始終會獲得一個新屬性。
  3. 隔離範圍 - 指令, scope: {...} 。 這不是原型,但是'=','@'和'&'提供了一種通過屬性訪問父範圍屬性的機制。
  4. transcluded作用域 - 指令transclude: true 。 這也是正常的原型範圍繼承,但它也是任何隔離範圍的兄弟。

對於所有範圍(原型或非原型),Angular始終通過屬性$ parent和$$ childHead和$$ childTail跟踪父子關係(即層次結構)。

圖表是用github上的graphviz “* .dot”文件生成的。 Tim Caswell的“用對像圖學習JavaScript ”是使用GraphViz進行圖表的靈感。

API參考範圍頁面顯示

範圍可以從父範圍繼承。

開發人員指南範圍頁面說:

範圍(原型)從其父範圍繼承屬性。

那麼,子範圍是否總是從其父範圍原型繼承? 有例外嗎? 當它繼承時,它是否總是正常的JavaScript原型繼承?


我想添加一個使用javascript到@Scott Driscoll答案的原型繼承的例子。 我們將使用Object.create()中的經典繼承模式,這是EcmaScript 5規範的一部分。

首先我們創建“Parent”對象函數

function Parent(){

}

然後將一個原型添加到“Parent”對象函數中

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

創建“兒童”對像功能

function Child(){

}

分配子原型(使原型繼承父子原型)

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

指定適當的“Child”原型構造函數

Child.prototype.constructor = Child;

將方法“changeProps”添加到子原型,它將在Child對像中重寫“primitive”屬性值並在Child和Parent對像中更改“object.one”值

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

啟動父(父)和子(兒)對象。

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

調用Child(兒子)changeProps方法

son.changeProps();

檢查結果。

父原始屬性沒有改變

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

子元素屬性已更改(重寫)

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

父對象和子對象的屬性已更改

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





prototypal-inheritance