Object.defineProperty()

Baseline Widely available

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。

尝试一下

语法

js
Object.defineProperty(obj, prop, descriptor)

参数

obj

要定义属性的对象。

prop

一个字符串或 Symbol,指定了要定义或修改的属性键。

descriptor

要定义或修改的属性的描述符。

返回值

传入函数的对象,其指定的属性已被添加或修改。

描述

Object.defineProperty() 允许精确地添加或修改对象上的属性。通过赋值添加的普通属性会在枚举属性时(例如 for...inObject.keys() 等)出现,它们的值可以被更改,也可以被删除。此方法允许更改这些额外细节,以使其不同于默认值。默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举和不可配置的。此外,Object.defineProperty() 使用 [[DefineOwnProperty]] 内部方法,而不是 [[Set]],因此即使属性已经存在,它也不会调用 setter

对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是一个具有可写或不可写值的属性。访问器描述符是由 getter/setter 函数对描述的属性。描述符只能是这两种类型之一,不能同时为两者。

数据描述符和访问器描述符都是对象。它们共享以下可选键(请注意:在使用 Object.defineProperty() 定义属性的情况下,下述所有键都是默认值):

configurable

当设置为 false 时,

  • 该属性的类型不能在数据属性和访问器属性之间更改,且
  • 该属性不可被删除,且
  • 其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则 value 可以被更改,writable 可以更改为 false)。

默认值为 false

enumerable

当且仅当该属性在对应对象的属性枚举中出现时,值为 true默认值为 false

数据描述符还具有以下可选键值:

value

与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。默认值为 undefined

writable

如果与属性相关联的值可以使用赋值运算符更改,则为 true默认值为 false

访问器描述符还具有以下可选键值:

get

用作属性 getter 的函数,如果没有 getter 则为 undefined。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined

set

用作属性 setter 的函数,如果没有 setter 则为 undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象。默认值为 undefined

如果描述符没有 valuewritablegetset 键中的任何一个,它将被视为数据描述符。如果描述符同时具有 [valuewritable] 和 [getset] 键,则会抛出异常。

这些属性不一定是描述符本身的属性。继承的属性也会被考虑在内。为了确保这些默认值得到保留,你可以预先冻结描述符对象原型链中的现有对象,明确指定所有选项,或使用 Object.create(null) 指向 null

js
const obj = {};
// 1. 使用 null 原型:没有继承的属性
const descriptor = Object.create(null);
descriptor.value = "static";

// 默认情况下,它们不可枚举、不可配置、不可写
Object.defineProperty(obj, "key", descriptor);

// 2. 使用一个包含所有属性的临时对象字面量来明确其属性
Object.defineProperty(obj, "key2", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static",
});

// 3. 重复利用同一对象
function withValue(value) {
  const d =
    withValue.d ||
    (withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value,
    });

  // 避免重复赋值
  if (d.value !== value) d.value = value;

  return d;
}
// 然后
Object.defineProperty(obj, "key", withValue("static"));

// 如果 freeze 可用,防止添加或删除对象原型属性
// (value、get、set、enumerable、writable、configurable)
(Object.freeze || Object)(Object.prototype);

当属性已经存在时,Object.defineProperty() 会尝试根据描述符和属性的当前配置修改属性。

如果旧描述符的 configurable 特性被设置为 false,则该属性被称为不可配置的。无法更改不可配置的访问器属性的任何特性,也不能将其在数据类型和访问器类型之间切换。对于具有 writable: true 的数据属性,可以修改其值并将 writable 特性从 true 改为 false。当试图更改不可配置的属性(除非允许更改 valuewritable)时,会抛出 TypeError,除非在数据属性上定义一个与原始值相同的值。

当当前属性是可配置的时,将特性设置为 undefined 可以有效地删除它。例如,如果 o.k 是一个访问器属性,Object.defineProperty(o, "k", { set: undefined }) 将删除 setter,使 k 只有 getter 并变成只读的。如果新描述符中缺少一个特性,则会保留旧描述符该特性的值(不会被隐式重新设置为 undefined)。通过提供不同类型的描述符,可以在数据属性和访问器属性之间切换。例如,如果新描述符是数据描述符(带有 valuewritable),则原始描述符的 getset 属性都将被删除。

示例

