Object.defineProperty()方法

Object.defineProperty()方法详解

查看原文

Object.defineProperty() 方法可以直接给对象定义一个新的属性, 或者修改对象上已存在的某个属性, 最后返回这个对象.

语法

Object.defineProperty(obj, prop, descriptor)

Parameters

obj
需要被定义属性的对象

prop
需要被定义(或修改)的属性名

descriptor
对需要被定义(或修改)的属性的描述

介绍

这个方法允许精确的添加或修改对象的属性.通过普通的分配方式给对象添加的属性,会在对象属性被枚举时展示(比如for in循环或者使用Object.keys),它的值也可以被修改,被delete.这个方法允许你通过更详细的配置来修改这些默认的行为. 默认情况下,通过 Object.defineProperty() 定义的属性值是不可变的.

对象的属性的描述符(即descriptor参数)有两种方式定义: 数据描述符(data descriptor)和存取器描述符(accessor descriptor).
数据描述符表示:这个属性有一个值,这个值可以是可写的,也可以是不可写的.
存取器描述符表示:这个属性有一对函数,一个是获取函数,一个是设置函数(getter&setter);
描述符必须是这两者之一,不能都有.

数据描述符和存取器描述符都是对象. 他们都需要下列属性:

configurable
true: 当且仅当描述符可以被修改并且属性可以从相应的对象中被删除的时候
默认是false.

enumerable
true: 当且仅当这个属性需要在对象属性被枚举的时候被展示出来.
默认是false.

数据描述符还拥有下列可选的属性:

value
属性对应的值.可以是任何有效的Javascript值(数值,对象,函数,等等).
默认是undefined.

writable
true: 当且仅当属性的值可以通过赋值操作而被改变时.
默认是false

存取器描述符还拥有下列可选的属性:

get
一个函数,它是属性的获取函数.如果没有定义获取函数,它就是undefined.函数的返回值就会成为这个属性的值.
默认是undefined.

set
一个函数,它是属性的设置函数,如果没有定义设置函数,它就是undefined.当属性值被赋值(通过普通方式)的时候,这个新值会被作为唯一的参数传入.
默认是undefined.

记住,上面这些选项不一定要是对象自身的属性,所以还需要考虑是否被继承的问题.为了确保在不写这些选项时,它取到的是默认值,你需要预先冻结Object.prototype. 所以,明确的指定每个选项,或者把 __proto__ 属性指定为 null.

// 冻结 __proto__
var obj = {};
Object.defineProperty(obj, 'key', {
  __proto__: null, // no inherited properties
  value: 'static'  // not enumerable
                   // not configurable
                   // not writable
                   // as defaults
});

// 明确指定每个选项值
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static'
});

// 重用同一个对象
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: null
    }
  );
  d.value = value;
  return d;
}
// ... 然后 ...
Object.defineProperty(obj, 'key', withValue('static'));

// 如果freeze方法是可用的,阻止对对象 prototype 属性进行新增或删除.(value, get, set, enumerable, writable, configurable)   
(Object.freeze || Object)(Object.prototype);

Examples

如果你想知道如何使用二进制标识风格句法来使用 Object.defineProperty 方法,查看额外例子 (这个好恶心…━┳━ ━┳━)

新建一个属性

当对象里没有既存的指定属性时,Object.defineProperty()方法会按照描述创建一个新的属性.描述符里的所有选项都可以省略,被省略的选项会取默认值.所有的布尔值都默认为false. value,get,set选项默认是undefined.如果一个属性,它的描述符里没有 get/set/value/writable 这些选项,它被称为 ‘通用型(generic)’, 并且被归类到数据描述符.

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

// 一个通过数据描述符,使用defineProperty来添加属性的例子
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true
});

// 'a' 属性存在于 o 对象中,它的值是37 

// 一个通过存取器描述符,使用defineProperty来添加属性的例子 
var bValue = 38;
Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(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: function() { return 0xdeadbeef; }
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

补充说一下这里的set选项,get选项很简单,就是通过这个函数来获取对象对应的属性值,而set函数,是当你使用 . 操作符来给属性分配值的时候被调用的. 需要注意的是,当使用 . 操作符来给属性分配值的时候,先调用 setter ,然后还会调用 getter.考虑以下代码:

Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue+1; },
  enumerable: true,
  configurable: true
});
o.b; // 38
o.b = 39;
console.log(o.b) //40

当使用 o.b = 39 来给对象分配属性值时, 首先调用 setter ,然后调用 getter, 而o.b最终的值,依然是 getter 得到的结果.

修改属性

当属性已经存在, Object.defineProperty() 会尝试根据描述符里的值和对象目前的配置来修改这个属性. 如果原来的描述符里,configurable 属性被设置为 false (代表这个属性不能被配置),那么,除了writable之外的所有属性都不能被修改.在这样的情况下,也不能切换描述符的类型,不能把数据描述符改成存取器描述符,反之也不能.

如果一个属性是不可配置的,它的 writable 属性只能被修改成 false.

尝试修改一个不可配置的属性(除了 writable),除非修改后的值和原来一样,否则会抛出一个 TypeError .

Writable 属性

writable 属性被设置为 false 的时候, 表示这个属性是不可写的. 它不能被重定义值.

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

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

