深刻理解 Object.defineProperty 及實現數據雙向綁定

Object.defineProperty() 和 Proxy 對象,均可以用來對數據的劫持操做。何爲數據劫持呢?就是在咱們訪問或者修改某個對象的某個屬性的時候,經過一段代碼進行攔截行爲,而後進行額外的操做,而後返回結果。那麼vue中雙向數據綁定就是一個典型的應用。javascript

Vue2.x 是使用 Object.defindProperty(),來進行對對象的監聽的。
Vue3.x 版本以後就改用Proxy進行實現的。
下面咱們先來理解下Object.defineProperty做用。css

一: 理解Object.defineProperty的語法和基本做用。html

在理解以前,咱們先來看看一個普通的對象,對象它是由多個名/值對組成的無序集合。對象中每一個屬性對於任意類型的值。
好比如今咱們想建立一個簡單的對象,能夠簡單的以下代碼:vue

複製代碼
const obj = new Object; // 或 const obj = {};

obj.name = 'kongzhi';

console.log(obj.name);  // 在控制檯中會打印 kongzhi

obj.xxx = function() {
  console.log(111);
}

// 調用 xxx 方法
obj.xxx();  // 在控制檯中會打印 111
複製代碼

可是除了上面添加對象屬性以外,咱們還可使用 Object.defineProperty 來定義新的屬性或修改原有的屬性。最終會返回該對象。
接下來咱們慢慢來理解下該用法。java

基本語法:git

Object.defineProperty(obj, prop, descriptor);

基本的參數解析以下:github

obj: 能夠理解爲目標對象。
prop: 目標對象的屬性名。
descriptor: 對屬性的描述。數組

那麼對於第一個參數obj 和 prop參數,咱們很容易理解,好比上面的實列demo,咱們定義的 obj對象就是第一個參數的含義,咱們在obj中定義的name屬性和xxx屬性是prop的含義,那麼第三個參數描述符是什麼含義呢?app

descriptor: 屬性描述符,它是由兩部分組成,分別是:數據描述符和訪問器描述符,數據描述符的含義是:它是一個包含屬性的值,並說明這個屬性值是可讀或不可讀的對象。訪問器描述符的含義是:包含該屬性的一對 getter/setter方法的對象。框架

下面咱們繼續來理解下 數據描述符 和 訪問器描述符具體包含哪些配置項含義及用法。

1.1 數據描述符

複製代碼
const obj = {
  name: 'kongzhi'
};

// 對obj對象已有的name屬性添加數據描述
Object.defineProperty(obj, 'name', {
  configurable: true | false,
  enumerable: true | false,
  value: '任意類型的值',
  writable: true | false
});

// 對obj對象添加新屬性的描述
Object.defineProperty(obj, 'newAttr', {
  configurable: true | false,
  enumerable: true | false,
  value: '任意類型的值',
  writable: true | false
});
複製代碼

如上代碼配置,數據描述符有如上configurable,enumerable,value 及 writable 配置項。

下面咱們來看下 每一個描述符中每一個屬性的含義:

1)value

屬性對應的值,值的類型能夠是任意類型的。好比我先定義一個obj對象,裏面有一個屬性 name 值爲 'kongzhi', 如今咱們經過以下代碼改變 obj.name 的值,以下代碼:

複製代碼
const obj = {
  name: 'kongzhi'
};

// 對obj對象已有的name屬性添加數據描述
Object.defineProperty(obj, 'name', {
  value: '1122'
});

console.log(obj.name); // 輸出 1122
複製代碼

若是上面我不設置 value描述符值的話,那麼它返回的值仍是 kongzhi 的。好比以下代碼:

複製代碼
const obj = {
  name: 'kongzhi'
};

// 對obj對象已有的name屬性添加數據描述
Object.defineProperty(obj, 'name', {
  
});

console.log(obj.name); // 輸出 kongzhi
複製代碼

2)writable

writable的英文的含義是:'可寫的',在該配置中它的含義是:屬性的值是否能夠被重寫,設置爲true能夠被重寫,設置爲false,是不能被重寫的,默認爲false。

以下代碼:

複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi'
});

console.log(obj.name); // 輸出 kongzhi

// 改寫obj.name 的值
obj.name = 111;

console.log(obj.name); // 仍是打印出 kongzhi
複製代碼

上面代碼中 使用 Object.defineProperty 定義 obj.name 的值 value = 'kongzhi', 而後咱們使用 obj.name 進行從新改寫值,再打印出 obj.name 能夠看到 值 仍是爲 kongzhi , 這是 Object.defineProperty 中 writable 默認爲false,不能被重寫,可是下面咱們將它設置爲true,就能夠進行重寫值了,以下代碼:

複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi',
  'writable': true
});

console.log(obj.name); // 輸出 kongzhi

// 改寫obj.name 的值
obj.name = 111;

