最近在寫一個 《JavaScript API 全解析》系列(剛寫完 String,現正在寫 Object,可戳 JavaScript API 全解析),想把 MDN 推薦使用的 API 全部擼一遍,也算是給自己準備一份資料。因為 Object.defineProperty() 涉及到的知識點比較複雜,所以單獨拎出來放到這裡,歡迎大家拍磚。
語法
defineProperty(o: any, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): any;
複製程式碼
描述
用於在一個物件上定義新的屬性或修改現有屬性, 並返回該物件.
引數
-
o
目標物件 -
p
需要定義的屬性或方法名 (可修改既有的, 也可新增新屬性或方法) -
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]]
屬性描述符可同時具有的鍵值
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
資料屬性 | Yes | Yes | Yes | Yes | No | No |
訪問器屬性 | Yes | Yes | No | No | Yes | Yes |
簡言之, 定義了 value 或 writable , 一定不能有 get 或 set, 反之亦然, 否則報錯.
Configurable
如果某個屬性的 configurable 為false
, 那麼:
- 將不能刪除此屬性, 即
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;
})();
複製程式碼
- 當 enumerable 或 writable 是
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
如果某個屬性的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
}());
複製程式碼
Enumerable
定義了物件的屬性是否可以在 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
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__()
__defineGetter__ 方法可以為一個已經存在
的物件設定 (新建或修改) 訪問器屬性, __defineSetter__ 方法可以將一個函式繫結在當前物件的指定屬性上, 當那個屬性被賦值時, 你所繫結的函式就會被呼叫.
var o = {};
o.__defineGetter__('gimmeFive', function() {
return 5;
});
o.gimmeFive; // 5
複製程式碼
:::danger 該特性是非標準的, 請儘量不要在生產環境中使用它!
該特性已經從 Web 標準中刪除, 雖然一些瀏覽器目前仍然支援它, 但也許會在未來的某個時間停止支援, 請儘量不要使用該特性. :::
物件字面量中的 get 語法
物件字面量中的 get 語法只能在新建一個物件
時使用.
var o = {
get gimmeFive() {
return 5;
},
};
o.gimmeFive; // 5
複製程式碼
參考
不會 Object.defineProperty 你就 out 了
vue.js 關於 Object.defineProperty 的利用原理