对于Object.defineProperty的深入理解

350 阅读5分钟

定义或作用:

efineProperty()`方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性并返回此对象。该方法允许精确地添加或修改对象的属性

用法:

Object.defineProperty(obj, prop, descriptor)

  • obj: 要定义属性的对象(也是返回值)
  • prop: 要定义或者修改的属性名称/Symbol
  • descriptor: 要定义或者修改的属性描述符
  • 定义/修改:
let obj = {}
console.log(obj.a); // undefined
Object.defineProperty(obj,'a',{
  value:1
})
console.log(obj.a); // 1
  • 返回此对象:
let obj = {}
let newObj = Object.defineProperty(obj,'a',{
  value:1
})
console.log(obj === newObj); // true

描述

该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是额可以枚举的,在枚举对象属性时会被枚举到for...in / Object.keys 方法,可以改变这些属性的值也可以删除。这个方法允许修改默认的额外选项/配置。默认情况下,使用Object.defineProperty()添加的属性值是不可以修改的。

  • 枚举的差异
let obj = {}
obj.a = 'test'
// 有输出
for(let k in obj){
  console.log('normal');
  console.log(k);
}
// 非空数组
let objKeys = Object.keys(obj) // ['a']
console.log(objKeys);

let objSp = {}
Object.defineProperty(obj,'a',{
  value:1
}) 
// 没有输出
for(let k in objSp){
  console.log('Object.defineProperty'); 
  console.log(k);
}
// 输出空数组
let objSpKeys = Object.keys(objSp) // []
console.log(objSpKeys);
  • 枚举的拓展
let obj = {}

Object.prototype.b = '2'

Object.defineProperty(obj,'a',{
  value:1
})
// 不仅遍历自身的,还会遍历原型的属性
for(let k in obj){
  console.log(k); // b
}
// 只遍历自身可以枚举的属性
console.log(Object.keys(obj));
// 遍历自身的属性 无视是否可以被枚举
console.log(Object.getOwnPropertyNames(obj));
  • 改变/删除
// 可以被修改和删除
let obj = {}
obj.a = '1'
console.log(obj); // {a:"1"}
obj.a = '2'
console.log(obj); // {a:"2"}
delete obj.a
console.log(obj); // {}

// 不可以被修改和删除
let objSp = {}
Object.defineProperty(objSp,'a',{
  value:'1'
})
console.log(objSp); // {a:"1"}
objSp.a = '2'
console.log(objSp); // {a:"1"}
delete objSp.a
console.log(objSp); // {a:"1"}

descriptor对象存在有两种主要形式的属性描述符: 数据描述符和存取描述符。数据描述符是具有值的属性。存储描述符是有getter和setter函数所描述的属性,一个描述符只能是这两者其中之一,不能同时是两者。

它们共享以下可选键值:

  • configurable: 只有为true时,1. 该属性的描述符才能改变,2. 同时该属性也能从对应对象上被删除。默认false。
// configurable
let objSp = {}
Object.defineProperty(objSp,'a',{
  configurable:true,
  value:"2"
})
console.log(objSp);
// 删除属性
delete objSp.a
console.log(objSp); // {}
// 改变描述符
Object.defineProperty(objSp,'a',{
  configurable:true,
  value:"3"
})
console.log(objSp); // {a:"3"}

let objSpSecond = {}
Object.defineProperty(objSpSecond,'a',{
  configurable:false,
  value:"2"
})
console.log(objSpSecond);
// 删除属性
delete objSp.a
console.log(objSpSecond); // {a:"2"}
// 改变描述符
Object.defineProperty(objSpSecond,'a',{
  configurable:false,
  value:"3"
})
console.log(objSpSecond); // 报错
  • enumerable:只有为true时,才可以被枚举。默认false。
// enumerable
let objSp = {}
Object.defineProperty(objSp,'a',{
  enumerable:true,
  value:"2"
})
// 有输出
for(let k in objSp){
  console.log(k);
}
// 非空数组
let objSpKeys = Object.keys(objSp) // ['a']
console.log(objSpKeys);
数据描述符:
  • value:设置属性对应的值,可以试任何有效的js值(数值,对象,函数等)。默认undefined。
let objSp = {}
Object.defineProperty(objSp,'a',{
  value:"2"
})
console.log(objSp); // {a:"2"}
  • writable:当为true是,value才能被复制运算符修改,即属性的值才可以修改。默认false。
let objSp = {}
Object.defineProperty(objSp,'a',{
  value:"2",
  writable:true
})
console.log(objSp); // {a:"2"}
objSp.a = '3'
console.log(objSp); // {a:"3"}
  • get:访问属性时,会调用这个函数,执行时不会传入任何参数,会传入this对象。返回值默认undefined。
  • set : 当属性被修改时执行这个函数,接受一个参数(也就是被赋予的新值),会传入this对象。 返回值默认undefined。
let objSp = {}
let value = null
Object.defineProperty(objSp,'a',{
  get(){
    console.log('查看a的值');
    return value
  },
  set(newVal){
    console.log('改变a的值');
    value = newVal
  }
})
console.log(objSp.a); // 查看a的值 1 
objSp.a = 2 
console.log(objSp.a);

拥有布尔值的键: configurable,enumerable,writable的默认值都是false

属性值和函数的键: value,get,set的字段默认值都是undefined

注意:描述符不能同时拥有value/writableget/set键,不然会报错。就是说有了get或者set中的一个就不能有value或者writable,反之同理。

前置知识

  • return
如果if里面有return的话,就不会再执行if外面后面的函数了
function setDataProp(data,dataDefine){
  // console.log(data,dataDefine);
  // 对于传入的data进行判断
  if(typeof data !== 'object' || data === null){
    console.log('test');
    throw TypeError('we need object or array')
  }
  // 因为if里面有return就不会走到后面的代码了!
  if(!Array.isArray(data)){
    return strictData(data,dataDefine)
  }
  // 第一个return为函数的,第二个为map的
  return data.map((obj)=>{
    return strictData(obj,dataDefine)
  })
}

方法的应用

需求:通过前置处理数据的属性,达到精确控制数据的属性目的,同时也提供可以修改描述符的方法。

返回的数据:

  {
    name: "zxx",
    age: 21,
    job: "前端工程师",
    key: 45,
  },
  {
    name: "plw",
    age: 18,
    job: "ui工程师",
    key: 46,
  },
  {
    name: "test",
    age: 24,
    job: "后端工程师",
    key: 47,
  },
];

设置属性值:

export default {
  name:{
    configurable:true,
    enumerable:true,
    writable:false
  },
  age:{
    configurable:true,
    enumerable:true,
    writable:false
  },
  job:{
    configurable:true,
    enumerable:true,
    writable:true
  },
  key:{
    configurable:true,
    enumerable:false,
    writable:false
  }
}

index.js:

;(()=>{
  // 处理函数
  let res = setDataProp(data,dataDefine)
})()

处理函数:

function setDataProp(data,dataDefine){
  // 对于传入的data进行判断
  // 先过滤非Obj
  // 在根据Array和Obj进行不同操作
  if(typeof data !== 'object' || data === null){
    console.log('test');
    throw TypeError('we need object or array')
  }
  if(!Array.isArray(data)){
    return strictData(data,dataDefine)
  }
  // 第一个return为
  return data.map((obj)=>{
    return strictData(obj,dataDefine)
  })
}


function strictData(obj,dataDefine){
  // console.log(obj,dataDefine);
  let keys = Object.keys(dataDefine)
  // console.log(keys);
  // 这里用新的对象,而不是旧的对象
  // 便于后面再对象的原型上挂载修改属性的方法
  let newObj = new CreateNewObj()
  keys.forEach((key)=>{
    Object.defineProperty(newObj,key,{
      value:obj[key],
      ...dataDefine[key]
    })
  })  
  return newObj
}

构造函数:

// 注意这里的构造函数只能在写在另外的文件,因为import会把这个js执行一遍
// 如果写在原来的页面,挂载原型的方法会在后面执行,会对后续的业务要求有差别
// 原型上挂载一个方法可以修改属性的true/false
CreateNewObj.prototype.setConfig = function(key,prop,value){
  Object.defineProperty(this,key,{
    [prop]:value
  })
}

// 会产生一个现象:用for in 会遍历出来原型的对象,Object.keys到不会
// 因此再对原型对象进行操作
function CreateNewObj(){
  let keys = Object.keys(CreateNewObj.prototype) 
  keys.map((key)=>{
    Object.defineProperty(CreateNewObj.prototype,key,{
      configurable:false,
      enumerable:false,
      writable:false
    })
  })
}

// 至此大功告成!