使用defineProperty實現自定義setter, 簡化前端Angular的重構工作

無風聽海發表於2021-04-29

一、問題場景

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);
    }

原文地址

相關文章