AngularJS 中范围原型/原型继承的细微差别是啥?
Posted
技术标签:
【中文标题】AngularJS 中范围原型/原型继承的细微差别是啥?【英文标题】:What are the nuances of scope prototypal / prototypical inheritance in AngularJS?AngularJS 中范围原型/原型继承的细微差别是什么? 【发布时间】:2018-11-09 18:14:15 【问题描述】:API Reference Scope page 说:
作用域可以从父作用域继承。
Developer Guide Scope page 说:
那么,子作用域是否总是从其父作用域原型继承? 有例外吗? 当它继承时,是否总是正常的 javascript 原型继承?作用域(典型地)从其父作用域继承属性。
【问题讨论】:
Prototypal is actually a word. 【参考方案1】:快速回答:
子作用域通常从其父作用域原型继承,但并非总是如此。该规则的一个例外是带有scope: ...
的指令——这会创建一个在原型上不继承的“隔离”范围。创建“可重用组件”指令时经常使用此构造。
至于细微差别,范围继承通常是直截了当的......直到您在子范围中需要2路数据绑定(即表单元素,ng-model)。如果您尝试从子作用域内绑定到父作用域中的 primitive(例如,数字、字符串、布尔值),那么 Ng-repeat、ng-switch 和 ng-include 可能会绊倒您。它不像大多数人期望的那样工作。子作用域拥有自己的属性,该属性隐藏/隐藏同名的父属性。您的解决方法是
-
在父模型中定义对象,然后在子模型中引用该对象的属性:parentObj.someProp
使用 $parent.parentScopeProperty(并非总是可行,但比 1. 更容易)
在父作用域上定义一个函数,并从子作用域调用它(并非总是可行)
新的 AngularJS 开发人员通常没有意识到 ng-repeat
、ng-switch
、ng-view
、ng-include
和 ng-if
都创建了新的子作用域,因此当涉及这些指令时,问题通常会出现。 (有关问题的快速说明,请参阅this example。)
遵循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 答案:
JavaScript 原型继承
也放在 AngularJS wiki 上: https://github.com/angular/angular.js/wiki/Understanding-Scopes
首先对原型继承有一个扎实的理解是很重要的,特别是如果您来自服务器端背景并且您更熟悉经典继承。所以让我们先回顾一下。
假设 parentScope 具有属性 aString、aNumber、anArray、anObject 和 aFunction。如果 childScope 原型继承自 parentScope,我们有:
(请注意,为了节省空间,我将 anArray
对象显示为具有三个值的单个蓝色对象,而不是具有三个单独的灰色文字的单个蓝色对象。)
如果我们尝试从子作用域访问定义在父作用域上的属性,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属性。 这个新属性隐藏/隐藏了同名的 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'
不参考原型链,子作用域获得两个新的对象属性,它们隐藏/隐藏同名的父作用域对象属性。
要点:
如果我们读取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;
这是一个使用这种“父函数”方法的sample fiddle。 (小提琴是作为这个答案的一部分写的:https://***.com/a/14104318/215945。)
另请参阅 https://***.com/a/13782671/215945 和 https://github.com/angular/angular.js/issues/1267。
ng-开关
ng-switch 范围继承就像 ng-include 一样工作。因此,如果您需要将 2 路数据绑定到父范围内的原语,请使用 $parent,或者将模型更改为对象,然后绑定到该对象的属性。这将避免子范围隐藏/隐藏父范围属性。
另见AngularJS, bind scope of a switch-case?
ng-重复
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 中),则本质上将值的副本分配给新的子范围属性。更改子范围属性的值(即,使用 ng-model,因此使用子范围 num
)确实不会更改父范围引用的数组。所以在上面的第一个 ng-repeat 中,每个子作用域都有一个独立于 myArrayOfPrimitives 数组的 num
属性:
这个 ng-repeat 将不起作用(就像您想要/期望的那样)。键入文本框会更改灰色框中的值,这些值仅在子范围内可见。我们想要的是输入影响 myArrayOfPrimitives 数组,而不是子范围的原始属性。为此,我们需要将模型更改为对象数组。
因此,如果 item 是一个对象,则将对原始对象(而不是副本)的引用分配给新的子范围属性。更改子范围属性的值(即,使用 ng-model,因此 obj.num
)确实更改了父范围引用的对象。所以在上面的第二个 ng-repeat 中,我们有:
(我将一条线涂成灰色,以便清楚它的去向。)
这按预期工作。在文本框中输入内容会更改灰色框中的值,这些值对子范围和父范围都可见。
另请参阅Difficulty with ng-model, ng-repeat, and inputs 和 https://***.com/a/13782671/215945
ng-控制器
使用 ng-controller 嵌套控制器会导致正常的原型继承,就像 ng-include 和 ng-switch 一样,因此适用相同的技术。 然而,“两个控制器通过 $scope 继承共享信息被认为是一种不好的形式”——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,所以要小心 2-way 数据绑定到父作用域原语,以及子作用域隐藏/隐藏父作用域属性。
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/
transclude: true
- 该指令创建一个新的“transcluded”子作用域,其原型继承自父作用域。嵌入的和隔离的范围(如果有的话)是同级的——每个范围的 $parent 属性引用相同的父范围。当一个转入和隔离范围都存在时,隔离范围属性 $$nextSibling 将引用转入范围。我不知道嵌入范围的任何细微差别。
对于下图,假设与上面的指令相同,并添加:transclude: true
这个fiddle 有一个showScope()
函数,可用于检查隔离和转入作用域。请参阅小提琴中的 cmets 中的说明。
总结
范围有四种类型:
-
正常的原型范围继承——ng-include、ng-switch、ng-controller、带有
scope: true
的指令
具有复制/分配的正常原型范围继承 -- ng-repeat。 ng-repeat 的每次迭代都会创建一个新的子作用域,并且这个新的子作用域总是会获得一个新属性。
隔离范围 -- 带有scope: ...
的指令。这不是原型,但“=”、“@”和“&”提供了一种通过属性访问父作用域属性的机制。
嵌入范围 -- 带有transclude: true
的指令。这也是普通的原型作用域继承,但它也是任何隔离作用域的兄弟。
对于所有范围(原型或非原型),Angular 总是通过属性 $parent 和 $$childHead 和 $$childTail 跟踪父子关系(即层次结构)。
图表是使用graphviz“*.dot”文件生成的,这些文件位于github。 Tim Caswell 的“Learning JavaScript with Object Graphs”是使用 GraphViz 绘制图表的灵感。
【讨论】:
很棒的文章,对于一个 SO 答案来说太长了,但无论如何都非常有用。请在编辑将其缩小之前将其放在您的博客上。 我在AngularJS wiki放了一份副本。 更正:“隔离范围的__proto__
引用对象。”应该改为“隔离作用域的__proto__
引用作用域对象。”因此,在最后两张图片中,橙色的“对象”框应该是“范围”框。
这个问题应该包含在 angularjs 指南中。这更加说教......
维基让我很困惑,首先它写道:“原型链被查询,因为在 childScope 中找不到对象。”然后它显示:“如果我们设置 childScope.propertyX,则不参考原型链。”。第二个暗示一个条件,而第一个没有。【参考方案2】:
我想在@Scott Driscoll 答案中添加一个使用 javascript 进行原型继承的示例。我们将使用带有 Object.create() 的经典继承模式,它是 EcmaScript 5 规范的一部分。
首先我们创建“父”对象函数
function Parent()
然后将原型添加到“父”对象函数
Parent.prototype =
primitive : 1,
object :
one : 1
创建“子”对象函数
function Child()
分配子原型(使子原型继承父原型)
Child.prototype = Object.create(Parent.prototype);
分配适当的“子”原型构造函数
Child.prototype.constructor = Child;
将“changeProps”方法添加到子原型中,这将重写子对象中的“原始”属性值并更改子对象和父对象中的“object.one”值
Child.prototype.changeProps = function()
this.primitive = 2;
this.object.one = 2;
;
启动 Parent(爸爸)和 Child(儿子)对象。
var dad = new Parent();
var son = new Child();
调用Child(子)changeProps方法
son.changeProps();
检查结果。
父基元属性没有改变
console.log(dad.primitive); /* 1 */
子基元属性已更改(重写)
console.log(son.primitive); /* 2 */
Parent 和 Child 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
【讨论】:
关于原型继承的这三个答案我最喜欢的部分是答案本身相互继承【参考方案3】:我绝不想与 Mark 的回答竞争,只是想强调一下作为 Javascript inheritance and its prototype chain 的新人最终让一切都点击的那篇文章。
只有属性读取搜索原型链,而不是写入。所以当你设置
myObject.prop = '123';
它不查找链,但是当你设置时
myObject.myThing.prop = '123';
在写入操作中进行了微妙的读取,它会在写入其 prop 之前尝试查找 myThing。所以这就是为什么从孩子写入 object.properties 会得到父母的对象。
【讨论】:
虽然这是一个非常简单的概念,但它可能不是很明显,因为我相信很多人都忽略了它。说得好。 优秀的评论。我带走,非对象属性的解析不涉及读取,而对象属性的解析则涉及。 为什么?属性写入不沿着原型链上升的动机是什么?这似乎很疯狂...... 如果你添加一个真正简单的例子会很棒。 请注意,它确实在原型链中搜索setters。如果没有找到,它会在接收器上创建一个属性。以上是关于AngularJS 中范围原型/原型继承的细微差别是啥?的主要内容,如果未能解决你的问题,请参考以下文章