关于javascript:AngularJS中作用域原型/原型继承的细微差别是什么?

What are the nuances of scope prototypal / prototypical inheritance in AngularJS?

API参考范围页面显示:

A scope can inherit from a parent scope.

开发者指南范围页面显示:

A scope (prototypically) inherits properties from its parent scope.

那么,子范围是否总是原型地从其父范围继承?有例外吗?当它确实继承时,它总是普通的JavaScript原型继承吗?


快速回答:子作用域通常从其父作用域原型继承,但并非总是如此。此规则的一个例外是使用scope: { ... }的指令——这创建了一个"隔离"范围,该范围不典型地继承。此构造通常在创建"可重用组件"指令时使用。好的。

至于细微差别,范围继承通常是直接的。直到在子范围中需要双向数据绑定(即表单元素、ng模型)。如果试图从子作用域内部绑定到父作用域中的基元(例如,数字、字符串、布尔值),则ng repeat、ng switch和ng include可能会使您绊倒。它不像大多数人期望的那样工作。子作用域获取自己的属性,该属性隐藏/隐藏同名的父属性。你的解决方法是好的。

  • 在模型的父级中定义对象,然后在子级中引用该对象的属性:parentobj.someprop
  • 使用$parent.parentScopeProperty(不总是可能的,但比1容易)。在可能的情况下)
  • 在父作用域上定义一个函数,并从子作用域调用它(不总是可能的)
  • 新的AngularJS开发人员往往没有意识到ng-repeatng-switchng-viewng-includeng-if都创建了新的子作用域,因此当涉及到这些指令时,问题往往会出现。(有关问题的快速说明,请参阅此示例。)好的。

    在您的NG模型中,遵循"总是有一个"的"最佳实践",可以很容易地避免与原语有关的这个问题——注意3分钟的价值。misko证明了与ng-switch的原始绑定问题。好的。

    在模型中使用"."将确保原型继承发挥作用。所以,使用好的。

    1
    2
    3
    4
    5
    <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,我们有:好的。

    prototypal inheritance好的。

    (注意,为了节省空间,我将anArray对象显示为具有三个值的单个蓝色对象,而不是具有三个单独灰色文字的单个蓝色对象。)好的。

    如果我们试图从子作用域访问ParentScope上定义的属性,javascript将首先查找子作用域,而不是查找属性,然后查找继承的作用域,并查找属性。(如果它在ParentScope中找不到属性,那么它将继续向原型链上游……一直到根作用域)。所以,这些都是真的:好的。

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

    假设我们这样做:好的。

    1
    childScope.aString = 'child string'

    不查询原型链,并向ChildScope添加新的Astring属性。此新属性隐藏/隐藏同名的ParentScope属性。当我们讨论ng repeat和ng include时,这将变得非常重要。好的。

    property hiding好的。

    假设我们这样做:好的。

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

    查询原型链是因为在ChildScope中找不到对象(Anarray和AnObject)。对象在ParentScope中找到,属性值在原始对象上更新。不会向ChildScope添加新属性;不会创建新对象。(请注意,在javascript数组和函数中也是对象。)好的。

    follow the prototype chain好的。

    假设我们这样做:好的。

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

    不参考原型链,子作用域获取两个新的对象属性,这些属性隐藏/隐藏具有相同名称的ParentScope对象属性。好的。

    more property hiding好的。

    外卖:好的。

    • 如果我们读取了childscope.propertyx,而childscope具有propertyx,则不参考原型链。
    • 如果我们设置childscope.propertyx,则不会参考原型链。

    最后一个场景:好的。

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

    我们首先删除了ChildScope属性,然后当我们再次尝试访问该属性时,会参考原型链。好的。

    after removing a child property好的。角度范围继承

    竞争者:好的。

    • 下面创建新的作用域,并继承原型:ng repeat、ng include、ng switch、ng controller、带有scope: true的指令、带有transclude: true的指令。
    • 下面创建了一个不继承原型的新范围:带有scope: { ... }的指令。这将创建一个"隔离"范围。

    注意,默认情况下,指令不会创建新的作用域——即,默认值是scope: false。好的。包括NG

    假设我们的控制器中有:好的。

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

    在我们的HTML中:好的。

    1
    2
    3
    4
    5
    6
    7
    <script type="text/ng-template" id="/tpl1.html">
    <input ng-model="myPrimitive">



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

    每个ng-include都会生成一个新的子作用域,原型继承自父作用域。好的。

    ng-include child scopes好的。

    在第一个输入文本框中键入(例如,"77")将导致子作用域获得一个新的myPrimitive作用域属性,该属性隐藏/隐藏相同名称的父作用域属性。这可能不是你想要/期望的。好的。

    ng-include with a primitive好的。

    在第二个输入文本框中键入(例如"99")不会产生新的子属性。因为tpl2.html将模型绑定到对象属性,所以当ngmodel查找对象myObject时,原型继承就开始了——它在父范围中找到了它。好的。

    ng-include with an object好的。

    如果不想将模型从原语更改为对象,我们可以重写第一个使用$parent的模板:好的。

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

    在此输入文本框中键入(例如"22")不会产生新的子属性。模型现在绑定到父作用域的属性(因为$parent是引用父作用域的子作用域属性)。好的。

    ng-include with $parent好的。

    对于所有范围(无论是否为原型),Angular始终通过范围属性$parent、$$childhead和$$childtail跟踪父子关系(即层次结构)。我通常不会在图表中显示这些作用域属性。好的。

    对于不涉及表单元素的场景,另一个解决方案是在父作用域上定义一个函数来修改原语。然后确保子级总是调用这个函数,由于原型继承,子级作用域可以使用这个函数。例如。,好的。

    1
    2
    3
    4
    // 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开关作用域继承的工作方式与ng include类似。因此,如果需要将双向数据绑定到父范围中的基元,请使用$parent,或者将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/隐藏。好的。

    另请参见AngularJS,交换机外壳的绑定范围?好的。NG重复

    ng repeat的工作方式略有不同。假设我们的控制器中有:好的。

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

    在我们的HTML中:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <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的角度源代码实际是:好的。

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

    如果项是原语(如MyArrayOfPrimitives中的原语),则基本上会将值的副本分配给新的子范围属性。更改子范围属性的值(即使用ng模型,因此子范围num不会更改父范围引用的数组。因此,在上面的第一个ng重复中,每个子作用域都得到一个独立于myArrayOfPrimitives数组的num属性:好的。

    ng-repeat with primitives好的。

    这个NG重复将不起作用(如您希望/期望的那样)。在文本框中键入内容会更改灰色框中的值,这些值仅在子范围中可见。我们希望输入影响myArrayOfPrimitives数组,而不是子范围原语属性。为了实现这一点,我们需要将模型更改为对象数组。好的。

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

    ng-repeat with objects好的。

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

    这按预期工作。在文本框中键入内容会更改灰色框中的值,这些值对子范围和父范围都可见。好的。

    另请参见ng模型、ng重复和输入的困难以及https://stackoverflow.com/a/13782671/215945好的。NG控制器

    使用ng控制器嵌套控制器会导致正常的原型继承,就像ng include和ng开关一样,所以同样的技术也适用。但是,"两个控制器通过$scope继承共享信息被认为是不好的形式"-http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/应该使用服务在控制器之间共享数据。好的。

    (如果您真的想通过控制器范围继承来共享数据,则无需做任何事情。子作用域将可以访问所有父作用域属性。另请参见控制器加载顺序在加载或导航时的不同)好的。指令

  • 默认(scope: false)—该指令不创建新的作用域,因此这里没有继承。这很容易,但也很危险,因为,例如,指令可能认为它在作用域上创建了一个新的属性,而实际上它正在破坏现有的属性。对于编写作为可重用组件的指令来说,这不是一个好的选择。
  • scope: true—该指令创建了一个新的子作用域,原型继承自父作用域。如果多个指令(在同一个dom元素上)请求新的作用域,则只创建一个子作用域。由于我们有"普通"原型继承,这类似于ng include和ng开关,因此要小心与父作用域原语的双向数据绑定,以及父作用域属性的子作用域隐藏/隐藏。
  • scope: { ... }—该指令创建了一个新的隔离/隔离范围。它不是典型的继承。这通常是创建可重用组件时的最佳选择,因为该指令不能意外地读取或修改父范围。然而,这样的指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用"=")或单向绑定(使用"@")。还有"amp;"要绑定到父作用域表达式。因此,所有这些都创建从父范围派生的本地范围属性。注意,属性用于帮助设置绑定——您不能只引用对象哈希中的父范围属性名,必须使用属性。例如,如果您希望在单独的范围内绑定到父属性parentPropscope: { localProp: '@parentProp' },那么这将不起作用。必须使用属性指定指令要绑定到的每个父属性:scope: { localProp: '@theParentProp' }。隔离作用域的__proto__引用对象。隔离作用域的$parent引用父作用域,因此尽管它是隔离的,并且没有从父作用域继承原型,但它仍然是子作用域。下面的图片我们有scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }另外,假设该指令在其链接函数中这样做:scope.someIsolateProp ="I'm isolated"isolated scope有关隔离范围的详细信息,请参阅http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  • transclude: true—该指令创建了一个新的"超越"子作用域,该子作用域的原型继承自父作用域。被超越的和独立的作用域(如果有的话)是兄弟的——每个作用域的$parent属性引用相同的父作用域。当一个被屏蔽和一个隔离作用域同时存在时,隔离作用域属性$$NextSibling将引用被屏蔽的作用域。我不知道有什么细微的差别与超越范围。对于下面的图片,采用与上面相同的指令加上这个加法:transclude: truetranscluded scope
  • 这个小提琴有一个showScope()功能,可以用来检查一个孤立和隐蔽的范围。请参见小提琴注释中的说明。好的。总结

    作用域有四种类型:好的。

  • 常规原型范围继承——ng包括,ng开关,ng控制器,带有scope: true的指令
  • 具有复制/分配的普通原型范围继承--ng repeat。每次迭代ng repeat都会创建一个新的子作用域,而这个新的子作用域总是得到一个新的属性。
  • 隔离范围——与scope: {...}指令。这一个不是原型,但"="、"@"和"&;"提供了通过属性访问父作用域属性的机制。
  • 超越范围——与transclude: true指令。这也是一个普通的原型范围继承,但它也是任何隔离范围的兄弟。
  • 对于所有范围(无论是否为原型),Angular始终通过属性$parent和$childhead和$childtail跟踪父子关系(即层次结构)。好的。

    图表是用Github上的graphviz"*.dot"文件生成的。TimCaswell的"用对象图学习JavaScript"是在图表中使用graphviz的灵感来源。好的。好啊。


    我不想和马克的答案竞争,但我只想强调一点,那就是最终让所有东西都像一个新的javascript继承和原型链一样点击。

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

    1
    myObject.prop = '123';

    它不查链子,但当你设置

    1
    myObject.myThing.prop = '123';

    在这个写操作中有一个微妙的阅读,它试图在写到它的道具之前查找神话。这就是为什么从子对象写入object.properties会得到父对象的原因。


    我想在@scott driscoll answer中添加一个带有javascript的原型继承示例。我们将使用带有object.create()的经典继承模式,这是ecmascript 5规范的一部分。

    首先我们创建"父"对象函数

    1
    2
    3
    function Parent(){

    }

    然后将原型添加到"父"对象函数

    1
    2
    3
    4
    5
    6
     Parent.prototype = {
     primitive : 1,
     object : {
        one : 1
       }
    }

    创建"子"对象函数

    1
    2
    3
    function Child(){

    }

    指定子原型(使子原型从父原型继承)

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

    指定适当的"子"原型构造函数

    1
    Child.prototype.constructor = Child;

    将方法"changeprops"添加到子原型中,该方法将重写子对象中的"primitive"属性值,并更改子对象和父对象中的"object.one"值。

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

    启动父对象(DAD)和子对象(SON)。

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

    调用child(son)changeProps方法

    1
    son.changeProps();

    检查结果。

    父基元属性未更改

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

    子基元属性已更改(重写)

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

    父对象和子对象.one属性已更改

    1
    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