【面试题解】Object.defineProperty 都能 "define" 什么?

839 阅读8分钟

Object.defineProperty 语法

⭐ 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

Object.defineProperty(obj, prop, descriptor)

参数

该方法接收三个参数

  • obj: 要定义或修改属性的目标对象。

  • prop: 要定义或修改的属性的名称。

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

Object.defineProperty() 方法接收的三个参数,均为必传,任何一个不传的话都会报错,prop 可以传空字符串,但是好像没有什么意义。descriptor 可以传空对象 {},属性的默认值为 undefined

(() => {
  // 定义对象
  const myObject = {};
  // 第三个参数可以传{},但不可以不传
  Object.defineProperty(myObject, 'lastName', {});
  console.log('🚀🚀~  myObject:', myObject);
  //第二个参数可以传""
  Object.defineProperty(myObject, '', { value: 'hzw' });
  console.log('🚀🚀~  myObject:', myObject);
})()

image.png

Object.defineProperty() 方法接收的第一个参数可以是一个数组,那么该方法就会给数组添加一个属性。

(() => {
  // 定义空数组
  const myObject = [];
  Object.defineProperty(myObject, 'lastName', {
    value: '123'
  });
  console.log('🚀🚀~  myObject:', myObject);
  console.log('🚀🚀~  myObject.lastName:', myObject.lastName);
})()

image.png

Object.defineProperty() 方法只能对已经存在的对象进行操作,如果对象不存在,不会默认创建新对象,而是会报错。

(() => {
  Object.defineProperty(myObject, 'firstName', {
    value: "han",
  });
})()

image.png

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。

(() => {
  // 定义对象
  const myObject = {
    firstName: "ywly"
  };
  console.log('🚀🚀~ init ~ myObject:', myObject);

  // 修改属性
  Object.defineProperty(myObject, 'firstName', {
    value: "han",
  });
  console.log('🚀🚀~ updataProp ~ myObject:', myObject);

  //添加属性
  Object.defineProperty(myObject, 'lastName', {
    value: "zhiwei",
  });
  console.log('🚀🚀~ addProp ~ myObject:', myObject);

})()

可以看到,我分别打印了原始对象,修改属性后的对象,添加属性后的对象。如果第二个参数 prop 存在,则是修改属性操作,如果 prop 不存在,则是添加属性操作。

image.png

Object.defineProperty() 方法会修改原对象,存在返回值,返回值是修改后的对象。

(() => {
  const myObject = {};
  console.log('🚀🚀~ init ~ myObject:', myObject);
  const result = Object.defineProperty(myObject, 'firstName', {
    value: "han",
  });
  console.log('🚀🚀~ defineProperty ~ myObject:', myObject);
  console.log('🚀🚀~ result:', result);
})()

可以看到我分别打印了原始对象,经过 Object.defineProperty() 处理过的对象,以及Object.defineProperty() 方法的返回值 result

image.png

属性描述符

前面提到 Object.defineProperty() 方法接收的第三个参数叫做 属性描述符,那它到底是个什么东西呢。

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

数据描述符可选键值

value

属性的值,可以是任何有效的 JavaScript 值(数值,对象,函数等),不设置的话默认为 undefined

(() => {
  // 定义对象
  const myObject = {};
  Object.defineProperty(myObject, 'firstName', {
    value: 'han'
  });
  Object.defineProperty(myObject, 'lastName', {});
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // han
  console.log('🚀🚀~  myObject.lastName:', myObject.lastName); // undefined
})()

writable(可写性)

writable 是一个布尔值,决定了目标属性的值(value)是否可以被改变。

⭐ 当 writable 设置为 true 时,使用 赋值运算符 或者 Object.defineProperty() 都可以修改属性的值。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 writable 为 true
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    writable: true
  });
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // han
  myObject.firstName = 'wang'
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // wang
  Object.defineProperty(myObject, 'firstName', {
    value: 'zhang',
  });
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // zhang
})()

⭐ 当 writable 设置为 false 时,使用 赋值运算符 修改属性的值,不会报错,但也不会生效。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 writable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    writable: false
  });
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // han
  myObject.firstName = 'wang'
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); // han
})()

⭐ 当 writable 设置为 false 时,使用 Object.defineProperty() 修改属性的值,会直接报错。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 writable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    writable: false
  });
  Object.defineProperty(myObject, 'firstName', {
    value: 'wang',
  });
  console.log('🚀🚀~  myObject.firstName:', myObject.firstName); 
})()

