Object.defineProperty()是 ES 標準中規定的一個能夠定義對象屬性特徵的一個API,之因此會想起這個 API 是由於在上一篇探究instanceof的細節文章中,想要將右側函數的Symbol.hasInstance屬性從新定義,可是無論怎麼定義都沒有生效。具體代碼以下:javascript
var A = function() {};
var a = new A();
console.log('Symbol.hasInstance修改以前', a instanceof A)
A[Symbol.hasInstance] = function() {
return false;
};
console.log('Symbol.hasInstance修改以後', a instanceof A)
複製代碼
代碼傳送門java
不敢確定打印結果的同窗,若是是對設置 A 的Symbol.hasInstance能夠改變 instanceof 關鍵字的檢測結果有疑問可先參考個人另外一篇文章instanceof使用中可能漏掉的一點細節,若是是隻對第二個打印結果有疑問的能夠繼續往下讀。函數
這裏能夠明確前言中代碼打印的兩次結果均是一致的,都是true。瞭解的同窗都知道當咱們定義了一個對象的Symbol.hasInstance屬性後,咱們是能夠在函數中自定義instanceof返回結果的。在上面的代碼中我設置的是直接返回false,也就是說只要函數個人屬性設置生效了,第二次打印的結果應該是false纔對。post
console.log(Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance));
// {
// configurable: false,
// enumerable: false,
// value: function [Symbol.hasInstance]() { [native code] },
// writable: false
// }
var a = function() {}
console.log(Object.getOwnPropertyDescriptor(a, Symbol.hasInstance)) // undefined
a[Symbol.hasInstance] = function() {
return false;
};
console.log(Object.getOwnPropertyDescriptor(a, Symbol.hasInstance)) // undefined
複製代碼
代碼傳送門ui
開始時我也是有點費解,隨後瞭解到是函數對象的原型對象上設置了Symbol.hasInstance屬性的writable是false,上面代碼的運行結果很好的驗證了這一點。可是原型上設置的屬性特徵值,爲何繼承了這個原型對象的對象也設置不了這個屬性呢,該好好看看Object.defineProperty()的具體特性了。this
下面列舉了可能會被使用的屬性特徵:spa
對象的屬性可分爲兩類,一類是直接設置值的屬性,被稱爲數據描述符,咱們大多數時候使用的都是數據描述符;另外一類是存取描述符,是由setter-getter函數對描述的屬性。實際上這兩類屬性的產生,是由於其使用的屬性特徵值不一樣。prototype
上述1,2特徵值是兩類對象屬性均可以使用的特徵值雙向綁定
該特徵值的做用主要是描述該屬性被設置的特徵值否能夠被修改,默認狀況下是false,也就是說,不設置這個值爲true,這個屬性的特徵值就沒法再被定義了,再定義的話會報錯,具體可參考以下代碼及運行結果。code
var a = {};
Object.defineProperty(a, 'confName', {
configurable: true,
value: 3
});
console.log('a.confName的特徵描述值', Object.getOwnPropertyDescriptor(a, 'confName'));
Object.defineProperty(a, 'confName', {
value: 5
})
console.log('a.confName的值', a.confName); // 5
Object.defineProperty(a, 'name', {
value: 3
});
console.log('a.name的特徵描述值', Object.getOwnPropertyDescriptor(a, 'name'));
Object.defineProperty(a, 'name', {
value: 5
}); // 不出意外這句會報錯
複製代碼
Tips:這段代碼中有一個關於 writable 的小細節,將在數據描述符小節中解釋。
該特徵值沒有太多說的,就是定義該屬性是否可枚舉。粗暴點來講,就是當咱們使用for...of,for...in等遍歷對象屬性的手段時是否能夠獲取到該屬性。若是其值爲true時就是可枚舉的,爲false不可枚舉。
若是對象屬性的特徵值使用了 value 或者 writable,那麼這個對象屬性就屬於數據描述符。
value 描述了該屬性是什麼值,其值能夠是任何 JS 中規定的數據類型。
這個是一個比較有意思的特徵值,其能夠描述數據描述符的 value 是否可使用 . 賦值修改,爲 true 表示能夠修改,false表示不能夠修改,數據描述符中該值默認是 false。
這裏有意思的點在於,當你設置writable爲false是並不能徹底保證該屬性的值不會被修改,而是必須配合 configurable 爲false才能夠徹底保證屬性不會被修改。上面強調了 value 是否可使用 . 賦值修改,除了這種方式咱們還可使用configurable代碼示例中的方式來修改屬性的值。具體可參考如下代碼:
var b = {};
Object.defineProperty(b, 'name', {
configurable: true,
value: 3
});
b.name = 9;
console.log('使用 = 從新賦值b.name', b.name); // 3 對象屬性不可 . 賦值修改時,不會報錯,只是值不會發生改變
Object.defineProperty(b, 'name', {
value: 9
});
console.log('使用definePeoperty從新賦值b.name', b.name); // 9 賦值成功
複製代碼
若是對象屬性的特徵值使用了 get 或者 set,那麼這個對象屬性就屬於存取描述符。
值得注意的是,當咱們在get和set中使用this時,this綁定的是當前使用該屬性的對象。不必定是設置該值的對象,也就是一個對象a的某個屬性name設置了get和set,這個對象是另外一個對象b的原型,那麼在b上設置name屬性時,調用的set函數中的this綁定的是吧,而不是設置了該特徵值的a。下面上代碼:
var c = {};
Object.defineProperty(c, 'name', {
get: function() {
return this.test
},
set: function(value) {
this.test = value;
}
});
var b = {};
Object.setPrototypeOf(b, c);
b.name = 'b'; // 此處會調用name屬性的set函數
// 會將'b'賦值給this.test,而this綁定的是b
// 因此至關於在b上設置了test屬性並賦值爲'b'
console.log('b.name', b.name); // b 出現這個結果是由於調用了name屬性設置的get函數
//get函數返回結果是this.test而this綁定的是b
console.log('b.test', b.test); // b 這個test屬性是設置在b對象上的
console.log('c.name', c.name); // undefined
console.log('c.test', c.test); // undefined
var s = 's';
var d = {};
Object.defineProperty(d, 'name', {
get: function() {
return s
},
set: function(value) {
s = value
}
});
var e = {};
Object.setPrototypeOf(e, d);
e.name = 'e'; // 此處會調用set函數,set函數將s賦值爲'e'
console.log('s的值', s); // 'e' 變量s的值由於set函數的調用,而被設置爲'e'
console.log('e.name', e.name); // 'e' 此處的name屬性不是在對象e上的
//而是經過委託查詢到的其原型對象d上的那麼屬性調用get函數返回的值
console.log('d.name', d.name); // 'e' 此處name屬性是在對象d上真實存在的
複製代碼
經過上面的栗子能夠看出,當對象的原型對象設置了某個屬性的get或set函數時,對象再使用該屬性時都會調用set或get函數,這是的調用效果相似set.call(b, value),指定了this是當前對象,可是調用的函數仍是在對象原型上的那個屬性的set函數。
若是一個描述符不具備value,writable,get 和 set 任意一個關鍵字,那麼它將被認爲是一個數據描述符。若是一個描述符同時有(value或writable)和(get或set)關鍵字,將會產生一個異常。
對象的屬性設置的特徵值是可能會被以其爲原型的對象繼承的,不論是在賦值仍是在獲取值時,都有可能會產生影響。
這裏的繼承指的是對象設置某個屬性時,該屬性的設置會受到其原型對象上的該屬性設置的特徵值的影響。
writable設置爲false時,以設置該屬性的對象爲原型的對象,在設置該屬性時也會沒法設置該值,這不會產生報錯,只是屬性設置一直無效。
var f = {};
Object.defineProperty(f, 'name', {
value: 'f'
});
var g = {};
Object.setPrototypeOf(g, f);
console.log('g.name', g.name); // 'f'
g.name = 'g';
console.log('g.name', g.name); // 'g'
複製代碼
只有writable設置爲false時,writable會表現出繼承性,而爲true時則對以其爲原型的對象沒有影響。平時咱們直接設置的對象屬性的writable就是true,回想一下平時的使用確實沒有設置不了對象屬性的狀況。
var a = {
name: 'a'
}
a.id = 'a';
console.log(Object.getOwnPropertyDescriptor(a, 'name'))
//configurable: true
//enumerable: true
//value: "a"
//writable: true
console.log(Object.getOwnPropertyDescriptor(a, 'id'))
//configurable: true
//enumerable: true
//value: "a"
//writable: true
var b = {};
Object.setPrototypeOf(b, a);
console.log(b.name); // 'a'
b.name = 'b';
console.log(b.name); // 'b'
console.log(a.name); // 'a'
複製代碼
value的繼承性主要表如今原型鏈的繼承上(就是原型繼承,能夠參考個人另外一篇理解原型實際上是理解原型鏈)。
屬性的set和get特徵值一樣是具備繼承性的,且與writable的繼承性有條件不一樣,set和get會一直被繼承,只要對象是以設置了該特徵值的屬性的對象爲原型,對象設置或者獲取該屬性的值時都會調用原型上的get和set函數。
var hValue;
var h = {};
Object.defineProperty(h, 'name', {
set: function(value) {
console.log('h set調用');
hValue = value;
},
get: function() {
console.log('h get調用')
return hValue;
}
});
var i = {};
Object.setPrototypeOf(i, h);
i.name = 'i'; // 會調用h上的set打印'h set調用'
i.test = 'testi';
console.log('hValue', hValue); // 'i'
console.log('i.name', i.name); // 'i' 會調用h上的get打印'h get調用'
console.log('h.name', h.name); // 'i' 會調用h上的get打印'h get調用'
var jValue;
var j = {};
Object.defineProperty(j, 'name', {
get: function() {
console.log('j get調用')
return jValue;
}
});
var k = {};
Object.setPrototypeOf(k, j);
k.name = 'k'; // 會調用h上的set打印'h set調用'
k.test = 'testk';
console.log('jValue', jValue); // 'k'
console.log('k.name', k.name); // 'k' 會調用h上的get打印'j get調用'
console.log('j.name', j.name); // 'k' 會調用h上的get打印'j get調用'
複製代碼
h是i的原型對象,h的name屬性設置了get和set函數,這種設置會讓i對象上沒法再使用.爲name屬性賦值,獲取name屬性值時也只會經過原型委託查找到h上的name。若是在i上使用Object.defineProperty()來定義一個那麼屬性,那麼這個屬性是能夠被定義在對象i上的。
var hValue;
var h = {};
Object.defineProperty(h, 'name', {
set: function(value) {
console.log('h set調用');
hValue = value;
},
get: function() {
console.log('h get調用')
return hValue;
}
});
var i = {};
Object.setPrototypeOf(i, h);
var iValue;
Object.defineProperty(i, 'name', {
set: function(value) {
console.log('i set調用');
iValue = value;
},
get: function() {
console.log('i get調用')
return iValue;
}
});
i.name = 'i'; // 會調用i上的set函數,打印'i set調用'
console.log('iValue', iValue); // 'i'
console.log('i.name', i.name); // 'i' 會調用i上的get打印'i get調用'
console.log('h.name', h.name); // 'i' 會調用h上的get打印'h get調用'
複製代碼
經過Object.defineProperty()設置的對象屬性依然是聽從原型繼承規則,查找屬性值會先從對象自身查找屬性,若是查找不到經過原型鏈向上查找,直到查找到原型鏈頂端,而設置對象屬性時只能設置到對象自己。只不過這個原型繼承是創建在子對象的屬性設置一樣是經過Object.defineProperty()定義的屬性。
Object.defineProperty()定義屬性某種程度上是給程序提供了一個能夠去定義屬性訪問器行爲的接口。
value能夠定義使用屬性訪問器獲取對象屬性時獲取到的值,writable 能夠定義使用屬性訪問器設置屬性的值是否被容許;get 能夠定義使用屬性訪問器獲取對象屬性時再作一些額外操做,set 能夠定義使用屬性訪問器給對象屬性賦值時進行一些額外的操做,例如 VUE2 中的雙向綁定機制的運用。
其實屬性特徵值的繼承性在獲取屬性值時的表現與原型繼承基本一致,與原型原型繼承不一樣的是在使用 屬性訪問器給對象屬性賦值時,原型對象上某屬性的writable爲false會被其子對象繼承致使子對象沒法使用屬性訪問器對該屬性從新賦值,子對象上也沒法設置屬性;原型上的某屬性的set會被其子對象繼承,子對象使用屬性訪問器設置對象的該屬性時會調用原型上的set函數來完成賦值操做。最後,當對象原型和其子對象的屬性設置都是用Object.defineProperty()來定義時,屬性的特徵值是不表現繼承性的。