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

起因是在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)则写子类的,子类没有的话就会在子类上新建一个同名的新属性,而父类继承过来的属性被隐藏。
  • 深入理解Angular作用域 - 简书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容