image.png

存取描述符可选键值

get

属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。如果没有定义 getter,则为 undefined

set

属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 如果没有定义 setter,则为 undefined

(() => {
  // 定义对象
  const myObject = {};
  Object.defineProperty(myObject, 'firstName', {
    get() {
      console.log('🚀🚀~  读取了myObject.firstName:');
      return this.value;
    },
    set(newValue) {
      console.log('🚀🚀~  修改了myObject.firstName:');
      this.value = newValue;
    },
  });
  myObject.firstName = 'wang'
  console.log('🚀🚀~ myObject.firstName:', myObject.firstName);
})()

image.png

共享键值

以下两个是 存取描述符数据描述符 允许共同拥有的键值。

enumerable (可遍历性)

enumerable 是一个布尔值,表示目标属性在 for..inObject.keysJSON.stringify 中是否可遍历。

⭐ 当 enumerable 设置为 true 时,目标属性可被遍历操作。

(() => {
  // 定义对象
  const myObject = {
    age: "23",
    sex: "boy"
  };
  // 设置 enumerable 为 true
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    enumerable: true,
  });
  for (const key in myObject) {
    console.log('🚀🚀~  for in', key);
  }
  console.log('🚀🚀~  Object.keys:', Object.keys(myObject));
  console.log('🚀🚀~  JSON.stringify:', JSON.stringify(myObject));
})()

image.png

⭐ 当 enumerable 设置为 false 时,目标属性不可被遍历操作。

(() => {
  // 定义对象
  const myObject = {
    age: "23",
    sex: "boy"
  };
  // 设置 enumerable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    enumerable: false,
  });
  for (const key in myObject) {
    console.log('🚀🚀~  for in', key);
  }
  console.log('🚀🚀~  Object.keys:', Object.keys(myObject));
  console.log('🚀🚀~  JSON.stringify:', JSON.stringify(myObject));
})()

image.png

for...in 循环包括继承的属性,Object.keys 方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用 Object.getOwnPropertyNames 方法。

configurable (可配置性)

configurable 是一个布尔值,决定了是否可以修改 属性描述对象 以及表示能否通过 delete 删除属性。

⭐ 当 configurablefalse 时,不能否通过 delete 删除属性。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 configurable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: false,
  });
  // 设置 configurable 为 true
  Object.defineProperty(myObject, 'lastName', {
    value: 'zhiwei',
    configurable: true,
  });
  console.log('🚀🚀 before delete ~ myObject.firstName :', myObject.firstName);
  console.log('🚀🚀 before delete ~ myObject.lastName :', myObject.lastName);
  delete myObject.firstName
  delete myObject.lastName
  console.log('🚀🚀 after delete ~ myObject.firstName :', myObject.firstName);
  console.log('🚀🚀 after delete ~ myObject.lastName :', myObject.lastName);
})()

可以看到 configurablefalse 时,delete 属性不会报错,但也不会生效。

可以看到 configurabletrue 时,delete 属性会生效,属性变为 undefined

image.png

⭐ 当 configurabletrue 并且 writabletrue 时,通过 赋值运算符 修改 value 的修改才会生效。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 configurable 为 true,writable 默认为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: true,
  });
  // 设置 configurable 为 true,writable 为 true
  Object.defineProperty(myObject, 'lastName', {
    value: 'zhiwei',
    configurable: true,
    writable: true,
  });
  console.log('🚀🚀 before change ~ myObject.firstName :', myObject.firstName); //han
  myObject.firstName = 'zhang'
  console.log('🚀🚀 after change ~ myObject.firstName :', myObject.firstName); //han 没有生效

  console.log('🚀🚀 before change ~ myObject.lastName :', myObject.lastName); //zhiwei
  myObject.lastName = 'san'
  console.log('🚀🚀 after change ~ myObject.lastName :', myObject.lastName); //san 生效了
})()

