起因是在StackOverflow上看到了这个帖子:Directive isolate scope with ng-repeat scope in AngularJS - Stack Overflow
题主原本想问的是一个AngularJS中对指令使用ng-repeat情况下的孤立作用域问题,最佳答主指出了题主理解的根本性错误,并用了最简单的demo来复现这个问题。
有问题的Demo:
html代码:
<div ng-app="my-app" ng-controller="MainController">
<div>
Selected: {{selected.first}} {{selected.last}}
</div>
<div>
<ul>
<li ng-repeat="name in names"
ng-class="{ active: $index == selected }"
ng-click="selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
</ul>
</div>
</div>
js代码:
var module = angular.module('my-app', []);
function MainController($scope) {
$scope.names = [
{first: "John", last: "Smith"},
{first: "Jane", last: "Smith"}
];
$scope.selected = undefined;
}
点击行就会发现,点击并不能触发"Selected:"后面出现被选中行的文字。
正常工作的demo:
html代码:
<div ng-app="my-app" ng-controller="MainController">
<div>
Selected: {{selected.first}} {{selected.last}}
</div>
<div>
<ul>
<li ng-repeat="name in names"
ng-class="{ active: $index == state.selected }"
ng-click="state.selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
</ul>
</div>
</div>
js代码:
var module = angular.module('my-app', []);
function MainController($scope) {
$scope.names = [
{first: "John", last: "Smith"},
{first: "Jane", last: "Smith"}
];
$scope.state = { selected: undefined };
}
对比两个Demo,就会发现,问题的关键在于正确代码中使用state对象来封装了这个selected属性。那么,为什么使用了state对象封装就能解决这个问题呢?
最佳答主解释说,selected是一个prmitive类型(姑且翻译为基本数据类型,包括null、undefined、boolean、number、string和symbol(es6),参考这里),这意味着,当子类在写这个属性时会覆盖父类的同名属性。并强调了一条AngularJS中的黄金法则:所有的模型值必须包含一个"."。
这让我想到在读《AngularJS权威教程》P8时也看到了一句类似的话:由于JavaScript自身的特点,以及它在传递值和引用时的不同处理方式,通常认为,在视图中通过对象的属性而非对象本身来进行引用绑定,是Angular中的最佳实践。
这么解释就可以了吗?非也非也,对前端小白来说,看到这些话时仍然是丈二和尚摸不着头脑。
如果你在Chrome中安装了Batarang插件,在点击任一行前查看每一个li行的Model,会发现其Model中并没有selected属性,而点击之后,其Model中就多了这个属性,并且与父Model中的selected值不一致,父Model中它的值仍为undefined。这个首先能说明, 每一个li行在被点击时,确实创建了自己的selected属性,并且覆盖了父类的同名属性。
最佳答主在答案中说到了一个关键的地方:the parameters passed into ngRepeat for use on your directive's attributes do use a prototypically-inherited scope。这里面提到了原型继承这个东西。首先,要搞明白Javascript中的原型(prototype)是怎么回事,这个可以参考Javascript继承机制的设计思想 - 阮一峰的网络日志。
简单总结一下:Javascript中一个"类"所有实例对象需要共享的属性和方法,都放在prototype对象里面.
这个StackOverflow问题,其实归根到底还是一个js基础问题,运行一下下面的代码,你就知道是怎么回事了:
var Parent = function(){
} ;
Parent.prototype.name = 'parent' ;
Parent.prototype.obj = {name : 'objparent'} ;
var Child = function(){
};
Child.prototype = new Parent() ;
var parent = new Parent() ;
var child = new Child() ;
console.log(parent.name) ; //parent
console.log(child.name) ; //parent
child.name = 'child' ;
console.log(parent.name) ; //parent
console.log(child.name) ; //child
console.log(parent.obj.name) ; //objparent
console.log(child.obj.name) ; //objparent
child.obj.name = 'objchild' ;
console.log(parent.obj.name) ; //objchild
console.log(child.obj.name) ; //objchild
console.log(parent.obj) ; //Object name:"objchild"
console.log(child.obj) ; //Object name:"objchild"
child.obj = {name : 'test'} ;
console.log(parent.obj) ; //Object name:"objchild"
console.log(child.obj) ; //Object name:"test"
在Parent原型中,name是string类型,属于Javascript中的Primitive类型(基本数据类型),在子类中写name时,不会修改父类的name值;
而obj是Javascript中的Object类型,不过直接在子类中修改obj对象,其表现与上面的name一样
只有修改obc.name时,才能保证父类与子类的obc.name保持一致。
这说明,子类无论是写与父类同名的Primitive类型属性,还是写Object属性,都是在子类中创建了新的属性覆盖了父类的同名属性。只有写对象(Object)的属性(即使用点表达式),才能实际上修改到父类中同名对象的属性值。
正因为如此,在Angular设置Model时,在父级作用域中宜使用.(点表达式)来处理基本数据模型,本质就是对像的原型继承会保持同一个引用。这样就可以避免上面的问题。
其实从根本上来说,是因为在子类进行属性写操作时,只有使用点表达式才能通过原型链访问父类的对象的属性值。
JavaScript 秘密花园也提到了这一点:
当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined
那么,为什么不使用点表达式,就是覆盖父类的同名属性呢?从Javascript的设计上来说,这个是给予了我们方便:在使用第三方JS类库的时候,往往有时候他们定义的原型方法是不能满足我们的需要,但是又离不开这个类库,直接对属性或者方法进行写操作就可以重写他们的原型中的一个或者多个属性或function。
注:以上说的点表达式不是绝对的,比如访问数组元素就是不用点表达式的。
写这篇文章时参考了下面的文章,我列出了一些关键的要点,需要的可以自行参考:
AngularJS子级作用域问题(ngInclude;ngView;ngSwitch;ngRepeat) - 简书
AngularJS的继承是通过javascript的原型继承方式实现的,进行原型继承即意味着父作用域在子作用域的原型链上。因为原型链的检索只会在属性检索的时候触发,不会在改变属性值的时候触发。所以我们需要把原始类型转换成对象,把值绑定在对象的属性上。
Feenan's Blog-[译]深入理解ng里的scope-程序人生
scope里的原型继承比较容易理解,一般情况下都不需要你去了解它的实现,但是当你在子作用域里绑定父作用域里的基本数据类型(比如,整型,字符串,布尔型)的时候,这种情况下就会出现问题,你会发现它并没有像你指望的那样去运行,当修改子作用域里的基本数据类型时,并不会修改父作用域,而是在子作用域里创建一个新的属性,这并不是ng干的,这只是js里原型继承所导致的,关于这个问题,可以看看这个例子
-
AngularJS中scope基于原型链的继承 // 进击的马斯特
- 读子类的属性时,子类有这个属性(hasOwnProperty)的时候则读子类自己的,子类没有的时候读父类的,不管子类有没有这个属性,在子类上都不会有新属性被创建。
- 写子类的属性时,如果子类有这个属性(hasOwnProperty)则写子类的,子类没有的话就会在子类上新建一个同名的新属性,而父类继承过来的属性被隐藏。