首页 文章

AngularJS中范围原型/原型继承的细微差别是什么?

提问于
浏览
987

API Reference Scope page说:

范围可以从父范围继承 .

Developer Guide Scope page说:

范围(原型)从其父范围继承属性 .

那么,子范围是否始终从其父范围继承原型?有例外吗?当它继承时,它是否总是正常的JavaScript原型继承?

3 回答

  • 1713

    我绝不想与Mark的答案竞争,但只是想强调最终使所有内容点击为Javascript inheritance and its prototype chain的新手 .

    Only property reads search the prototype chain, not writes. 所以当你设置

    myObject.prop = '123';
    

    它不会查找链条,但是当你设置时

    myObject.myThing.prop = '123';
    

    there's a subtle read going on within that write operation 试图在写入其道具之前查找myThing . 这就是's why writing to object.properties from the child gets at the parent'的对象 .

  • 138

    Quick answer
    子范围通常原型继承自其父范围,但并非总是如此 . 此规则的一个例外是带有 scope: { ... } 的指令 - 这会创建一个不会原型继承的"isolate"范围 . 创建"reusable component"指令时经常使用此构造 .

    至于细微差别,范围继承通常是直接的......直到您在子范围内需要 2-way data binding (即表单元素,ng-模型) . 如果您尝试从子范围内部绑定到父范围中的 primitive (例如,数字,字符串,布尔值),则Ng-repeat,ng-switch和ng-include会使您失意 . 它不像大多数人期望的那样工作 . 子作用域获取其自己的属性,该属性隐藏/隐藏同名的父属性 . 你的解决方法是

    • 在模型的父级中定义对象,然后在子级中引用该对象的属性:parentObj.someProp

    • 使用$ parent.parentScopeProperty(并非总是可行,但在可能的情况下比1.更容易)

    • 在父作用域上定义一个函数,并从子作用中调用它(并不总是可行的)

    新的AngularJS开发人员通常没有意识到 ng-repeatng-switchng-viewng-includeng-if 都创建了新的子范围,因此当涉及这些指令时,问题经常会出现 . (有关问题的快速说明,请参阅this example . )

    遵循"best practice" always have a '.' in your ng-models可以轻松避免这种原语问题 - 请注意3分钟 . Misko用 ng-switch 演示了原始绑定问题 .

    有一个 ' . '在你的模型中将确保原型继承发挥作用 . 所以,使用

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

    L-o-n-g answer

    JavaScript原型继承

    Also placed on the 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中找到该属性,它将继续原型链......一直到根范围) . 所以,这些都是真的:

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

    假设我们这样做:

    childScope.aString = 'child string'
    

    不参考原型链,并且向childScope添加了新的aString属性 . This new property hides/shadows the parentScope property with the same name. 当我们在下面讨论ng-repeat和ng-include时,这将变得非常重要 .

    property hiding

    假设我们这样做:

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

    查询原型链是因为在childScope中找不到对象(anArray和anObject) . 可以在parentScope中找到对象,并在原始对象上更新属性值 . 没有新的属性添加到childScope;没有创建新对象 . (注意,在JavaScript数组和函数中也是对象 . )

    follow the prototype chain

    假设我们这样做:

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

    未查询原型链,子范围获取两个新对象属性,这些属性隐藏/隐藏具有相同名称的parentScope对象属性 .

    more property hiding

    小贴士:

    • 如果我们读取childScope.propertyX,并且childScope具有propertyX,则不会查询原型链 .

    • 如果我们设置了childScope.propertyX,则不会查询原型链 .

    最后一个场景:

    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 .

    • 以下内容创建了一个新范围,该范围不会继承原型:directive with 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都会生成一个新的子作用域,它原型继承自父作用域 .

    ng-include child scopes

    在第一个输入文本框中键入(例如,"77")会导致子范围获取新的 myPrimitive 范围属性,该属性隐藏/隐藏同名的父范围属性 . 这可能不是你想要/期望的 .

    ng-include with a primitive

    在第二个输入文本框中键入(例如,“99”)不会产生新的子属性 . 因为tpl2.html将模型绑定到对象属性,所以当ngModel查找对象myObject时,原型继承会启动 - 它会在父范围内找到它 .

    ng-include with an object

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

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

    在此输入文本框中键入(例如,“22”)不会导致新的子属性 . 该模型现在绑定到父作用域的属性(因为$ parent是引用父作用域的子作用域属性) .

    ng-include with $parent

    对于所有范围(原型或非原型),Angular始终通过范围属性$ parent,$$ childHead和$$ childTail跟踪父子关系(即层次结构) . 我通常不会在图表中显示这些范围属性 .

    对于不涉及表单元素的场景,另一种解决方案是在父作用域上定义一个函数来修改基元 . 然后确保子节点始终调用此函数,该函数由于原型继承而可用于子作用域 . 例如 . ,

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

    这是sample fiddle使用这种"parent function"方法 . (小提琴是作为这个答案的一部分写的:https://stackoverflow.com/a/14104318/215945 . )

    另见https://stackoverflow.com/a/13782671/215945https://github.com/angular/angular.js/issues/1267 .

    ng-switch

    ng-switch范围继承与ng-include一样工作 . 因此,如果需要双向数据绑定到父作用域中的基元,请使用$ parent,或将模型更改为对象,然后绑定到该对象的属性 . 这将避免子范围隐藏/遮蔽父范围属性 .

    另见AngularJS, bind scope of a 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创建一个新范围,该范围原型继承自父范围 but it also assigns the item's value to a new property on the new child scope . (新属性的名称是循环变量's name.) Here' s 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 with primitives

    这种ng-repeat不起作用(就像你想要/期望的那样) . 键入文本框会更改灰色框中的值,这些值仅在子范围中可见 . 我们想要的是输入影响myArrayOfPrimitives数组,而不是子范围原始属性 . 为此,我们需要将模型更改为对象数组 .

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

    ng-repeat with objects

    (我将一条线涂成灰色,以便清楚它的位置 . )

    这按预期工作 . 键入文本框会更改灰色框中的值,这些值对子作用域和父作用域都可见 .

    另见Difficulty with ng-model, ng-repeat, and inputshttps://stackoverflow.com/a/13782671/215945

    ng-controller

    使用ng-controller嵌套控制器会产生正常的原型继承,就像ng-include和ng-switch一样,因此适用相同的技术 . 但是,"it is considered bad form for two controllers to share information via $scope inheritance" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/应该使用服务来代替控制器之间共享数据 .

    (如果您确实希望通过控制器范围继承来共享数据,则无需执行任何操作 . 子范围将可以访问所有父范围属性 . 另请参阅Controller load order differs when loading or navigating

    指令

    • default( scope: false ) - 该指令不创建新范围,因此这里没有继承 . 这很容易,但也很危险,因为,例如,指令可能认为它正在范围内创建新属性,而实际上它正在破坏现有属性 . 对于编写旨在作为可重用组件的指令,这不是一个好的选择 .

    • scope: true - 该指令创建一个原型继承自父作用域的新子作用域 . 如果多个指令(在同一DOM元素上)请求新范围,则只有一个新的子范围被建造 . 由于我们有原型继承,这类似于ng-include和ng-switch,因此要警惕双向数据绑定到父作用域基元,以及子作用域隐藏/遮蔽父作用域属性 .

    • scope: { ... } - 该指令创建一个新的隔离/隔离范围 . 它没有原型继承 . 在创建可重用组件时,这通常是您的最佳选择,因为该指令不会意外地读取或修改父作用域 . 但是,此类指令通常需要访问一些父作用域属性 . 对象散列用于在父作用域和隔离作用域之间设置双向绑定(使用'=')或单向绑定(使用'@') . 还有'&'绑定到父作用域表达式 . 因此,这些都创建了从父作用域派生的本地作用域属性 . 请注意,属性用于帮助设置绑定 - 如果要绑定到隔离范围中的父属性 parentProp ,则可以't just reference parent scope property names in the object hash, you have to use an attribute. E.g., this won'工作: <div my-directive>scope: { localProp: '@parentProp' } . 必须使用属性来指定指令要绑定到的每个父属性: <div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' } .
      隔离范围的 __proto__ 参考对象 . 隔离范围's $parent references the parent scope, so although it is isolated and doesn' t从父范围继承原型,它仍然是子范围 .
      对于下面的图片我们有
      <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
      scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
      此外,假设该指令在其链接函数中执行此操作: scope.someIsolateProp = "I'm isolated"

    isolated scope

    有关隔离范围的更多信息,请参阅http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/

    • transclude: true - 该指令创建一个新的"transcluded"子作用域,它原型继承自父作用域 . transcluded和隔离范围(如果有)是兄弟 - 每个范围的$ parent属性引用相同的父范围 . 当transcluded和isolate范围都存在时,隔离范围属性$$ nextSibling将引用transcluded范围 . 我不知道转换范围有任何细微差别 .
      对于下图,假设与上述相同的指令添加: transclude: true

    transcluded scope

    fiddle具有 showScope() 函数,可用于检查隔离和转换范围 . 请参阅小提琴中的注释中的说明 .


    摘要

    范围有四种类型:

    • 普通原型范围继承 - ng-include,ng-switch,ng-controller,指令 scope: true

    • 正常的原型范围继承与复制/赋值 - ng-repeat . ng-repeat的每次迭代都会创建一个新的子范围,并且新的子范围始终会获得一个新属性 .

    • isolate scope - 指令 scope: {...} . 这个不是原型,但是'=','@'和'&'提供了一种通过属性访问父作用域属性的机制 .

    • transcluded scope - 指令 transclude: true . 这个也是正常的原型范围继承,但它也是任何隔离范围的兄弟 .

    对于所有范围(原型或非原型),Angular始终通过属性$ parent和$$ childHead和$$ childTail跟踪父子关系(即层次结构) .

    使用graphviz "*.dot"文件生成图表,这些文件位于github上 . Tim Caswell的“Learning JavaScript with Object Graphs”是将GraphViz用于图表的灵感来源 .

  • 20

    我想用javascript向@Scott Driscoll回答添加一个原型继承的例子 . 我们将使用经典的继承模式与Object.create(),这是EcmaScript 5规范的一部分 .

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

    function Parent(){
    
    }
    

    然后将原型添加到“父”对象功能

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

    创建“Child”对象功能

    function Child(){
    
    }
    

    分配子原型(让子原型从父原型继承)

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

    分配适当的“Child”原型构造函数

    Child.prototype.constructor = Child;
    

    将方法“changeProps”添加到子原型,它将重写Child对象中的“原始”属性值,并在Child和Parent对象中更改“object.one”值

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

    启动父(爸爸)和子(子)对象 .

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

    调用Child(son)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

相关问题