⭐ 当 configurablewritable 其中有任意一个为 true 时,通过 Object.defineProperty()方法修改属性的值,就会生效。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 configurable 为 true,writable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: true,
    writable: false
  });
  Object.defineProperty(myObject, 'firstName', {
    value: 'wang',
  });
  console.log('🚀🚀~ myObject.firstName:', myObject.firstName); // wang
})()
(() => {
  // 定义对象
  const myObject = {};
  // 设置 configurable 为 false,writable 为 true
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: false,
    writable: true
  });
  Object.defineProperty(myObject, 'firstName', {
    value: 'wang',
  });
  console.log('🚀🚀~ myObject.firstName:', myObject.firstName); // wang

})()

⭐ 当 configurablefalse 时, enumerableconfigurable 都不可修改,会直接报错。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 enumerable 为 true
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: false,
    enumerable: true,
  });
  // 设置 enumerable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'wang',
    configurable: false,
    enumerable: false,
  });
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

⭐ 当 configurablefalse 时, writable 可以由 true 改为 false ,但不可由 false 改为 true ,否则会报错。

(() => {
  // 定义对象
  const myObject = {};
  // 设置 writable 为 true
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
    configurable: false,
    writable: true
  });
  // 设置 writable 为 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'wang',
    configurable: false,
    writable: false,
  });
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

⭐ 当 configurablefalse 时, gettersetter 方法无法被修改。

(() => {
  // 定义对象
  const myObject = {};
  Object.defineProperty(myObject, 'firstName', {
    configurable: false,
    get() {
      console.log('🚀🚀~  读取了myObject.firstName:');
      return this.value;
    },
    set(newValue) {
      console.log('🚀🚀~  修改了myObject.firstName:');
      this.value = newValue;
    },
  });
  Object.defineProperty(myObject, 'firstName', {
    configurable: false,
    get() {
      console.log('🚀🚀~  读取了myObject.firstName:');
      return this.value + 'y';
    },
    set(newValue) {
      console.log('🚀🚀~  修改了myObject.firstName:');
      this.value = newValue + 'y';
    },
  });
})()

image.png

描述符默认值

通过字面量方式定义属性

(() => {
  //configurable/writable/enumerable 默认都是 true
  const myObject = {
    firstName: "han"
  };
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

通过构造函数方式定义属性

(() => {
  //configurable/writable/enumerable 默认都是 true
  function Obj(firstName) {
    this.firstName = firstName;
  }
  let myObject = new Obj("han");
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

通过 new Object() 定义属性

(() => {
  const myObject = new Object();
  myObject.firstName = "han";
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

通过 Object.defineProperty () 定义属性

(() => {
  const myObject = {};
  //configurable/writable/enumerable 默认都是 false
  Object.defineProperty(myObject, 'firstName', {
    value: 'han',
  });
  console.log('🚀🚀~ Descriptor:', Object.getOwnPropertyDescriptor(myObject, 'firstName'));
})()

image.png

属性描述符小结

描述符可拥有的键值

configurableenumerablevaluewritablegetset
数据描述符XX
存取描述符XX

⭐ 一个描述符只能是这两者其中之一,不能同时是两者。

⭐ 如果一个描述符不具有 valuewritableget 和 set 中的任意一个键,那么它将被认为是一个数据描述符。

⭐ 如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

实现一个简单的双向绑定

//html
<body>
  <h1>Object.defineProperty</h1>
  <input type="text" id="input" />
  <div id="div"></div>
</body>

//js
(() => {
  const obj = {};
  const inputVal = document.getElementById("input");
  const div = document.getElementById("div");

  Object.defineProperty(obj, "name", {
    set: function(newVal) {
      inputVal.value = newVal;
      div.innerHTML = newVal;
    }
  });
  inputVal.addEventListener('input', (e) => {
    obj.name = e.target.value;
  });
})()

61.gif

其它相关的方法

Object.defineProperties()

定义多个属性,内部循环调用 Object.defineProperty

Object.prototype.propertyIsEnumerable()

propertyIsEnumerable() 方法返回一个布尔值,表示指定的属性是否可枚举。

Object.entries()

Object.entries() 方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。

Object.values()

Object.values() 方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。

Object.keys()

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。

Object.freeze()

Object.freeze()  方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。

Object.isFrozen()

Object.isFrozen() 方法判断一个对象是否被冻结。

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。

Object.preventExtensions()

Object.preventExtensions() 方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

Object.isExtensible()

Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

Object.isSealed()

Object.isSealed()  方法判断一个对象是否被密封。

Object.seal()

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

参考

兼容性👉👉 Can I use

MDN Object.defineProperty()