◆

JavaScript Object.defineProperty 属性描述符详解

2023-02-02 01:370

前置知识

对象中方法的定义

看如下代码对象中的方法定义是可以简写的

const obj = {
  fnn() {
    return 'fn-->fnn';
  },
  testOne: function () {
    return 'fn-->testOne';
  },
  testTwo: () => {
    return 'fn-->testTwo';
  },
  async fnns() {
    return 'fn-->fnns';
  },
  testOnes: async function () {
    return 'fn-->testOnes';
  },
  testTwos: async () => {
    return 'fn-->testTwos';
  },
  ['test' + 'three']() {
    return `fn-->${['test' + 'three']}`;
  },
};

console.log('fnn:', obj.fnn());
console.log('testOne:', obj.testOne());
console.log('testTwo:', obj.testTwo());
console.log('fnns:', obj.fnns());
console.log('testOnes:', obj.testOnes());
console.log('testTwos:', obj.testTwos());
console.log('test' + 'three', obj['test' + 'three']());
console.log('test' + 'three', obj.testthree());

getter 与 setter

get:将对象属性绑定到查询该属性时将被调用的函数。

  • 语法:get [prop]() { ... }
  • prop: 要绑定到给定函数的属性名。

set:将对象属性绑定到要调用的函数。

  • 语法:set [prop](val) { ... }
  • prop: 要绑定到给定函数的属性名。
  • val: 用于保存尝试分配给 prop 的值的变量的一个别名。

使用示例

const obj = {
  a: '我是 a 初始的值,稍后我将改变',
  // 相当于一个普通函数
  get() {
    console.log('调用了此对象的 get 方法');
    return 'get';
  },
  // getter 绑定了 test 属性
  get testGetter() {
    return this.a;
  },
  set testGetter(name) {
    this.a = name;
  },
};

console.log('第一次获取 testGetter:', obj.testGetter);
obj.a = '第一次获取 testGetter 后 a 的值变成此文字';
console.log('第二次获取 testGetter:', obj.testGetter);
obj.testGetter = '这是一个新值';
console.log('第三次获取 testGetter:', obj.testGetter);

Object.defineProperty 作用

直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法: Object.defineProperty(obj, prop, descriptor)

  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的 属性描述符

返回值: 被传递给函数的对象。

属性描述符

JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”

  • 对象里目前存在的属性描述符有两种主要形式:数据描述符 和 存取描述符。

  • 数据描述符和存取描述符均具有以下可选键值

    • configurable:当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false(直接声明对象时默认值:true)。
    • enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false(直接声明对象时默认值:true)。
  • 数据描述符同时具有以下可选键值:

    • value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
    • writable:当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。默认为 false(直接声明对象时默认值:true)。
  • 存取描述符同时具有以下可选键值:

    • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。默认为 undefined。
    • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
  • 如果一个描述符不具有 value, writable , get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。

  • 如果一个描述符同时有(value 或 writable)和(get 或 set)关键字,将会产生一个异常。

  • 拥有布尔值的字段的默认值都是false。value,get和set字段的默认值为undefined。

  • 当描述符中省略某些字段时,这些字段将使用它们的默认值。

存储描述符示例

const obj = {
  firstName: 'A',
  lastName: 'B',
};

Object.defineProperty(obj, 'fullName', {
  // 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this 为 obj
  get() {
    return this.firstName + '-' + this.lastName;
  },
  // 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this 为 obj
  set(value) {
    const names = value.split('-');
    this.firstName = names[0];
    this.lastName = names[1];
  },
});
console.log(obj.fullName); // A-B
obj.fullName = 'w-fly';
console.log('fullName:', obj.fullName); // w-fly
console.log('firstName:', obj.firstName); // w
console.log('lastName:', obj.lastName); // fly

使用 Object.defineProperty 创建属性

如果对象中不存在指定的属性,Object.defineProperty() 就创建这个属性。

var o = {}; // 创建一个新对象

// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true,
});

// 对象 o 拥有了属性 a,值为 37
console.log(o);

// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, 'b', {
  get: function () {
    return bValue;
  },
  set: function (newValue) {
    bValue = newValue;
  },
  enumerable: true,
  configurable: true,
});

