JS `Object.getOwnPropertyDescriptors`:获取所有属性描述符,包括存取器

各位观众老爷们,大家好!今天咱们来聊聊JavaScript里一个挺有意思的家伙:Object.getOwnPropertyDescriptors。这玩意儿,说白了,就是个扒皮专家,能把一个对象扒得只剩骨头架子,不对,是属性描述符。

开场白:啥是属性描述符?

在深入Object.getOwnPropertyDescriptors之前,咱们先得搞清楚属性描述符是个啥玩意儿。简单来说,它就是描述一个对象属性特征的“说明书”。这个说明书里包含了以下几个关键信息:

  • value: 属性的值。这个好理解,属性是啥就是啥。
  • writable: 属性是否可写。true表示可以修改,false表示只读。
  • enumerable: 属性是否可枚举。true表示可以用for...in循环或Object.keys()等方法遍历到,false表示不可遍历。
  • configurable: 属性是否可配置。true表示可以删除属性,或者修改属性的描述符(除了writable如果是false,那就不能再改回true了)。false表示属性是“金钟罩铁布衫”,刀枪不入,谁也别想动它。
  • get: 一个函数,作为属性的 getter。当读取属性时,会调用这个函数。
  • set: 一个函数,作为属性的 setter。当设置属性时,会调用这个函数。

注意,valuewritable通常是和数据属性一起出现的,而getset是和存取器属性(也叫访问器属性)一起出现的。一个属性要么是数据属性,要么是存取器属性,不能既是又是。

Object.getOwnPropertyDescriptor:单个属性的“CT扫描”

Object.getOwnPropertyDescriptors登场之前,我们先来认识一下它的“小弟”:Object.getOwnPropertyDescriptor。这个函数接收两个参数:一个对象和一个属性名,然后返回该属性的描述符。

const obj = {
  name: '张三',
  age: 30
};

const nameDescriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(nameDescriptor);
// 输出:
// {
//   value: '张三',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

const ageDescriptor = Object.getOwnPropertyDescriptor(obj, 'age');
console.log(ageDescriptor);
// 输出:
// {
//   value: 30,
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

const nonExistentDescriptor = Object.getOwnPropertyDescriptor(obj, 'address');
console.log(nonExistentDescriptor); // 输出: undefined

可以看到,Object.getOwnPropertyDescriptor就像一个CT扫描仪,可以对对象的单个属性进行扫描,然后告诉你这个属性的各种特征。如果属性不存在,就返回undefined

Object.getOwnPropertyDescriptors:全家桶式的“透视眼”

好了,现在主角登场了!Object.getOwnPropertyDescriptors接收一个对象作为参数,然后返回一个对象,这个对象包含了原对象所有自身属性的描述符。注意,是自身属性,不包括原型链上的属性。

const obj = {
  name: '张三',
  age: 30,
  get greeting() {
    return `你好,我是${this.name}`;
  },
  set greeting(value) {
    console.log(`设置greeting为: ${value}`);
  }
};

