我爱编程

彻底理解Angular中scope的属性更新问题

2016-12-13  本文已影响308人  千若逸

起因是在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。

注:以上说的点表达式不是绝对的,比如访问数组元素就是不用点表达式的。

写这篇文章时参考了下面的文章,我列出了一些关键的要点,需要的可以自行参考:

上一篇下一篇

猜你喜欢

热点阅读