创建属性

当对象中不存在指定的属性时,Object.defineProperty() 将根据描述符创建一个新的属性。描述符中的字段可以省略,省略的字段将使用默认值。

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

// 通过 defineProperty 使用数据描述符添加对象属性的示例
Object.defineProperty(o, "a", {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true,
});
// 'a' 属性存在于对象 o 中,其值为 37

// 通过 defineProperty 使用访问器属性描述符添加对象属性的示例
let bValue = 38;
Object.defineProperty(o, "b", {
  get() {
    return bValue;
  },
  set(newValue) {
    bValue = newValue;
  },
  enumerable: true,
  configurable: true,
});
o.b; // 38
// 'b' 属性存在于对象 o 中,其值为 38。
// o.b 的值现在始终与 bValue 相同,除非重新定义了 o.b。

// 数据描述符和访问器描述符不能混合使用
Object.defineProperty(o, "conflict", {
  value: 0x9f91102,
  get() {
    return 0xdeadbeef;
  },
});
// 抛出错误 TypeError: value appears only in data descriptors, get appears only in accessor descriptors

修改属性

当修改已存在的属性时,操作的结果取决于当前属性的配置,可能会成功、不执行任何操作,或抛出一个 TypeError 异常。

Writable 特性

writable 特性设置为 false 时,该属性被称为“不可写的”。它不能被重新赋值。尝试对一个不可写的属性进行写入不会改变它,在严格模式下还会导致错误。

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

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

console.log(o.a); // 37
o.a = 25; // 不会抛出错误
// (在严格模式下,即使值相同,它也会抛出异常)
console.log(o.a); // 37;赋值不会成功

// 严格模式
(() => {
  "use strict";
  const o = {};
  Object.defineProperty(o, "b", {
    value: 2,
    writable: false,
  });
  o.b = 3; // 抛出 TypeError: "b" is read-only
  return o.b; // 如果没有上一行的话,会返回 2
})();

Enumerable 特性

enumerable 特性定义了属性是否可以被 Object.assign()展开运算符所考虑。对于非 Symbol 属性,它还定义了属性是否会在 for...in 循环和 Object.keys() 中显示。更多信息,请参见属性的枚举性和所有权

js
const o = {};
Object.defineProperty(o, "a", {
  value: 1,
  enumerable: true,
});
Object.defineProperty(o, "b", {
  value: 2,
  enumerable: false,
});
Object.defineProperty(o, "c", {
  value: 3,
}); // enumerable 默认为 false
o.d = 4; // 通过赋值创建属性时 enumerable 默认为 true
Object.defineProperty(o, Symbol.for("e"), {
  value: 5,
  enumerable: true,
});
Object.defineProperty(o, Symbol.for("f"), {
  value: 6,
  enumerable: false,
});

for (const i in o) {
  console.log(i);
}
// 打印 'a' 和 'd'(总是按这个顺序)

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

o.propertyIsEnumerable("a"); // true
o.propertyIsEnumerable("b"); // false
o.propertyIsEnumerable("c"); // false
o.propertyIsEnumerable("d"); // true
o.propertyIsEnumerable(Symbol.for("e")); // true
o.propertyIsEnumerable(Symbol.for("f")); // false

const p = { ...o };
p.a; // 1
p.b; // undefined
p.c; // undefined
p.d; // 4
p[Symbol.for("e")]; // 5
p[Symbol.for("f")]; // undefined

Configurable 特性

configurable 特性控制属性是否可以从对象中删除以及其特性(除了 valuewritable)是否可以更改。

以下示例演示了一个不可配置的访问器属性。

js
const o = {};
Object.defineProperty(o, "a", {
  get() {
    return 1;
  },
  configurable: false,
});

Object.defineProperty(o, "a", {
  configurable: true,
}); // 抛出 TypeError
Object.defineProperty(o, "a", {
  enumerable: true,
}); // 抛出 TypeError
Object.defineProperty(o, "a", {
  set() {},
}); // 抛出 TypeError(set 在之前未定义)
Object.defineProperty(o, "a", {
  get() {
    return 1;
  },
}); // 抛出 TypeError
// (即使新的 get 做的事情完全相同)
Object.defineProperty(o, "a", {
  value: 12,
}); // 抛出 TypeError
// ‘value’只有在 writable 为 true 时才可以被修改

