屬性的操作
在 JavaScript 中,給物件增加一個屬性是非常簡單的,直接呼叫屬性並賦值即可。
const obj = {};
obj.name = 'Tom';
console.log(obj);
/**
* 輸出:
* {name: 'Tom'}
*/
通過這種方式新增的屬性,可以隨意操作:
- 可修改
- 可列舉
- 可刪除
可修改:
// 可修改
+ obj.name = 'Jim';
+ console.log(obj.name);
/**
* 輸出:
* 'Jim'
*/
可列舉:
// 可列舉
+ for (let key in obj) {
+ console.log(`${key} : ${obj[key]}`);
+ }
/**
* 輸出:
* name : Jim
*/
可刪除:
// 可刪除
+ delete obj.name;
+ console.log(obj);
/**
* 輸出:
* {}
*/
如果想通過 Object.defineProperty
實現上面的功能,可以使用下面的程式碼:
- obj.name = 'Tom';
+ Object.defineProperty(obj, 'name', {
+ value: 'Tom',
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ });
函式簽名
在對 Object.defineProperty
深入學習之前,先對這個方法簽名有一個認識:
Object.defineProperty(obj, prop, descriptor);
從函式簽名中可以看出,defineProperty
是 Object
上的一個靜態方法,可以傳遞三個引數:
obj
要定義屬性的物件prop
要定義或修改的屬性名稱descriptor
要定義或修改屬性的描述符
返回值是被傳遞給函式的物件,也就是第一個引數 obj
。
描述符可以有以下幾個可選值:
configurable
enumerable
value
writable
get
set
描述符
通過 Object.defineProperty
來為物件定義一個屬性。
const obj = {};
Object.defineProperty(obj, 'name', {});
console.log(obj);
/**
* 輸出:
* {name: undefined}
*/
從輸出的結果可以看出,在物件 obj
上增加一個屬性 name
,但是它的值是 undefined
。
value
如果想給屬性賦值,可以使用描述符中的 value
屬性。
- Object.defineProperty(obj, 'name', {});
+ Object.defineProperty(obj, 'name', {
+ value: 'Tom',
+ });
/**
* 輸出:
* {name: 'Tom'}
*/
writable
一般情況下,修改一個物件中的屬性值,可以使用 obj.name = 'Jim'
的形式。
+ obj.name = 'Jim';
+ console.log(obj);
/**
* 輸出:
* {name: 'Tom'}
*/
從輸出結果可以看出,並沒有修改成功。如果想修改屬性值,可以把描述符中的 writable
設定為 true
。
Object.defineProperty(obj, 'name', {
value: 'Tom',
+ writable: true,
});
enumerable
列舉物件的屬性,可以使用 for...in
。
+ for (let key in obj) {
+ console.log(`${key} : ${obj[key]}`);
+ }
比較奇怪的是,執行上面的程式碼沒有輸出任何資訊。
如果想正常列舉物件的屬性,可以將描述符中的 enumerable
值設定為 true
。
Object.defineProperty(obj, 'name', {
value: 'Tom',
writable: true,
+ enumerable: true,
});
configurable
當這個屬性不需要時,可以通過 delete
來刪除。
+ delete obj.name;
+ console.log(obj);
/**
* 輸出:
* {name: 'Jim'}
*/
從輸出結果可以看出,並沒有達到預期的效果。如果想從物件上正常刪除屬性,可以將描述符中的 configurable
設定為 true
。
Object.defineProperty(obj, 'name', {
value: 'Tom',
writable: true,
enumerable: true,
+ configurable: true,
});
get
如果需要獲取物件的值,可以使用描述符中的 get
。
const obj = {};
let _tmpName = 'Tom';
Object.defineProperty(obj, 'name', {
get() {
return _tmpName;
},
});
console.log(obj.name);
/**
* 輸出:
* {name: 'Tom'}
*/
set
如果需要設定物件的值,可以使用描述符中的 set
,它需要傳遞一個引數,就是修改後的值。
Object.defineProperty(obj, 'name', {
get() {
return _tmpName;
},
+ set(newVal) {
+ _tmpName = newVal;
+ },
});
+ obj.name = 'Jim';
+ console.log(obj.name);
/**
* 輸出:
* {name: 'Jim'}
*/
注意事項
在操作符物件中,如果存在了 value
或 writable
中的任意一個或多個,就不能存在 get
或 set
了。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 1,
get() {
return 2;
},
});
報錯資訊如下:
Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
為了方便後期查閱,總結一下互斥的情況:
value
和get
互斥value
和set
互斥value
和set
+get
互斥writable
和get
互斥writable
和set
互斥writable
和set
+get
互斥
使用場景
Object.defineProperty()
方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。該方法允許精確地新增或修改物件的屬性。
這個方法是 JavaScript 的一個比較底層的方法,主要用於在物件上新增或修改物件的屬性。
簡單應用
基礎修飾符的使用
假設現在有一個需求,實現下面的效果:
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
obj[key] += 1;
}
console.log(obj);
/*
輸出:
{ a: 3, b: 3, c: 5 }
*/
Object.defineProperty()
不僅可以定義屬性,也可以修改屬性。這個時候就可以使用修改屬性的方式來實現上面的需求。
for (let key in obj) {
Object.defineProperty(obj, key, {
enumerable: true,
value: ++obj[key],
writable: key !== 'b',
});
}
get 的使用
使用 Object.defineProperty
中的 get
將 console.log
中的資訊正常輸出。
if (num === 1 && num === 2 && num === 3) {
console.log('you win ...');
}
從題目中可以看出,num
不可能即等於 1
,又等於 2
,還等於 3
。如果想實現這樣的效果,必然需要在 num
取值的同時自增。
要實現一個變數取值並自增,就需要使用 Object.defineProperty
中的 get
。num
是直接使用的,在瀏覽器中,只有掛載到 window
物件上的屬性可以直接使用。
let _tmpNum = 0;
Object.defineProperty(window, 'num', {
get() {
return ++_tmpNum;
},
});
假設現在有一個需求,實現如下效果:
_ // a
_ + _ // ab
_ + _ + _ // abc
這個需求其實就是需要在 window
物件上掛載一個 _
屬性,每呼叫一次 _
就會自增一次 ASCII 碼。
Object.defineProperty(window, '_', {
get() {
// 獲取字母 a 的 ASCII 碼
const aAsciiCode = 'a'.charCodeAt(0);
// 獲取字母 z 的 ASCII 碼
const zAsciiCode = 'z'.charCodeAt(0);
// 如果 _code 不存在,將其賦值為 a 的 ASCII 碼
this._code = this._code || aAsciiCode;
// 如果 _code 的範圍超出了小寫字母的範圍,直接返回
if (this._code > zAsciiCode) return;
// 獲取當前 ASCII 碼對應的字母
const _char = String.fromCharCode(this._code);
// 每呼叫一次自增一次
this._code++;
// 返回
return _char;
},
});
如果想列印輸出 26 個字母的組合,可以通過遍歷的方式。
let resStr = '';
for (let i = 0; i < 26; i++) {
resStr += _;
}
console.log(resStr);
set 的使用
如果將一個字串賦值為 'Object'
,列印這個字串輸出 {type: 'Object', length: 6}
;如果將一個字串賦值為 'Object'
,列印這個字串輸出 {type: 'Array', length: 5}
;如果將字串賦值成其他值,程式報錯 TypeError: This type is invalid.
。
分析這個題目,列印的時候其實就是取值的過程,需要用到 get
操作符,type
是當前字串的值,length
是當前字串的長度。
let _tmpStr = '';
Object.defineProperty(window, 'str', {
get() {
return { type: _tmpStr, length: _tmpStr.length };
},
});
在給字串賦值的時候,當字串的值是 'Object'
或 'Array'
的時候正常賦值,其餘情況直接丟擲錯誤。這個操作就需要在操作符的 set
中實現。
Object.defineProperty(window, 'str', {
get() {
return { type: _tmpStr, length: _tmpStr.length };
},
set(newVal) {
+ if (newVal === 'Object' || newVal === 'Array') {
+ _tmpStr = newVal;
+ } else {
+ throw new TypeError('This type is invalid.');
+ }
},
});
驗證程式碼的執行效果:
str = 'Object';
console.log(str);
/*
輸出:
{type: 'Object', length: 6}
*/
str = 'Array';
console.log(str);
/*
輸出:
{type: 'Array', length: 5}
*/
str = '123';
console.log(str);
/*
輸出:
TypeError: This type is invalid.
*/
複雜應用
需求:在頁面中有一個輸入框,下面有一個顯示區域,當輸入框中的內容發生變化時,顯示區中的內容同步變化。當重新整理頁面時,頁面中的資訊保持和重新整理前一致。
首先,在 index.html
中繪製頁面資訊:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div><input type="text" placeholder="輸入資訊" id="idInfo" /></div>
<div id="idShowInfo"></div>
<script type="module" src="./js/index.js"></script>
</body>
</html>
簡單實現
最簡單的實現方式就是進入頁面的時候從快取中獲取資料,能夠獲取到就給輸入框和顯示區域賦值;同時監聽輸入框的輸入事件,向快取和顯示區域中寫入對應的資訊。
function init() {
const eleInfo = document.getElementById('idInfo');
const eleShowInfo = document.getElementById('idShowInfo');
const _storageInfo = JSON.parse(localStorage.getItem('storageInfo') || '{}');
if (_storageInfo.info) {
eleInfo.value = _storageInfo.info;
}
eleShowInfo.innerHTML = eleInfo.value;
eleInfo.addEventListener(
'input',
function () {
localStorage.setItem(
'storageInfo',
JSON.stringify({ info: eleInfo.value || '' }),
);
eleShowInfo.innerHTML = eleInfo.value;
},
false,
);
}
init();
Object.defineProperty 實現
上面的實現方式相對而言比較直接且比較簡單,但是程式碼的封裝性比較差,並且資料耦合性比較高。如果使用 Object.defineProperty
就可以更好的組織程式碼。
首先書寫入口檔案的程式碼 js/index.js
:
import { observer } from './observer.js';
const eleInfo = document.getElementById('idInfo');
const eleShowInfo = document.getElementById('idShowInfo');
const infoObj = observer({ info: '' }, eleInfo, eleShowInfo);
function init() {
bindEvent(eleInfo);
}
function bindEvent(ele) {
ele.addEventListener('input', handleInput, false);
}
function handleInput(event) {
const _info = event.target.value || '';
infoObj.info = _info;
}
init();
其次書寫 js/observer.js
中的程式碼:
export function observer(infoObj, inputDom, viewDom) {
const _storageInfo = JSON.parse(localStorage.getItem('storageInfo') || '{}');
const _resInfo = {};
init(_storageInfo, infoObj, _resInfo, inputDom, viewDom);
return _resInfo;
}
function init(storageInfo, infoObj, resInfo, inputDom, viewDom) {
initData(storageInfo, infoObj, resInfo, inputDom, viewDom);
initDom(resInfo, inputDom, viewDom);
}
function initData(storageInfo, infoObj, resInfo, inputDom, viewDom) {
for (let key in storageInfo) {
infoObj[key] = storageInfo[key];
}
for (let key in infoObj) {
(function (key) {
Object.defineProperty(resInfo, key, {
get() {
return infoObj[key];
},
set(newVal) {
infoObj[key] = newVal;
localStorage.setItem('storageInfo', JSON.stringify(infoObj));
initDom(resInfo, inputDom, viewDom);
},
});
})(key);
}
}
function initDom(resInfo, inputDom, viewDom) {
inputDom.value = resInfo.info;
viewDom.innerHTML = resInfo.info;
}