提问者:小点点

AngularJS中的作用域原型/原型继承有什么细微差别?


API Reference Scope页面显示:

作用域可以从父作用域继承。

Developer Guide Scope页面显示:

作用域(原型)从其父作用域继承属性。

  • 那么,子作用域总是原型地从其父作用域继承吗?
  • 有例外吗?
  • 当它继承时,是否总是正常的JavaScript原型继承?

共3个答案

匿名用户

快速回答:
子作用域通常从其父作用域原型继承,但并不总是这样。此规则的一个例外是具有作用域的指令:{。。。}--这将创建一个不典型继承的“隔离”作用域。在创建“可重用组件”指令时,经常使用这种构造。

至于细微之处,作用域继承通常是直截了当的。。。直到您需要子作用域中的双向数据绑定(即表单元素,ng-model)。如果您试图从子作用域内绑定到父作用域中的基元(例如,number,string,boolean),则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">`
-->

null

null

也放在AngularJS的wiki上:https://github.com/angular/angular.js/wiki/understandment-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'

不参考原型链,并向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' }

不咨询原型链,而child scope获得两个新的对象属性,它们隐藏/阴影具有相同名称的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:false

假设我们的控制器中有:

$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”)会使子作用域获得一个新的MyPrimity作用域属性,该属性隐藏/阴影同名的父作用域属性。这很可能不是你想要/期待的。

在第二个输入文本框中键入(例如,“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;
}

下面是一个使用这种“父函数”方法的示例fiddle。(小提琴是作为回答的一部分写的:https://stackoverflow.com/a/14104318/215945.)

另见https://stackoverflow.com/A/13782671/215945和https://github.com/angular/angular.js/issues/1267。

ng-switch作用域继承的工作原理与ng-include类似。因此,如果需要双向数据绑定到父作用域中的基元,请使用$parent,或者将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/阴影。

另见AngularJS,bind scope of a switch-case?

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中所示),则本质上会将值的副本分配给新的子scope属性。更改子作用域属性的值(即使用ng-model,因此子作用域num)不会更改父作用域引用的数组。因此,在上面的第一个ng-repeat中,每个子作用域都获得一个独立于myArrayOfPrimitives数组的num属性:

这个ng-repeat不能工作(就像你想要/期望的那样)。在文本框中键入内容会更改灰色框中的值,这些值仅在子作用域中可见。我们希望输入影响myArrayOfPrimitives数组,而不是子scope Primitives属性。为了实现这一点,我们需要将模型更改为对象数组。

因此,如果item是一个对象,则将对原始对象(而不是副本)的引用分配给新的子范围属性。更改子作用域属性的值(即使用ng-model,因此是obj.num)确实会更改父作用域引用的对象。所以在上面的第二个ng-重复中,我们有:

(我把一条线涂成灰色,这样就可以清楚地看到它的去向。)

这和预期的一样有效。在文本框中键入内容会更改灰色框中的值,这些值对子作用域和父作用域都是可见的。

另见ng-model,ng-repeat和inputs的困难和https://stackoverflow.com/a/13782671/215945

使用ng-controller嵌套控制器会导致正常的原型继承,就像ng-include和ng-switch一样,因此应用相同的技术。但是,“两个控制器通过$scope继承共享信息被认为是糟糕的形式”--应该使用http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/A服务来在控制器之间共享数据。

(如果您确实希望通过控制器作用域继承共享数据,则无需执行任何操作。子作用域将可以访问所有父作用域属性。另请参阅加载或导航时控制器加载顺序不同)

  1. default(scope:false)-指令不创建新的作用域,因此这里没有继承。这很容易,但也很危险,因为例如,一个指令可能认为它是在作用域上创建一个新属性,而实际上它是在破坏一个现有属性。对于编写旨在作为可重用组件的指令,这不是一个好的选择。
  2. scope:true-该指令创建一个新的子作用域,该子作用域通常从父作用域继承。如果(在同一个DOM元素上)有多个指令请求新作用域,则只创建一个新的子作用域。由于我们有“正常”原型继承,这就像ng-include和ng-switch,所以要小心到父作用域原语的双向数据绑定,以及父作用域属性的子作用域隐藏/阴影。
  3. scope:{。。。}-指令创建一个新的隔离/隔离作用域。它不典型地继承。在创建可重用组件时,这通常是您的最佳选择,因为指令不会意外地读取或修改父作用域。但是,这样的指令通常需要访问几个父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用'=')或单向绑定(使用'@')。还有“&;”若要绑定到父范围表达式,请执行以下操作。因此,这些都创建了从父作用域派生的本地作用域属性。请注意,属性用于帮助设置绑定--您不能只在对象散列中引用父作用域属性名,您必须使用属性。例如,如果要绑定到独立作用域
    作用域{localprop:'@parentprop'}中的父属性parentprop,则此操作将不起作用。必须使用属性指定指令要绑定到的每个父属性:
    作用域:{localprop:'@theparentprop'}
    隔离作用域的__protO_引用对象。Isolate作用域的$parent引用父作用域,因此尽管它是隔离的,并且不典型地从父作用域继承,但它仍然是一个子作用域。
    对于下面的图片,我们有

    作用域:{interpolatedprop:'@interpolated',twowaybindingprop:'=twowaybinding'}
    另外,假设该指令在其链接函数中执行此操作:scope.someIsolateprop=“I'm isolated”
    transclude:true-该指令创建一个新的“transcluded”子作用域,它典型地从父作用域继承。transcluded和Isolate作用域(如果有的话)是同级作用域--每个作用域的$parent属性引用相同的父作用域。当transcluded和isolate作用域都存在时,isolate scope属性$$NextSibling将引用transcluded作用域。我不知道转喻范围有什么细微差别。
    对于下面的图片,假设与上面相同的指令,并添加以下内容:transpclude:true

这个fiddle有一个showscope()函数,可用于检查隔离和转换的作用域。请参阅小提琴注释中的说明。

作用域有四种类型:

  1. 普通原型作用域继承--ng-include,ng-switch,ng-controller,带有作用域的指令:true
  2. 带有复制/赋值的普通原型范围继承--ng-repeat。ng-repeat的每次迭代都创建一个新的子作用域,并且该新的子作用域总是获得一个新的属性。
  3. 隔离作用域--具有作用域的指令:{。。。}。这个不是原型,而是“=”,“@”和“&;”提供通过属性访问父作用域属性的机制。
  4. transcluded scope--带有transclude:true的指令。这也是正常的原型作用域继承,但它也是任何隔离作用域的兄弟。

对于所有作用域(原型或非原型),Angular总是通过属性$Parent,$$ChildHead和$$ChildTail跟踪父子关系(即层次结构)。

图表是用GitHub上的graphviz“*.dot”文件生成的。TimCaswell的“学习JavaScript with Object Graphs”是使用GraphViz制作图表的灵感来源。

匿名用户

我绝不想与Mark的答案竞争,而只是想强调最终使一切都点击成为Javascript继承及其原型链的新人的那部分。

只有属性读搜索原型链,而不是写。所以当你设置

myObject.prop = '123';

它不查找链,但当你设置

myObject.myThing.prop = '123';

在写操作中有一个微妙的读操作,它试图在写到它的道具之前查找myThing。这就是为什么从子对象写入Object.Properties会获取父对象的原因。

匿名用户

我想在@Scott Driscoll答案中添加一个使用javascript的原型继承的示例。我们将对object.create()使用经典继承模式,它是ECMAScript5规范的一部分。

首先,我们创建“父”对象函数

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 */

父对象和子对象。已更改一个属性

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