// 对象 o 拥有了属性 b,值为 38
// o.b 的值现在总是与 bValue 相同,除非重新定义 o.b
o.b = 38;

// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, 'conflict', {
  value: 'aaa',
  get: function () {
    return 'bbb';
  },
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

修改属性

  • 如果属性已经存在,Object.defineProperty() 将尝试根据描述符中的值以及对象当前的配置来修改这个属性。
  • 如果旧描述符将其 configurable 属性设置为 false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变 writable 为 false)。
  • 当属性不可配置时,不能在数据和访问器属性类型之间切换。
  • 当试图改变不可配置属性(除了 value 和 writable 属性之外)的值时会抛出 TypeError,除非当前值和新值相同。
  • Writable 属性:当 writable 属性设置为 false 时,该属性被称为“不可写”。它不能被重新分配。

如示例所示,试图写入非可写属性不会改变它,也不会引发错误。

var o = {}; // Creates a new object

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false,
});

console.log(o.a); // 37
o.a = 25; // 不会报错,但是修改不会生效
console.log(o.a); // 37

目前,有四个操作会忽略 enumerable 为 false 的属性。

  • for...in 循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略 enumerable 为 false 的属性,只拷贝对象自身的可枚举的属性。
var o = {};
Object.defineProperty(o, 'a', { value: 1, enumerable: true });
Object.defineProperty(o, 'b', { value: 2, enumerable: false });
Object.defineProperty(o, 'c', { value: 3 }); // enumerable defaults to false
o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则这个属性的 enumerable 为 true

for (var i in o) {
  // 'a' 和 'd'
  console.log(i);
}

Object.keys(o); // ["a", "d"]

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false

Configurable 特性:configurable 特性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。如果 o.a 的 configurable 属性为 true ,则不会抛出任何错误,并且该属性将在最后被删除。

var o = {};
Object.defineProperty(o, 'a', {
  get: function () {
    return 1;
  },
  configurable: false,
});
// throws a TypeError
Object.defineProperty(o, 'a', { configurable: true });
// throws a TypeError
Object.defineProperty(o, 'a', { enumerable: true });
// throws a TypeError (set was undefined previously)
Object.defineProperty(o, 'a', {
  set: function () {},
});
// throws a TypeError (even though the new get does exactly the same thing)
Object.defineProperty(o, 'a', {
  get: function () {
    return 1;
  },
});
// throws a TypeError
Object.defineProperty(o, 'a', { value: 12 });
console.log(o.a); // 1
delete o.a; // false
console.log(o.a); // 1

添加多个属性和默认值:考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和 Object.defineProperty() 为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。

var o = {};

o.a = 1;
// 等同于 :
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true,
});

// 另一方面,
Object.defineProperty(o, 'a', { value: 1 });
// 等同于 :
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false,
});

一般的 Setters 和 Getters:下面的例子展示了如何实现一个自存档对象。 当设置temperature属性时,archive数组会获取日志条目。

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function () {
      console.log('get!');
      return temperature;
    },
    set: function (value) {
      temperature = value;
      archive.push({ val: temperature });
    },
  });

  this.getArchive = function () {
    return archive;
  };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

存取器的写法区别

写法一

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  },
});

obj.p; // "getter"
obj.p = 123; // "setter: 123"

写法二

var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  },
};

上面两种写法,虽然属性 p 的读取和赋值行为是一样的,但是有一些细微的区别。

  • 第一种写法,属性 p 的 configurable 和 enumerable 都为 false,从而导致属性 p 是不可遍历的
  • 第二种写法,属性 p 的 configurable 和 enumerable 都为 true,因此属性 p 是可遍历的

Object.defineProperties

可以一次性定义或修改多个属性

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 123, enumerable: true },
    p2: { value: 'abc', enumerable: true },
    p3: {
      get: function () {
        return this.p1 + this.p2;
      },
      enumerable: true,
      configurable: true,
    },
  },
);

obj.p1; // 123
obj.p2; // "abc"
obj.p3; // "123abc"

Object.getOwnPropertyDescriptor()

可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名

只能用于对象自身的属性,不能用于继承的属性。