console.log(obj.name); // 設置 writable爲true的時候 打印出改寫後的值 111
複製代碼

3)enumerable

此屬性的含義是:是否能夠被枚舉,好比使用 for..in 或 Object.keys() 這樣的。設置爲true能夠被枚舉,設置爲false,不能被枚舉,默認爲false.

以下代碼:

複製代碼
const obj = {
  'name1': 'xxx'
};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi',
  'writable': true
});

// 枚舉obj的屬性
for (const i in obj) {
  console.log(i); // 打印出 name1
}
複製代碼

如上代碼,對象obj自己有一個屬性 name1, 而後咱們使用 Object.defineProperty 給 obj對象新增 name屬性,可是經過for in循環出來後能夠看到 只打印出 name1 屬性了,那是由於 enumerable 默認爲false,它裏面的值默認是不可被枚舉的。可是若是咱們將它設置爲true的話,那麼 Object.defineProperty 新增的屬性也是能夠被枚舉的,以下代碼:

複製代碼
const obj = {
  'name1': 'xxx'
};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi',
  'writable': true,
  'enumerable': true
});

// 枚舉obj的屬性
for (const i in obj) {
  console.log(i); // 打印出 name1 和 name
}
複製代碼

4) configurable

該屬性英文的含義是:可配置的意思,那麼該屬性的含義是:是否能夠刪除目標屬性。若是咱們設置它爲true的話,是能夠被刪除。若是設置爲false的話,是不能被刪除的。它默認值爲false。

好比以下代碼:

複製代碼
const obj = {
  'name1': 'xxx'
};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi',
  'writable': true,
  'enumerable': true
});

// 使用delete 刪除屬性 
delete obj.name;
console.log(obj.name); // 打印出kongzhi
複製代碼

如上代碼 使用 delete命令刪除 obj.name的話,該屬性值是刪除不了的,由於 configurable 默認爲false,不能被刪除的。
可是若是咱們把它設置爲true,那麼就能夠進行刪除了。

以下代碼:

複製代碼
const obj = {
  'name1': 'xxx'
};

Object.defineProperty(obj, 'name', {
  'value': 'kongzhi',
  'writable': true,
  'enumerable': true,
  'configurable': true
});

// 使用delete 刪除屬性 
delete obj.name;
console.log(obj.name); // 打印出undefined
複製代碼

如上就是 數據描述符 中的四個配置項的基本含義。那麼下面咱們來看看 訪問器描述符 的具體用法和含義。

1.2 訪問器描述符

訪問器描述符的含義是:包含該屬性的一對 getter/setter方法的對象。以下基本語法:

複製代碼
const obj = {};

Object.defineProperty(obj, 'name', {
  get: function() {},
  set: function(value) {},
  configurable: true | false,
  enumerable: true | false
});
複製代碼

注意:使用訪問器描述符中 getter或 setter方法的話,不容許使用 writable 和 value 這兩個配置項。

getter/setter

當咱們須要設置或獲取對象的某個屬性的值的時候,咱們可使用 setter/getter方法。

以下代碼的使用demo.

複製代碼
const obj = {};

let initValue = 'kongzhi';

Object.defineProperty(obj, 'name', {
  // 當咱們使用 obj.name 獲取該值的時候,會自動調用 get 函數
  get: function() {
    return initValue;
  },
  set: function(value) {
    initValue = value;
  }
});

// 咱們來獲取值,會自動調用 Object.defineProperty 中的 get函數方法。

console.log(obj.name); // 打印出kongzhi

// 設置值的話,會自動調用 Object.defineProperty 中的 set方法。
obj.name = 'xxxxx';

console.log(obj.name); // 打印出 xxx
複製代碼

注意:configurable 和 enumerable 配置項和數據描述符中的含義是同樣的。

1.3:使用 Object.defineProperty 來實現一個簡單雙向綁定的demo

以下代碼:

複製代碼
<!DOCTYPE html>
 <html>
    <head>
      <meta charset="utf-8">
      <title>標題</title>
    </head>
    <body>
      <input type="text" id="demo" />
      <div id="xxx">{{name}}</div>

      <script type="text/javascript">
        const obj = {};
        Object.defineProperty(obj, 'name', {
          set: function(value) {
            document.getElementById('xxx').innerHTML = value;
            document.getElementById('demo').value = value;
          }
        });
        document.querySelector('#demo').oninput = function(e) {
          obj.name = e.target.value;
        }
        obj.name = '';
      </script>
    </body>
</html>
複製代碼

查看效果

1.4 Object.defineProperty 對數組的監聽

看以下demo代碼來理解下對數組的監聽的狀況。

複製代碼
const obj = {};

let initValue = 1;

Object.defineProperty(obj, 'name', {
  set: function(value) {
    console.log('set方法被執行了');
    initValue = value;
  },
  get: function() {
    return initValue;
  }
});

console.log(obj.name); // 1

