深入了解|深入了解 Object.defineProperty

属性的操作 在 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);

从函数签名中可以看出,definePropertyObject 上的一个静态方法,可以传递三个参数:
  • 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'} */

注意事项 在操作符对象中,如果存在了 valuewritable 中的任意一个或多个,就不能存在 getset 了。
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

为了方便后期查阅,总结一下互斥的情况:
  • valueget 互斥
  • valueset 互斥
  • valueset + get 互斥
  • writableget 互斥
  • writableset 互斥
  • writableset + 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 中的 getconsole.log 中的信息正常输出。
if (num === 1 && num === 2 && num === 3) { console.log('you win ...'); }

从题目中可以看出,num 不可能即等于 1,又等于 2,还等于 3。如果想实现这样的效果,必然需要在 num 取值的同时自增。
要实现一个变量取值并自增,就需要使用 Object.defineProperty 中的 getnum 是直接使用的,在浏览器中,只有挂载到 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 中绘制页面信息:
Document

简单实现
最简单的实现方式就是进入页面的时候从缓存中获取数据,能够获取到就给输入框和显示区域赋值;同时监听输入框的输入事件,向缓存和显示区域中写入对应的信息。
function init() { const eleInfo = document.getElementById('idInfo'); const eleShowInfo = document.getElementById('idShowInfo'); const _storageInfo = JSON.parse(localStorage.getItem('storageInfo') || '{}'); if (_storageInfo.info) { eleInfo.value = https://www.it610.com/article/_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();

【深入了解|深入了解 Object.defineProperty】其次书写 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 = https://www.it610.com/article/resInfo.info; viewDom.innerHTML = resInfo.info; }

参考资料
  • Object.defineProperty()
  • Object.defineProperties()
  • 从 0 到 1 学习「Object.defineProperty」
  • 『Object.defineProperty』考题训练与考点应用

    推荐阅读