var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
//   value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Object.getOwnPropertyDescriptor(obj, 'toString'); // undefined

Object.getOwnPropertyNames()

返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 1, enumerable: true },
    p2: { value: 2, enumerable: false },
  },
);

Object.getOwnPropertyNames(obj);
// ["p1", "p2"]

和 Object.keys 的行为不同,Object.keys 只返回对象自身的可遍历属性的全部属性名。

Object.keys([]); // []
// 数组自身的length属性是不可遍历的
Object.getOwnPropertyNames([]); // [ 'length' ]

Object.keys(Object.prototype); // []
// Object.prototype 自身的属性都是不可遍历的
Object.getOwnPropertyNames(Object.prototype);
// ['hasOwnProperty',
//  'valueOf',
//  'constructor',
//  'toLocaleString',
//  'isPrototypeOf',
//  'propertyIsEnumerable',
//  'toString']

Object.getOwnPropertySymbols(obj)

返回一个数组,包含对象自身的所有 Symbol 属性的键名。

Object.prototype.propertyIsEnumerable

返回一个布尔值,用来判断某个属性是否可遍历(可枚举),这个方法只能用于判断对象自身的属性,对于继承的属性一律返回 false。

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable('p'); // true
obj.propertyIsEnumerable('toString'); // false

Object.preventExtensions()

使得一个对象无法再添加新的属性

var obj = new Object();
Object.preventExtensions(obj);

Object.defineProperty(obj, 'p', {
  value: 'hello',
});
// TypeError: Cannot define property:p, object is not extensible.

obj.p = 1;
obj.p; // undefined

Object.isExtensible()

用于检查一个对象是否使用了 Object.preventExtensions 方法。也就是说,检查是否可以为一个对象添加属性。

var obj = new Object();

Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false

Object.seal()

使得一个对象既无法添加新属性,也无法删除旧属性。

var obj = { p: 'hello' };
Object.seal(obj);

delete obj.p;
obj.p; // "hello"

obj.x = 'world';
obj.x; // undefined

Object.seal 实质是把属性描述对象的 configurable 属性设为 false,因此属性描述对象不再能改变了。

var obj = {
  p: 'a',
};

// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
//   value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Object.seal(obj);

// seal方法之后
Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
//   value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: false
// }

Object.defineProperty(obj, 'p', {
  enumerable: false,
});
// TypeError: Cannot redefine property: p

Object.seal 只是禁止新增或删除属性,并不影响修改某个属性的值。

var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p; // 'b'

上面代码中,Object.seal 方法对 p 属性的 value 无效,是因为此时 p 属性的可写性由 writable 决定。

Object.isSealed()

用于检查一个对象是否使用了 Object.seal 方法。

var obj = { p: 'a' };

Object.seal(obj);
Object.isSealed(obj); // true

Object.isExtensible 方法返回 false。

var obj = { p: 'a' };

Object.seal(obj);
Object.isExtensible(obj); // false

Object.freeze()

使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。

var obj = {
  p: 'hello',
};

Object.freeze(obj);

obj.p = 'world';
obj.p; // "hello"

obj.t = 'hello';
obj.t; // undefined

delete obj.p; // false
obj.p; // "hello"

Object.isFrozen()

用于检查一个对象是否使用了 Object.freeze 方法

var obj = {
  p: 'hello',
};

Object.freeze(obj);
Object.isFrozen(obj); // true

使用 Object.freeze 方法以后,Object.isSealed 将会返回 true,Object.isExtensible 返回 false。

var obj = {
  p: 'hello',
};

Object.freeze(obj);

Object.isSealed(obj); // true
Object.isExtensible(obj); // false

如何在使用了上面的方法后,给对象添加属性

上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t;
// hello

上面代码中,对象 obj 本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在 obj 上读到。

如何解决

把 obj 的原型也冻结住

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);

proto.t = 'hello';
obj.t; // undefined

另一个问题是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。

var obj = {
  foo: 1,
  bar: ['a', 'b'],
};
Object.freeze(obj);

obj.bar.push('c');
obj.bar; // ["a", "b", "c"]
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript Object.defineProperty 属性描述符详解加载中