/* 原文發表在自己的博客上 歡迎踩踩 */
一個人人都要踩的坑
Angular容易上手的一個重要原因就是data binding非常簡單,當你在controller里面給scope綁定上一個object,立刻就能在view中show出來,而且也能夠非常輕松地實現two way binding。生活十分愉快。
但突然有一天,不知道從加了哪一行代碼開始,two way binding不工作了。你翻箱倒柜把書從頭翻到尾,到SO上求爺爺告奶奶,最終你發現,你遇到了一個名叫nested scope
的問題。
這個坑長什么樣
我們來舉一個不能再簡單的栗子,童鞋們可以到這里看demo。首先我們有個html頁面充當view
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input ng-model="name">
<div ng-if="includeForm">
<input ng-model="name">
</div>
</body>
接著咱們有段javascript
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.includeForm = true;
});
很容易看出,我們這個 angular app,其實就是把 $scope.name
綁定到 p
和兩個 input
element 上。初始化后,頁面是這樣的,初始值都是 world

然后我們在第一個 input box 里面將文字改成 kitty

接下來屬于高危動作,睜大你的雙眼:修改第二個 input box 里的 text,把它改成 peng 。驚人的是,Title 和 第一個 input 里的 kitty 并未隨之改變。

最后就是見證奇跡的時刻,修改第一個 input box 的值,改回成 world,Title 立刻隨著一起改變,但第二個 input box 像是與這個世界失去了聯系。

在分析上面的 case 中 到底發生了什么之前,我們一起回顧一下 JavaScript 的基礎知識 Inheritance and the prototype chain。JS 高玩自行跳過這個章節 :)
什么是prototype
我們知道在 C++/Java/C# 這樣的面向對象編程語言中,我們可以使用繼承(inheritance)來實現屬性和方法的共享,減少冗余代碼的書寫。
JavaScript 也支持繼承,但是它并沒有類的概念,而是使用 prototype 來實現這一目標。JavaScript 中的每個對象都有一個內部私有的鏈接指向另一個對象,這個對象就是原對象的原型(prototype)。這個原型對象也有自己的原型,直到對象的原型為 null 為止(也就是沒有原型)。這種一級一級的鏈結構就稱為原型鏈。
擁有了繼承之后,JavaScript 的 Object 就擁有了兩種屬性,一種是對象自身的屬性,另外一種是繼承于原型鏈上的屬性。當我們去讀取 Object 的某個屬性時,首先查看當前 Object 是否擁有該屬性,有的話返回值,如果沒有的話,找到它的 prototype,看看這個對象上是否有有該屬性。JavaScript 會順著 prototype chain 一路上去,直到找到這個屬性活著 prototype chain 到頭為止。
關于 prototype 更加詳細和通透的解釋,大家可以參考 這篇文章 和 ruanyf 老師的大作,我高中語文老是不及格,就不給大家添麻煩了。我就帶大家來看個小小的栗子。第一步,我們創建一個 object,就叫它爹吧。
> parent = { "first_name": "Peng"}
< Object {first_name: "Peng"}
爹有一個屬性叫做 first_name,值為 Peng。接著我們生一個兒子,
> child = Object.create(parent)
Object.create() 這個函數會創建一個新的 Object 并將新Object 的 prototype 指定為 傳入的參數。比如這里,我們傳入的參數是 parent,那么 child 的prototype 就是 parent,child 會從 parent 這里繼承屬性。比如:
> child.first_name
< "Peng"
child 上本身并沒有 first_name 這個屬性,但是他爹有,于是依然得到了 Peng 這個值。到這里為止,我們展示了如何從 prototype 上繼承一個 primitive value 。繼承 object property 也是一樣的。
> parent = { "name" : { "first": "peng", "last": "lv"}}
> child = Object.create(parent)
> child.name.first
< "peng"
童鞋們可以在瀏覽器的 console 里面玩一下

到這里,即使是從沒聽說過 prototype 的朋友肯定也明白了,這不就是老鼠的兒子會打洞么。但是關于 prototype 的繼承,我想把 MDN 文檔里的一句話高亮出來
Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when there is an inherited property with a getter or a setter.
最重要的就是第一句了,Setting a property to an object creates an own property。當我們去 get property 的時候,會順著 prototype chain 一直往上找,但是 set property 并不會這樣,而是為當前對象生成一個新的 property 。比如這樣:

自此 child 和 parent 就失聯了。下面我們可以來看看 Angular 的 nested scope 是怎么一回事。
Angular 如何創建child scope
在使用 Angular 的一些 built-in directive 時,比如 ng-if/ng-include/ng-repeat/ng-switch/etc 時,需要注意的一點是,Angular 會為其生成一個新的 scope,這個 scope 繼承自 外層的 scope。回到我們最上面提到的 demo
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input ng-model="name">
<div ng-if="includeForm">
<input ng-model="name">
</div>
</body>
MainCtrl
上有一個 scope 作為膠水來粘合 controller 和 view,而ng-if
又會生成一個 scope,這個 scope 向上繼承 controller 的 scope。這個繼承 Angular 是如何實現的呢,我們來看源碼
function createChildScopeClass(parent) {
function ChildScope() {
this.$$watchers = this.$$nextSibling =
this.$$childHead = this.$$childTail = null;
this.$$listeners = {};
this.$$listenerCount = {};
this.$$watchersCount = 0;
this.$id = nextUid();
this.$$ChildScope = null;
}
ChildScope.prototype = parent;
return ChildScope;
}
我們看到,創建 child scope 的時候,會把 child scope 的 prototype (原型) 設置為 parent 。根據我們上面剛剛溫習的 prototype 繼承機制,當在第二個 input box 里訪問 ng-model="name"
時,會先到 ng-if 上的 child scope 尋找 name 這個屬性,如果沒有,沿著 prototype 找到 parent scope,最終找到 name
這個屬性。
而當我們往第二個 input box 里面輸入新的值(見 第三張圖片),則觸發了 prototype 的另一個規則 Setting a property to an object creates an own property , ng-if 上的 child scope 增加了一個新的屬性 name ,parent scope 上的 name 和 child scope 的 name 從此再無瓜葛。當我們再次修改 第一個 input box 里的值時,實際上我們修改的 parent scope 上的 name ,對于 第二個 input box 來說,并沒有什么卵用。

問題到這里已經清楚了,Angular 的 nested scope 使用了 prototype 這個機制來實現 child scope 對 parent scope 的繼承,當我們修改 child scope 上的屬性時,會導致無法更新 parent scope 的屬性。那么該如何解救它們呢?
有兩招
Dot Notation 和 $parent
第一招,江湖人稱 Dot Notation
換做人能夠聽懂的語言就是,避免給 child scope 上的屬性賦值。還記得上文我們講解 prototype chain 的時候說過,屬性是 object 也可以繼承
> parent = { "name" : { "first": "peng", "last": "lv"}}
> child = Object.create(parent)
> child.name.first = "hulk"
< "hulk"
> parent.name
< Object {first: "hulk", last: "lv"}
parent 有個屬性叫 name,name 有個屬性叫 first 。如果我們修改 child.name.first ,第一步是查找 child 上的 name 屬性,沒有找到,根據 prototype chain 找到了 parent 上的 name 屬性,然后修改了它的 property first
。整個過程并沒有給 child 的 property name
賦值。
當然,如果你直接修改 child.name
,name 的繼承就消失了。
> child.name = {"first": "captain", last: "america"}
< Object {first: "captain", last: "america"}
> parent.name
< Object {first: "hulk", last: "lv"}
Dot Notation,這個名字真的是傳神啊,修改和訪問 $scope上屬性的屬性($scope.name.first),而不是直接操作 $scope的屬性($scope.name),多一個 Dot ,就解決了 two way binding 的坑。
第二招,見招拆招,使用$parent。
不是擔心修改 child scope 上的屬性么,直接訪問 parent scope 上的屬性不就行了么。我們再來看 Angular 的代碼
child.$parent = parent;
child scope 直接有個 $parent 屬性來 reference parent scope。
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input ng-model="name">
<div ng-if="includeForm">
<input ng-model="$parent.name">
</div>
</body>
這個方法過于暴力,博主不推薦使用,被同事爆的風險太高了。
每篇文章的最后總該總結點什么,不能虎頭蛇尾....
想到了!以上問題只在 Angular 1.x 出現,因為2.0開始就沒有 scope 咯~