console.log(o.a); // 打印出 37
o.a = 25; // 不会报错(如果是在严格模式下还是会报错,即使你定义的值和原来一样,也会报错)
console.log(o.a); // 打印出 37. 之前的定义没有生效

就如这个栗子所见, 尝试写入一个不可写的属性不会生效,但也不会报错

虽然不能重写,但是是否可以删除取决于configurable,而不是writable

Enumerable 属性

enumerable 属性定义了对象属性在使用for…in循环或者Object.keys()枚举时是否被展示

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 默认为false
o.d = 4; // 使用这种方式设置的时候,enumerable 默认为true

for (var i in o) {
  console.log(i);
}
// 打印出 'a' 和 'd' (in undefined order)

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

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

Configurable 属性

configurable 同时控制了该属性是否能从对象中被删除,以及(描述符里的)属性(除了writable)是否可以被修改.

var o = {};
Object.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', { configurable: true }); // throws a TypeError
Object.defineProperty(o, 'a', { enumerable: true }); // throws a TypeError
Object.defineProperty(o, 'a', { set: function() {} }); // throws a TypeError (set属性值之前是undefined)
Object.defineProperty(o, 'a', { get: function() { return 1; } }); // throws a TypeError (即使新的函数做的事情和原来的一模一样)
Object.defineProperty(o, 'a', { value: 12 }); // throws a TypeError

console.log(o.a); // logs 1
delete o.a; // 什么也没发生
console.log(o.a); // logs 1

如果 o.aconfigurable 属性被设置为 true , 以上报错都不会有,最后这个属性会被删除.

添加对象属性时的默认值

有一个重要的事需要被考虑到:在你为对象添加属性的时候,属性的默认属性是怎么样的.通常来说,简单地使用 . 操作符和使用 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

下面的例子展示了如何实现一个自动存档的对象.每当温度属性被设置, 存档数组都会有相应的记录.

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 }]

版本说明

版本 状态 备注
‘Object.defineProperty’方法被定义在了ECMAScript 5.1 (ECMA-262) ST 标准 初次定义. 在JavaScript 1.8.5 里被实现
‘Object.defineProperty’方法被定义在了ECMAScript 2015 (6th Edition, ECMA-262) ST 标准
‘Object.defineProperty’方法被定义在了ECMAScript 2016 Draft (7th Edition, ECMA-262) D 草案

浏览器兼容性

桌面端

特性 Firefox (Gecko) Chrome Internet Explorer Opera Safari
基本支持 4.0(2) 5 9[1] 11.60 5.1[2]

[1] 在ie8下只支持DOM对象, 而且有一些非标准行为.
[2] Safari5 也支持, 但不支持DOM对象.

移动端

特性 Firefox Mobile (Gecko) Android IE Mobile Opera Mobile Safari Mobile
基本支持 4.0(2) (支持) 9 11.5 (支持)

兼容性问题补充说明

重新定义数组对象的 length 属性

重新定义数组的 length 属性是可行的,但也受制于普通的重定义时的限制.(length属性一开始是不能配置,不可枚举,但是可写的.所以,改变一个没有被修改过(描述符)的数组的 length 属性是可行的.也可以把它改成不可写的,但是不允许修改它的可枚举性和可配置性.如果它已经被改成不可写的,也不能修改它的值,也不能修改它的可写性.)然而,并不是所有的浏览器都允许重定义它的.

如果在 Firefox 4 到 22 里尝试此操作会抛出一个 TypeError , 无论它是否允许重定义数组的 length 属性.

某些版本的Chrome浏览器在使用 Object.defineProperty() 来修改数组 length 属性时,如果修改后的值不同于数组当前的 length 属性,它会忽略这次修改.在某些情况下,修改它的可写性其实也没有生效(也不会报错).另外,与此相关的是,一些修改数组的方法,比如 Array.prototype.push 却无视 length 属性的不可写性.(就是说,使用push方法,数组的length属性值依然会被改变,即使它是不可写的.)

某些版本的Safari浏览器在使用 Object.defineProperty() 来修改数组 length 属性时,如果修改后的值不同于数组当前的 length 属性,它会忽略这次修改,还会尝试去改变它的可写性,但最终,属性的可写性并没有被改变,这个过程也不会报错.

只有ie 9 及以后的版本, Firefox 23 及以后的版本, 开始完全地,正确地支持重定义数组的 length 属性.到目前为止,不要指望使用 Object.defineProperty() 重定义数组的 length 属性能够以一种通用的方式正常工作.而且,就算它靠得住,也没有足够的理由去这样做.

针对ie8的特别说明

ie 8 下的 Object.defineProperty() 方法只能被用在DOM对象上.有一些事情需要注意:

  • 如果尝试在原生的对象上使用 Object.defineProperty() 方法,会报错.
  • 定义属性时,描述符的选项必须被设置为指定的值. 如果是数据描述符,configurable,enumerable,writable 都必须被设置为true,如果是存取器描述符, configurable 必须是 true, enumerable 必须是 false, 如果设置的值不是这些,会报错.
  • 重定义属性的话,需要把原来的属性给删掉.如果原来的属性没有删掉,重定义不会生效,它还是保持原来的属性.

看看相关内容