obj.name = []; // 會執行set方法,會打印信息

// 給 obj 中的name屬性 設置爲 數組 [1, 2, 3], 會執行set方法,會打印信息
obj.name = [1, 2, 3];

// 而後對 obj.name 中的某一項進行改變值,不會執行set方法,不會打印信息
obj.name[0] = 11;

// 而後咱們打印下 obj.name 的值
console.log(obj.name);

// 而後咱們使用數組中push方法對 obj.name數組添加屬性 不會執行set方法,不會打印信息
obj.name.push(4);

obj.name.length = 5; // 也不會執行set方法
複製代碼

如上執行結果咱們能夠看到,當咱們使用 Object.defineProperty 對數組賦值有一個新對象的時候,會執行set方法,可是當咱們改變數組中的某一項值的時候,或者使用數組中的push等其餘的方法,或者改變數組的長度,都不會執行set方法。也就是若是咱們對數組中的內部屬性值更改的話,都不會觸發set方法。所以若是咱們想實現數據雙向綁定的話,咱們就不能簡單地使用 obj.name[1] = newValue; 這樣的來進行賦值了。那麼對於vue這樣的框架,那麼通常會重寫 Array.property.push方法,而且生成一個新的數組賦值給數據,這樣數據雙向綁定就觸發了。

所以咱們須要從新編寫數組的push方法來實現數組的雙向綁定,咱們能夠參照以下方法來理解下。

1) 重寫編寫數組的方法:

複製代碼
const arrPush = {};

// 以下是 數組的經常使用方法
const arrayMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
// 對數組的方法進行重寫
arrayMethods.forEach((method) => {

  const original = Array.prototype[method]; 
  arrPush[method] = function() {
    console.log(this);
    return original.apply(this, arguments);
  }
});

const testPush = [];
// 對 testPush 的原型 指向 arrPush,所以testPush也有重寫後的方法
testPush.__proto__ = arrPush;

testPush.push(1); // 打印 [], this指向了 testPush

testPush.push(2); // 打印 [1], this指向了 testPush
複製代碼

2)使用 Object.defineProperty 對數組方法進行監聽操做。

所以咱們須要把上面的代碼繼續修改下進行使用 Object.defineProperty 進行監聽便可:

Vue中的作法以下, 代碼以下:

複製代碼
function Observer(data) {
  this.data = data;
  this.walk(data);
}

var p = Observer.prototype;

var arrayProto = Array.prototype;

var arrayMethods = Object.create(arrayProto);

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(function(method) {
  // 使用 Object.defineProperty 進行監聽
  Object.defineProperty(arrayMethods, method, {
    value: function testValue() {
      console.log('數組被訪問到了');
      const original = arrayProto[method];
      // 使類數組變成一個真正的數組
      const args = Array.from(arguments);
      original.apply(this, args);
    }
  });
});

p.walk = function(obj) {
  let value;
  for (let key in obj) {
    // 使用 hasOwnProperty 判斷對象自己是否有該屬性
    if (obj.hasOwnProperty(key)) {
      value = obj[key];
      // 遞歸調用,循環全部的對象
      if (typeof value === 'object') {
        // 而且該值是一個數組的話
        if (Array.isArray(value)) {
          const augment = value.__proto__ ? protoAugment : copyAugment;
          augment(value, arrayMethods, key);
          observeArray(value);
        }
        /* 
         若是是對象的話,遞歸調用該對象,遞歸完成後,會有屬性名和值,而後對
         該屬性名和值使用 Object.defindProperty 進行監聽便可
         */
        new Observer(value);
      }
      this.convert(key, value);
    }
  }
}

p.convert = function(key, value) {
  Object.defineProperty(this.data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log(key + '被訪問到了');
      return value;
    },
    set: function(newVal) {
      console.log(key + '被從新設置值了' + '=' + newVal);
      // 若是新值和舊值相同的話,直接返回
      if (newVal === value) return;
      value = newVal;
    }
  });
}

function observeArray(items) {
  for (let i = 0, l = items.length; i < l; i++) {
    observer(items[i]);
  }
}

function observer(value) {
  if (typeof value !== 'object') return;
  let ob = new Observer(value);
  return ob;
}

function def (obj, key, val) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: true,
    writable: true,
    configurable: true
  })
}

// 兼容不支持 __proto__的方法
function protoAugment(target, src) {
  target.__proto__ = src;
}

// 不支持 __proto__的直接修改先關的屬性方法
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}


// 下面是測試數據

var data = {
  testA: {
    say: function() {
      console.log('kongzhi');
    }
  },
  xxx: [{'a': 'b'}, 11, 22]
};

var test = new Observer(data);

console.log(test); 

data.xxx.push(33);
複製代碼

 

轉載來自-龍恩0707-博客園地址:https://home.cnblogs.com/u/tugenhua0707/

相關文章
相關標籤/搜索