最近在寫一個 《JavaScript API 全解析》系列(剛寫完 String,現正在寫 Object,可戳 JavaScript API 全解析),想把 MDN 推薦使用的 API 所有擼一遍,也算是給本身準備一份資料。由於 Object.defineProperty() 涉及到的知識點比較複雜,因此單獨拎出來放到這裏,歡迎你們拍磚。vue
defineProperty(o: any, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): any;
複製代碼
用於在一個對象上定義新的屬性或修改現有屬性, 並返回該對象.git
o
目標對象github
p
須要定義的屬性或方法名 (可修改既有的, 也可添加新屬性或方法)web
attributes
屬性描述符, 具體屬性以下:面試
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
複製代碼
ECMAScript 中有兩種屬性: 數據屬性
和 訪問器屬性
.數組
數據屬性包括: [[Configurable]], [[Enumerable]], [[Writable]], [[Value]]瀏覽器
訪問器屬性包括: [[Configurable]], [[Enumerable]], [[Get]], [[Set]]mvvm
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
數據屬性 | Yes | Yes | Yes | Yes | No | No |
訪問器屬性 | Yes | Yes | No | No | Yes | Yes |
簡言之, 定義了 value 或 writable , 必定不能有 get 或 set, 反之亦然, 不然報錯.函數
若是某個屬性的 configurable 爲false
, 那麼:post
delete obj.xxx
無效, 在嚴格模式下直接報錯.// 非嚴格模式下刪除一個"不可配置"的屬性會返回false
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'yancey',
configurable: false,
});
delete obj.name; // false
// obj.name並無被刪除
obj.name; // yancey
複製代碼
// 嚴格模式下刪除一個"不可配置"的屬性直接報錯
(function() {
'use strict';
var o = {};
Object.defineProperty(o, 'b', {
value: 2,
configurable: false,
});
delete o.b; // Uncaught TypeError: Cannot delete property 'b' of #<Object>
return o.b;
})();
複製代碼
false
時, 再次將它們變成true
則報錯; 但當它們是true
時, 卻能夠把它們變成false
( 注意必須是在不可配置
的前提下, 若是屬性可配置
, enumerable 和 writable 可任意切換 true 和 false)const obj = {};
Object.defineProperty(obj, 'name', {
value: 'yancey',
configurable: false,
writable: false,
});
// 當"writable"和"configurable"均爲false時, 嘗試將"writable"變爲true會報錯
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { writable: true });
複製代碼
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'yancey',
configurable: false,
writable: true,
});
// 但"writable"可成功從true切換到false
Object.defineProperty(obj, 'name', { writable: false });
複製代碼
get
和set
都會報錯, 由於二者的屬性值是一個函數,在 JS 中不可能存在一個相同的函數。:::tip REVIEW 複雜數據類型在棧
中存儲數據名和一個堆的地址, 在堆
中存儲屬性及值. 訪問時先從棧獲取地址, 再到堆中拿出相應的值. :::
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'yancey',
configurable: false,
});
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { get: function() {} });
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(obj, 'name', { set: function() {} });
複製代碼
writable
是 true, 能夠任意從新定義
value, 但當writable
是 false 時, 須要看具體數據類型. 第一個例子中, 雖然 configurable 是 false, 但只要 writable 是 true, 即可以從新定義 value; 第二個例子中, value 是 基本數據類型
, 因此再次定義 value 時只要覆蓋原值便可; 第三個例子 value 是複雜數據類型, 一樣由於 堆棧 問題而不能從新賦值.const obj = {};
Object.defineProperty(obj, 'name', {
value: [],
configurable: false,
writable: true,
});
// 任意重定義value不報錯
Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123}
// 任意重定義value不報錯
Object.defineProperty(obj, 'name', { value: {}); // {name: {}}
複製代碼
const obj = {};
Object.defineProperty(obj, 'name', {
value: 123,
configurable: false,
writable: false,
});
// 當value是基本數據類型, 用原值覆蓋不會報錯
Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123}
// 用其餘值代替必然報錯
Object.defineProperty(obj, 'name', { value: {}); // Uncaught TypeError: Cannot redefine property: name
複製代碼
const obj = {};
Object.defineProperty(obj, 'name', {
value: [],
configurable: false,
writable: false,
});
// 當value是複雜數據類型, 修改value一定報錯, 一樣是堆棧的緣由
Object.defineProperty(obj, 'name', { value: [] }); // {name: 123}
複製代碼
若是某個屬性的writable
設爲false
, 那麼該屬性將不能被賦值運算符
改變. 但屬性值假如是數組時, 將不受 push
, splice
等方法的影響.
const obj = {};
Object.defineProperty(obj, 'hobby', {
value: ['girl', 'music', 'sleep'],
writable: false,
configurable: true,
enumerable: true,
});
// "writable: false"並不對push、shift等方法起做用
obj.hobby.push('drink');
obj.hobby; // ['girl', 'music', 'sleep', 'drink']
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 當 hobby 被"賦值"給一個空數組時, 此屬性的屬性值不會被改變
obj.hobby = [];
obj.hobby; // ['girl', 'music', 'sleep', 'drink']
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 而當使用"嚴格模式"時, 給一個"不可寫"屬性賦值將直接報錯
(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; // 2
}());
複製代碼
定義了對象的屬性是否能夠在 for...in 循環和 Object.keys() 中被枚舉
const obj = {
name: 'yancey',
age: 18,
say() {
return 'say something...';
},
};
Object.defineProperty(obj, 'hobby', {
value: ['girl', 'music', 'sleep'],
enumerable: true,
});
Object.defineProperty(obj, 'income', {
value: '100,00,000',
enumerable: false,
});
// 如下迭代器均不能輸出"不可枚舉屬性", 即 income 的相關信息
for (const i in obj) {
console.log(obj[i]);
}
Object.keys(obj);
Object.values(obj);
Object.entries(obj);
複製代碼
Getter 爲讀取屬性時調用的函數. Setter 爲設置屬性是調用的函數, Setter 會有一個參數, 即設置的那個值.
下面的代碼建立一個 obj 對象, 定義了兩個屬性 name 和 _time, 注意 _time 的下劃線是一個經常使用記號, 用於表示只能經過對象方法訪問的屬性
. 而訪問器屬性 time 則包含一個 getter 函數和一個 setter 函數. getter 函數返回被修飾
的 _time 的值, setter 則根據被設置的值
修改 name. 所以當obj.time = 2
, name 會變成我爲長者+2s
. 這是使用訪問器屬性的常見方式, 即設置一個屬性的值會致使其餘屬性發生變化.
const obj = {
name: '長者',
_time: 1,
};
Object.defineProperty(obj, 'time', {
configurable: true,
get() {
return `default: ${this._time}s`;
},
set(newValue) {
if (Number(newValue)) {
this._time = newValue;
this.name = `我爲${this.name}+${newValue}s`;
}
},
});
obj.time; // 'default: 1s'
obj.time = 2; // 2
obj.name; // '我爲長者+2s'
複製代碼
再看另外一個例子, 經過 Object.defineProperty 劫持 obj.input
, 將輸入的值 set 到 id 爲 name
的標籤裏. 這裏便有了種 Vue.js 的味道, 推薦一篇文章 剖析 Vue 實現原理 - 如何實現雙向綁定 mvvm.
<p>Hello, <span id='name'></span></p>
<input type='text' id='input'>
const obj = {
input: '',
};
const inputDOM = document.getElementById('input');
const nameDOM = document.getElementById('name');
inputDOM.addEventListener('input', function (e) {
obj.input = e.target.value;
})
Object.defineProperty(obj, 'input', {
set: function (newValue) {
nameDOM.innerHTML = newValue.trim().toUpperCase();
}
})
複製代碼
最後看一個關於繼承的例子, 咱們建立了一個 Person 構造函數, 它包括兩個參數: firstName 和 lastName, 此構造函數暴露出四個屬性: firstName, lastName, fullName, species, 咱們想讓前三個屬性動態變化, 最後一個屬性是一個常量而不容許變化.
下面這段代碼顯然沒有達到想要的效果: 在嘗試修改 firstName 或 lastName 時, fullName 並無實時被更新; species屬性能隨意被改變.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = this.firstName + ' ' + this.lastName;
this.species = 'human';
}
const person = new Person('Yancey', 'Leo');
// 雖然 firstName 和 lastName 被修改了, 但 fullName 仍然是 "Yancey Leo"
person.firstName = 'Sayaka';
person.lastName = 'Yamamoto';
// 咱們定義了一個關於「人」的構造函數, 因此並不但願 species 被修改爲 fish
person.species = 'fish';
// 當咱們修改了 fullName, 也一樣但願 firstName 和 lastName 被更新
person.fullName = 'Kasumi Arimura';
複製代碼
因此咱們使用 Object.defineProperty() 重寫這個例子. 須要注意的是: 被劫持的屬性應放在原型裏. 經過下面這種方式, 即便建立多個實例, 也不會衝突, 因此能夠放心使用.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Object.defineProperty(Person.prototype, 'species', {
value: 'human',
writable: false,
});
Object.defineProperty(Person.prototype, 'fullName', {
get() {
return this.firstName + ' ' + this.lastName;
},
set(newValue) {
const newValueArr = newValue.trim().split(' ');
if (newValueArr.length === 2) {
this.firstName = newValueArr[0];
this.lastName = newValueArr[1];
}
},
});
const person = new Person('Yancey', 'Leo');
person.firstName = 'Sakaya';
person.lastName = 'Yamamoto';
person.fullName; // 'Sayaka Yamamoto'
person.fullName = 'Kasumi Arimura';
person.firstName; // 'Kasumi'
person.lastName; // 'Arimura'
person.species = 'fish';
person.species; // 'human'
複製代碼
除了 Object.defineProperty() 中的 Getter 和 Setter, 還有兩種相似的方式.
__defineGetter__ 方法能夠爲一個已經存在
的對象設置 (新建或修改) 訪問器屬性, __defineSetter__ 方法能夠將一個函數綁定在當前對象的指定屬性上, 當那個屬性被賦值時, 你所綁定的函數就會被調用.
var o = {};
o.__defineGetter__('gimmeFive', function() {
return 5;
});
o.gimmeFive; // 5
複製代碼
:::danger 該特性是非標準的, 請儘可能不要在生產環境中使用它!
該特性已經從 Web 標準中刪除, 雖然一些瀏覽器目前仍然支持它, 但也許會在將來的某個時間中止支持, 請儘可能不要使用該特性. :::
對象字面量中的 get 語法只能在新建一個對象
時使用.
var o = {
get gimmeFive() {
return 5;
},
};
o.gimmeFive; // 5
複製代碼
不會 Object.defineProperty 你就 out 了
vue.js 關於 Object.defineProperty 的利用原理