const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
// 输出:
// {
//   name: {
//     value: '张三',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   age: {
//     value: 30,
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   greeting: {
//     get: [Function: get greeting],
//     set: [Function: set greeting],
//     enumerable: true,
//     configurable: true
//   }
// }

可以看到,Object.getOwnPropertyDescriptors返回了一个对象,这个对象的每个属性都是原对象对应属性的描述符。而且,它还能正确处理存取器属性,把getset函数都给你扒出来。

实际应用:复制对象,保留所有细节

Object.getOwnPropertyDescriptors一个很重要的应用场景就是复制对象。以往我们复制对象,通常会用Object.assign()或者...扩展运算符。但是,这两种方法都有一个缺点:它们只会复制属性的值,而不会复制属性的描述符。也就是说,如果你要复制一个只读的属性,或者一个不可枚举的属性,用这两种方法复制出来的属性就不是只读的,也不是不可枚举的了。

const originalObj = {
  name: '张三',
  age: 30
};

Object.defineProperty(originalObj, 'id', {
  value: '123456',
  writable: false,
  enumerable: false,
  configurable: false
});

// 使用 Object.assign 复制
const copiedObj1 = Object.assign({}, originalObj);
console.log(copiedObj1); // { name: '张三', age: 30, id: '123456' }
console.log(Object.getOwnPropertyDescriptor(copiedObj1, 'id'));
// { value: '123456', writable: true, enumerable: true, configurable: true }  writable和enumerable都变成了true

// 使用 ... 扩展运算符复制
const copiedObj2 = { ...originalObj };
console.log(copiedObj2); // { name: '张三', age: 30, id: '123456' }
console.log(Object.getOwnPropertyDescriptor(copiedObj2, 'id'));
// { value: '123456', writable: true, enumerable: true, configurable: true } writable和enumerable都变成了true

// 使用 Object.getOwnPropertyDescriptors 复制
const copiedObj3 = Object.create(Object.getPrototypeOf(originalObj), Object.getOwnPropertyDescriptors(originalObj));
console.log(copiedObj3); // { name: '张三', age: 30, id: '123456' }
console.log(Object.getOwnPropertyDescriptor(copiedObj3, 'id'));
// { value: '123456', writable: false, enumerable: false, configurable: false }  writable和enumerable保持不变

可以看到,使用Object.assign()...扩展运算符复制出来的id属性,writableenumerable都变成了true,丢失了原有的属性特征。而使用Object.getOwnPropertyDescriptors复制出来的id属性,则保留了原有的属性特征。

Object.create()的妙用

上面的代码中,我们用到了Object.create()方法。这个方法可以创建一个新对象,并指定新对象的原型和属性。Object.create(proto, propertiesObject)

  • proto: 新创建对象的原型对象。 如果传入null,则创建的对象不继承任何原型链上的属性和方法。
  • propertiesObject: 可选。 如果指定了该参数且该参数不为 null 或非原始值,则它是将添加到新创建对象的属性(即,覆盖原型链上的属性)的属性描述符对象。 这些属性是新创建对象自身的自有属性。

在这个场景下,我们使用Object.create(Object.getPrototypeOf(originalObj), Object.getOwnPropertyDescriptors(originalObj))来创建一个新对象,并将新对象的原型设置为原对象的原型,然后将原对象的所有自身属性的描述符都复制到新对象上。这样,就可以创建一个完全克隆的原对象,包括属性值、属性描述符和原型。

案例分析:防止对象被篡改

Object.getOwnPropertyDescriptors还可以用来防止对象被篡改。我们可以先用Object.getOwnPropertyDescriptors获取对象的所有属性描述符,然后用Object.defineProperties()方法重新定义对象的属性,并将writableconfigurable都设置为false。这样,就相当于给对象加上了一层保护罩,防止别人修改或删除对象的属性。

const originalObj = {
  name: '张三',
  age: 30
};

const descriptors = Object.getOwnPropertyDescriptors(originalObj);

for (const key in descriptors) {
  descriptors[key].writable = false;
  descriptors[key].configurable = false;
}

Object.defineProperties(originalObj, descriptors);

// 尝试修改属性
originalObj.name = '李四'; // 静默失败,严格模式下会报错
delete originalObj.age; // 静默失败,严格模式下会报错

console.log(originalObj); // { name: '张三', age: 30 }
console.log(Object.getOwnPropertyDescriptor(originalObj, 'name'));
// { value: '张三', writable: false, enumerable: true, configurable: false }

可以看到,即使我们尝试修改name属性,或者删除age属性,都不会成功。因为我们已经将writableconfigurable都设置为false了。

进阶用法:结合Proxy实现更强大的对象控制

Object.getOwnPropertyDescriptors还可以和Proxy结合使用,实现更强大的对象控制。Proxy是ES6新增的一个特性,可以用来创建一个对象的代理,然后拦截对这个对象的操作,比如读取属性、设置属性、删除属性等等。

我们可以用Object.getOwnPropertyDescriptors获取对象的所有属性描述符,然后创建一个Proxy对象,并在Proxy对象的getOwnPropertyDescriptor方法中返回这些描述符。这样,就可以防止别人通过Object.getOwnPropertyDescriptor()方法获取对象的真实属性描述符,从而保护对象的属性不被篡改。

const originalObj = {
  name: '张三',
  age: 30
};

const descriptors = Object.getOwnPropertyDescriptors(originalObj);

const proxyObj = new Proxy(originalObj, {
  getOwnPropertyDescriptor(target, prop) {
    return descriptors[prop];
  }
});

console.log(Object.getOwnPropertyDescriptor(proxyObj, 'name'));
// { value: '张三', writable: true, enumerable: true, configurable: true }

// 尝试修改 originalObj 的属性描述符
Object.defineProperty(originalObj, 'name', { writable: false });

console.log(Object.getOwnPropertyDescriptor(proxyObj, 'name'));
// { value: '张三', writable: true, enumerable: true, configurable: true }  Proxy 拦截了修改
console.log(Object.getOwnPropertyDescriptor(originalObj, 'name'));
// { value: '张三', writable: false, enumerable: true, configurable: true } 原始对象已经修改

在这个例子中,我们创建了一个Proxy对象,并在Proxy对象的getOwnPropertyDescriptor方法中返回了原对象的属性描述符。然后,我们尝试修改原对象的name属性的writable属性,但是Proxy对象返回的name属性的writable属性仍然是true,说明Proxy对象成功拦截了修改操作。

与其他方法的比较

方法 作用 返回值
Object.keys(obj) 返回对象自身的可枚举属性组成的数组。 包含对象所有可枚举属性名称的数组。
Object.values(obj) 返回对象自身的可枚举属性值组成的数组。 包含对象所有可枚举属性值的数组。
Object.entries(obj) 返回对象自身的可枚举属性的键值对组成的数组。 包含对象所有可枚举属性的键值对数组,每个键值对是一个数组 [key, value]
Object.getOwnPropertyNames(obj) 返回对象自身的所有属性(包括不可枚举属性)组成的数组。 包含对象所有属性名称(包括不可枚举属性)的数组。
Object.getOwnPropertySymbols(obj) 返回对象自身的所有Symbol属性组成的数组。 包含对象所有Symbol属性的数组。
Object.getOwnPropertyDescriptor(obj, prop) 返回对象指定属性的属性描述符。 一个对象,包含属性的描述符:value (属性值), writable (是否可写), enumerable (是否可枚举), configurable (是否可配置), get (getter函数), set (setter函数)。如果属性不存在,返回 undefined
Object.getOwnPropertyDescriptors(obj) 返回对象自身的所有属性描述符。 一个对象,其属性名是原对象的属性名,属性值是对应属性的属性描述符对象。

兼容性

Object.getOwnPropertyDescriptors 是 ES2017 (ES8) 引入的特性。这意味着它在较旧的浏览器或 JavaScript 引擎中可能不受支持。为了确保兼容性,你可以使用 polyfill。

if (typeof Object.getOwnPropertyDescriptors !== 'function') {
  Object.getOwnPropertyDescriptors = function getOwnPropertyDescriptors(obj) {
    if (obj === null || obj === undefined) {
      throw new TypeError('Cannot convert undefined or null to object');
    }

    const descriptors = {};
    Object.getOwnPropertyNames(obj).forEach(function(key) {
      descriptors[key] = Object.getOwnPropertyDescriptor(obj, key);
    });
    return descriptors;
  };
}

这段代码检查 Object.getOwnPropertyDescriptors 是否存在,如果不存在,则定义一个 polyfill,使用 Object.getOwnPropertyNamesObject.getOwnPropertyDescriptor 来模拟其行为。

总结:这玩意儿,真香!

总而言之,Object.getOwnPropertyDescriptors是一个非常强大的工具,可以用来复制对象、防止对象被篡改,以及实现更高级的对象控制。虽然它在日常开发中可能不常用,但是在某些特定的场景下,它可以发挥巨大的作用。就像一把瑞士军刀,平时可能放在抽屉里吃灰,但是关键时刻就能派上大用场。

好了,今天的讲座就到这里。希望大家能够掌握Object.getOwnPropertyDescriptors的用法,并在实际开发中灵活运用。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注