console.log(o.a); // 1
delete o.a; // 无法删除;严格模式下会抛出错误
console.log(o.a); // 1

如果 o.aconfigurable 特性为 true,则不会抛出任何错误,并且该属性会在最后被删除。

以下示例说明了一个不可配置但可写的数据属性。该属性的 value 仍然可以被更改,writable 也仍然可以从 true 切换到 false

js
const o = {};
Object.defineProperty(o, "b", {
  writable: true,
  configurable: false,
});
console.log(o.b); // undefined
Object.defineProperty(o, "b", {
  value: 1,
}); // 即使 configurable 为 false,因为对象是可写的,我们仍然可以替换属性的值。
console.log(o.b); // 1
o.b = 2; // 我们也可以使用赋值运算符来更改属性的值
console.log(o.b); // 2
// 切换属性的可写性
Object.defineProperty(o, "b", {
  writable: false,
});
Object.defineProperty(o, "b", {
  value: 1,
}); // TypeError: because the property is neither writable nor configurable, it cannot be modified
// 此时,无法再次修改属性 'b' 或者恢复它的可写性。

这个示例演示了一个可配置但不可写的数据属性。该属性的 value 仍然可以使用 defineProperty 进行替换(但不能使用赋值运算符),并且 writable 特性仍然可以切换。

js
const o = {};
Object.defineProperty(o, "b", {
  writable: false,
  configurable: true,
});
Object.defineProperty(o, "b", {
  value: 1,
}); // 我们可以使用 defineProperty 方法替换属性的值
console.log(o.b); // 1
o.b = 2; // 在严格模式下抛出 TypeError:cannot change a non-writable property's value with assignment

这个示例演示了一个不可配置且不可写的数据属性。无法更新该属性的任何特性,包括它的 value 值。

js
const o = {};
Object.defineProperty(o, "b", {
  writable: false,
  configurable: false,
});
Object.defineProperty(o, "b", {
  value: 1,
}); // TypeError: the property cannot be modified because it is neither writable nor configurable.

添加多个属性和默认值

考虑属性默认值应用的方式是非常重要的。通常,在使用属性访问器分配一个值和使用 Object.defineProperty() 之间存在差异,如下面的示例所示。

js
const 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,
});

自定义 setter 和 getter

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

js
function Archiver() {
  let temperature = null;
  const archive = [];

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

  this.getArchive = () => archive;
}

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

下面这个例子中,getter 总是会返回一个相同的值。

js
const pattern = {
  get() {
    return "我总是返回这个字符串,无论你的赋值是什么";
  },
  set() {
    this.myname = "这是我名称的字符串";
  },
};

function TestDefineSetAndGet() {
  Object.defineProperty(this, "myproperty", pattern);
}

const instance = new TestDefineSetAndGet();
instance.myproperty = "test";
console.log(instance.myproperty);
// 我总是返回这个字符串,无论你的赋值是什么

console.log(instance.myname); // 这是我名称的字符串

继承属性

如果访问器属性被继承,它的 getset 方法会在派生对象的属性被访问或者修改时被调用。如果这些方法用一个变量存值,该值会被所有对象共享。

js
function MyClass() {}

let value;
Object.defineProperty(MyClass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  },
});

const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // 1

这可以通过将值存储在另一个属性中解决。在 getset 方法中,this 指向某个被访问和修改属性的对象。

js
function MyClass() {}

Object.defineProperty(MyClass.prototype, "x", {
  get() {
    return this.storedX;
  },
  set(x) {
    this.storedX = x;
  },
});

const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // undefined

与访问器属性不同,数据属性始终在对象自身上设置,而不是一个原型。然而,如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。

js
function MyClass() {}

MyClass.prototype.x = 1;
Object.defineProperty(MyClass.prototype, "y", {
  writable: false,
  value: 1,
});

const a = new MyClass();
a.x = 2;
console.log(a.x); // 2
console.log(MyClass.prototype.x); // 1
a.y = 2; // 没有作用;严格模式下会报错
console.log(a.y); // 1
console.log(MyClass.prototype.y); // 1

规范

Specification
ECMAScript Language Specification
# sec-object.defineproperty

浏览器兼容性

BCD tables only load in the browser

参见