Vue 中 數據劫持 Object.defineProperty()

本文能幫你瞭解到什麼?
我們都知道vue是通過數據劫持的方式來做數據綁定的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一,本文將由淺到深帶你理解和使用defineProperty

Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。

語法

Object.defineProperty(obj, prop, descriptor)
參數

obj

要在其上定義屬性的對象。

prop

要定義或修改的屬性的名稱。

descriptor

將被定義或修改的屬性描述符。

返回值

被傳遞給函數的對象。

在ES6中,由於 Symbol類型的特殊性,用Symbol類型的值來做對象的key與常規的定義或修改不同,而Object.defineProperty 是定義key爲Symbol的屬性的方法之一。

描述

該方法允許精確添加或修改對象的屬性。通過賦值操作添加的普通屬性是可枚舉的,能夠在屬性枚舉期間呈現出來(for...inObject.keys方法), 這些屬性的值可以被改變,也可以被刪除。這個方法允許修改默認的額外選項(或配置)。默認情況下,使用 Object.defineProperty()添加的屬性值是不可修改的。


屬性描述符

對象裏目前存在的屬性描述符有兩種主要形式:數據描述符存取描述符。數據描述符是一個具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。

數據描述符和存取描述符均具有以下可選鍵值(默認值是在使用Object.defineProperty()定義屬性的情況下):

configurable

當且僅當該屬性的 configurable 爲 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。

enumerable

當且僅當該屬性的enumerable爲true時,該屬性才能夠出現在對象的枚舉屬性中。默認爲 false。

數據描述符同時具有以下可選鍵值:

value

該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined

writable

當且僅當該屬性的writabletrue時,value才能被賦值運算符改變。默認爲 false。

存取描述符同時具有以下可選鍵值:

get

一個給屬性提供 getter 的方法,如果沒有 getter 則爲undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,但是會傳入this對象(由於繼承關係,這裏的this並不一定是定義該屬性的對象)。

默認爲 undefined

set

一個給屬性提供 setter 的方法,如果沒有 setter 則爲undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一參數,即該屬性新的參數值。

默認爲undefined

描述符可同時具有的鍵值

configurable enumerable value writable get set
數據描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

如果一個描述符不具有value,writable,get 和 set 任意一個關鍵字,那麼它將被認爲是一個數據描述符。如果一個描述符同時有(value或writable)和(get或set)關鍵字,將會產生一個異常。

記住,這些選項不一定是自身屬性,如果是繼承來的也要考慮。爲了確認保留這些默認值,你可能要在這之前凍結 Object.prototype,明確指定所有的選項,或者通過 Object.create(null)將__proto__屬性指向null

// 使用 __proto__
var obj = {};
var descriptor = Object.create(null); // 沒有繼承的屬性
// 默認沒有 enumerable,沒有 configurable,沒有 writable
descriptor.value = 'static';
Object.defineProperty(obj, 'key', descriptor);

// 顯式
Object.defineProperty(obj, "key", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static"
});

// 循環使用同一對象
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: null
    }
  );
  d.value = value;
  return d;
}
// ... 並且 ...
Object.defineProperty(obj, "key", withValue("static"));

// 如果 freeze 可用, 防止代碼添加或刪除對象原型的屬性
// (value, get, set, enumerable, writable, configurable)
(Object.freeze||Object)(Object.prototype);

示例

如果你想了解如何使用Object.defineProperty方法和類二進制標記語法,看看這篇文章。

創建屬性

如果對象中不存在指定的屬性,Object.defineProperty()就創建這個屬性。當描述符中省略某些字段時,這些字段將使用它們的默認值。擁有布爾值的字段的默認值都是falsevaluegetset字段的默認值爲undefined。一個沒有get/set/value/writable定義的屬性被稱爲“通用的”,並被“鍵入”爲一個數據描述符。

var o = {}; // 創建一個新對象

// 在對象中添加一個屬性與數據描述符的示例
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

// 對象o擁有了屬性a,值爲37

// 在對象中添加一個屬性與存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

o.b = 38;
// 對象o擁有了屬性b,值爲38

// o.b的值現在總是與bValue相同,除非重新定義o.b

// 數據描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
  value: 0x9f91102, 
  get: function() { 
    return 0xdeadbeef; 
  } 
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
修改屬性

如果屬性已經存在,Object.defineProperty()將嘗試根據描述符中的值以及對象當前的配置來修改這個屬性。如果舊描述符將其configurable 屬性設置爲false,則該屬性被認爲是“不可配置的”,並且沒有屬性可以被改變(除了單向改變writablefalse)。當屬性不可配置時,不能在數據和訪問器屬性類型之間切換。

當試圖改變不可配置屬性(除了value和writable 屬性之外)的值時會拋出TypeError,除非當前值和新值相同。

Writable 屬性

writable屬性設置爲false時,該屬性被稱爲“不可寫”。它不能被重新分配。

var o = {}; // Creates a new object

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // logs 37
o.a = 25; // No error thrown
// (it would throw in strict mode,
// even if the value had been the same)
console.log(o.a); // logs 37. The assignment didn't work.

// strict mode
(function() {
  'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // throws TypeError: "b" is read-only
  return o.b; // returns 2 without the line above
}());

如示例所示,試圖寫入非可寫屬性不會改變它,也不會引發錯誤。

Enumerable 特性

