一、問題場景
Angular的雙向繫結給我們開發提供了很大的遍歷,將父scope的引用變數作為引數傳遞給子指令,這樣就可以方便的在父作用域內進行業務操作,資料變更會自動傳遞到子指令。但是如果你基於一個已有的複雜業務模組進行擴充套件開發,同時要將耦合其中一個功能提取為指令,這個時候就涉及到引數的傳遞問題。最簡的方式就是直接將已有的根資料物件作為引數直接傳遞過去,引數攜帶資料大而全,指令內部肯定資料夠用不會報錯,但是缺點就是引數結構複雜,使用者無法準確的連線所需引數,極大地降低了指令的可用性;
二、問題分析
新開發的指令一般都是隻傳遞需要的引數,一般結構形式比較簡單,直接在原有的複雜結構中找到對應的欄位直接賦值到新的引數即可;但是這裡會涉及到一些非引用型別的欄位,會給Angular的雙向繫結帶來問題;這就需要兩者之間進行資料的同步更新,最簡單的方式就是找到原來資料變更的地方,然後給指令的引數進行賦值即可,但是這種方式不僅繁瑣容易出錯,而且給以後的開發維護帶來不便。
那麼有沒有更好的方式來解決這個問題呢,從程式碼的重構實踐來說,最好將資料的變更封裝在一個方法中,方便對資料欄位的訪問控制,那麼js是否提供有相關的機制來實現欄位的自定義settor和getter呢?
三、js的屬性描述符
從ES5開始,所有的屬性都具備了屬性描述符。我們可以通過getOwnPropertyDescriptor獲取屬性的描述符資訊,這個普通的物件屬性對應的屬性描述符可不僅僅只是一個字串。它還包含另外三個特性:writable(可寫)、enumerable(可列舉)和configurable(可配置)。
var myObj ={
name:'mango'
};
descriptor = Object.getOwnPropertyDescriptor(myObj,'name');
console.log(descriptor);
// {
// "value": "mango",
// "writable": true,
// "enumerable": true,
// "configurable": true
// }
在建立普通屬性時屬性描述符會使用預設值,我們也可以使用Object.defineProperty(..)來新增一個新屬性或者修改一個已有屬性(如果它是configurable)並對特性進行設定。我們使用defineProperty(..)給myOb新增了一個普通的屬性並顯式指定了一些特性。然而,一般來說我們不會使用這種方式,除非想修改屬性描述符。
var myObj = {};
Object.defineProperty(myObj, 'name', {
value: 'apple',
writable: true,
configurable: true,
enumerable: true
});
//apple
writable決定是否可以修改屬性的值。
var myObj = {};
Object.defineProperty(myObj, 'name', {
value: 'apple',
writable: false,
configurable: true,
enumerable: true
});
myObj.name='pear'
console.log(myObj.name)
//apple
可以看到,我們對於屬性值的修改靜默失敗(silently failed)了。如果在嚴格模式下,這種方法會出錯,TypeError錯誤表示我們無法修改一個不可寫的屬性。
'use strict'
var myObj = {};
Object.defineProperty(myObj, 'name', {
value: 'apple',
writable: false,
configurable: true,
enumerable: true
});
myObj.name='pear'
console.log(myObj.name)
// Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
通過configurable可以控制是否可以修改屬性描述符,同時會限制不能刪除屬性;
enumerable控制這個屬性是否會出現在物件的屬性列舉中,比如說for..in迴圈。如果把enumerable設定成false,這個屬性就不會出現在列舉中,雖然仍然可以正常訪問它。相對地,設定成true就會讓它出現在列舉中。
在ES5中可以使用getter和setter部分改寫預設操作,但是隻能應用在單個屬性上,無法應用在整個物件上。getter是一個隱藏函式,會在獲取屬性值時呼叫。setter也是一個隱藏函式,會在設定屬性值時呼叫。當你給一個屬性定義getter、setter或者兩者都有時,這個屬性會被定義為“訪問描述符”(和“資料描述符”相對)。對於訪問描述符來說,JavaScript會忽略它們的value和writable特性,取而代之的是關心set和get(還有configurable和enumerable)特性。
var myObj = {
_no:0,
get no(){
return this._no;
},
set no(val){
this._no = val;
}
}
Object.defineProperty(myObj, 'name', {
get:function(){
return 'mango' + this.no;
}
});
console.log(myObj.no);
console.log(myObj.name)
myObj.no = 3
console.log(myObj.name)
// 0
// mango0
// mango3
四、解決方案
通過了解js提供的屬性描述符機制,我們可以通過defineProperty來給需要同步的欄位新增getter和setter訪問控制器,實現方式如下
function addPropertyControl(obj, property, syncObj, syncProperty) {
syncProperty = syncProperty ? syncProperty : property;
var dProperty = '_' + property;
obj[dProperty] = obj[property];
Object.defineProperty(obj, property, {
get: function () {
return obj[dProperty];
},
set: function (value) {
syncObj[syncProperty] = value;
obj[dProperty] = value;
}
});
}
function autoSyncDataModel() {
var sObj = $scope.dataModel;
var syncObj = $scope.option;
addPropertyControl(sObj, 'name', syncObj);
addPropertyControl(sObj, 'parents', syncObj);
addPropertyControl(sObj, 'age', syncObj);
addPropertyControl(sObj, 'isMan', syncObj);
}