enumerable定義了對象的屬性是否可以在 for...in循環和 Object.keys()中被枚舉。

var o = {};
Object.defineProperty(o, "a", { value : 1, enumerable:true });
Object.defineProperty(o, "b", { value : 2, enumerable:false });
Object.defineProperty(o, "c", { value : 3 }); // enumerable defaults to false
o.d = 4; // 如果使用直接賦值的方式創建對象的屬性,則這個屬性的enumerable爲true

for (var i in o) {    
  console.log(i);  
}
// 打印 'a' 和 'd' (in undefined order)

Object.keys(o); // ["a", "d"]

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
Configurable 特性

configurable特性表示對象的屬性是否可以被刪除,以及除valuewritable特性外的其他特性是否可以被修改。

var o = {};
Object.defineProperty(o, "a", { get : function(){return 1;}, 
                                configurable : false } );

// throws a TypeError
Object.defineProperty(o, "a", {configurable : true}); 
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true}); 
// throws a TypeError (set was undefined previously) 
Object.defineProperty(o, "a", {set : function(){}}); 
// throws a TypeError (even though the new get does exactly the same thing) 
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});

console.log(o.a); // logs 1
delete o.a; // Nothing happens
console.log(o.a); // logs 1

如果o.aconfigurable屬性爲true,則不會拋出任何錯誤,並且該屬性將在最後被刪除。

添加多個屬性和默認值

考慮特性被賦予的默認特性值非常重要,通常,使用點運算符和Object.defineProperty()爲對象的屬性賦值時,數據描述符中的屬性默認值是不同的,如下例所示。

var o = {};

o.a = 1;
// 等同於 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : true,
  configurable : true,
  enumerable : true
});


// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同於 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : false,
  configurable : false,
  enumerable : false
});
一般的 Setters 和 Getters

下面的例子展示瞭如何實現一個自存檔對象。 當設置temperature屬性時,archive數組會獲取日誌條目。

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

var pattern = {
    get: function () {
        return 'I alway return this string,whatever you have assigned';
    },
    set: function () {
        this.myname = 'this is my name string';
    }
};


function TestDefineSetAndGet() {
    Object.defineProperty(this, 'myproperty', pattern);
}


var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';

// 'I alway return this string,whatever you have assigned'
console.log(instance.myproperty);
// 'this is my name string'
console.log(instance.myname);繼承屬性
繼承屬性

如果訪問者的屬性是被繼承的,它的getset 方法會在子對象的屬性被訪問或者修改時被調用。如果這些方法用一個變量存值,該值會被所有對象共享。

function myclass() {
}

var value;
Object.defineProperty(myclass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1

這可以通過將值存儲在另一個屬性中解決。在getset方法中,this 指向某個被訪問和修改屬性的對象。

function myclass() {
}

Object.defineProperty(myclass.prototype, "x", {
  get() {
    return this.stored_x;
  },
  set(x) {
    this.stored_x = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // undefined

不像訪問者屬性,值屬性始終在對象自身上設置,而不是一個原型。然而,如果一個不可寫的屬性被繼承,它仍然可以防止修改對象的屬性。

function myclass() {
}

myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
  writable: false,
  value: 1
});

var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1

規範


瀏覽器兼容

兼容性問題


重定義數組對象的 length 屬性

數組的 length 屬性重定義是可能的,但是會受到一般的重定義限制。(length 屬性初始爲 non-configurable,non-enumerable 以及 writable。對於一個內容不變的數組,改變其 length 屬性的值或者使它變爲 non-writable 是可能的。但是改變其可枚舉性和可配置性或者當它是 non-writable 時嘗試改變它的值或是可寫性,這兩者都是不允許的。)然而,並不是所有的瀏覽器都允許Array.length的重定義。

在 Firefox 4 至 22 版本中嘗試去重定義數組的 length 屬性都會拋出一個 TypeError 異常。

有些版本的Chrome中,Object.defineProperty() 在某些情況下會忽略不同於數組當前length屬性的length值。有些情況下改變可寫性並不起作用(也不拋出異常)。同時,比如Array.prototype.push的一些數組操作方法也不會考慮不可讀的length屬性。

有些版本的Safari中,Object.defineProperty()在某些情況下會忽略不同於數組當前length屬性的length值。嘗試改變可寫性的操作會正常執行而不拋出錯誤,但事實上並未改變屬性的可寫性。

只在Internet Explorer 9及以後版本和Firefox 23及以後版本中,才完整地正確地支持數組length屬性的重新定義。目前不要依賴於重定義數組length 屬性能夠起作用,或在特定情形下起作用。與此同時,即使你能夠依賴於它,你也沒有合適的理由這樣做。

Internet Explorer 8 具體案例

Internet Explorer 8 實現了 Object.defineProperty() 方法,但 只能在 DOM 對象上使用。 需要注意的一些事情:

  • 嘗試在原生對象上使用 Object.defineProperty()會報錯。
  • 屬性特性必須設置一些特定的值。對於數據屬性描述符,configurable, enumerablewritable 特性必須全部設置爲 true;對於訪問器屬性描述符,configurable 必須設置爲 true,enumerable必須設置爲 false。(?) 任何試圖提供其他值(?)將導致一個錯誤拋出。
  • 重新配置一個屬性首先需要刪除該屬性。如果屬性沒有刪除,就如同重新配置前的嘗試。
學如逆水行舟,不進則退
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章