defineProperty与Proxy进阶

204 阅读5分钟

一.Object.defineProperty(obj, prop, descriptor)

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

1.1 数据描述符

数据描述符特有的两个属性:value、writable

1.2 存取描述符

存取描述符特有的两个属性:get、set

1.3 描述符的用法

属性描述符主要有以上两种形式分别是数据描述符和存取描述符,两种形式的属性描述符的使用规则如下:

一个属性描述符只能是数据描述符和存取描述符两者其一

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

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

1.4 举个例子

var obj = {}
Object.prototype.b = 2
var newObj = Object.defineProperty(obj,'a',{
  value :1
  // 为对象添加新的属性与值并返回一个新的对象
})
console.log(obj) // {a:1}
console.log(obj === newObj)  // true
console.log(newObj)  // {a:1}

// 使用defineProperty之后带来的影响
delete obj.a
console.log(obj.a)  // 不可删除
obj.a = 2
console.log(obj.a)  // 不可修改
for(let i in obj){
  console.log(i,obj[i])
  // 只可以枚举原型链上的属性 但是对于obj的自有属性是不可以枚举的
  // 因为 b in i 这种方式可以直接查看到对象本身或者原型链上是否具有该属性
  // 所以当a属性不可枚举的时候 但是b属性是可以枚举的 并且in操作符可以映射到原型链上 所以可以遍历到b
}
console.log(Object.keys(obj))   // 不可枚举的则遍历不到
console.log(Object.getOwnPropertyNames(obj))  // 不可枚举也能获取到自身的属性 但是原型链上的属性获取不到

为什么会带来这样的现象?

因为在使用defineproperty定义之后属性默认设置为

configurable:false

configure表示对象的该属性是否可以被删除,以及其它特性是否可以被修改。false为不可删除。

writable:false

writable表示对象的属性是否可以通过赋值运算符改变其对应的属性值。false为不可修改。

enumerable:false

enumerable定义了对象的属性是否可以通过for in循环以及Object.keys()来枚举。false为不可枚举。

Tips:三种获取属性的方式

for in 可遍历自身对象以及原型链上可枚举的属性

Object.keys() 可遍历自身对象上可枚举的属性

Object.getOwnPropertyNames() 可遍历自身对象上的属性(无论是否可枚举)

1.5 configurable的特殊性

var obj = {}
var newObj = Object.defineProperty(obj,'a',{
  value :1,
  configurable:false,
})
var newObj = Object.defineProperty(obj,'a',{
  value:1,
  configurable:true,
  writable:true,
  enumerable:true,
})

image.png

var obj = {}
var newObj = Object.defineProperty(obj,'a',{
  value :1,
  configurable:true,
})
var newObj = Object.defineProperty(obj,'a',{
  value:1,
  writable:true,
  enumerable:true,
})

当初次定义一个对象的属性的configurable为false的时候后续无法重定义该属性的其它内容!

二.实现一个可扩展defineProperty

扩展成可以实现修改对应属性的对应的操作符

//data.js
export const personalInfo = [
  // obj
  {
    name:'zcl',
    age:'22',
    job:'worker',
    publicKey:980714
  },
  {
    name:'yz',
    age:'22',
    job:'teacher',
    publicKey:980618
  }
]
//dataDefine.js
export const personalInfoDefine = {
  // 描述符
  name:{
    configurable:true,
    writable:false,
    enumerable:true
  },
  age:{
    configurable:true,
    writable:false,
    enumerable:true
  },
  job:{
    writable:true,
    configurable:true,
    enumerable:true,
  },
  publicKey:{
    writable:false,
    configurable:true,
    enumerable:true,
  }
}
//defineObject.js
export function useStrictObject(data,descriptor){
  if(typeof data !== 'object' || data===null){
    throw new TypeError('to be an Object')
  }
  if(!Array.isArray(data)){
    // 如果是对象直接defineProperty
    return defineObject(data,descriptor)
  }
  // 如果是数组 遍历数组中的每个对象进行defineProperty
  return data.map((item)=>{
    return defineObject(item,descriptor)
  })
}

function defineObject(data,desc){
  let _obj = new Modify()
  for(let k in data){
    Object.defineProperty(_obj,k,{ // name
      value:data[k],// 取name对应的属性值
      ...desc[k] // 取name对应的操作符
    })
  }
  return _obj
}

// 我们想要实现可以修改对应属性的对应操作符

// 由于我们在原型上定义了修改属性对应操作符的方法
// 所以我们在遍历对象属性的时候 这个属性也会被遍历出来 所以我们需要对原型上的属性的操作符也进行修改 让其不可遍历
function Modify(){
  for(let k in Modify.prototype){
    Object.defineProperty(Modify.prototype,k,{
      enumerable:false,
      writable:false,
      configurable:false,
    })
  }
}
//demo.js
import {personalInfo} from './data'
import { personalInfoDefine } from './dataDefine'
import {useStrictObject} from './defineObject'


const _personalInfo = useStrictObject(personalInfo,personalInfoDefine)
console.log(_personalInfo)
_personalInfo[0].job = '123'
// _personalInfo[1].name = 'zxcxz'  // 这里会报错 因为name不可修改
_personalInfo[1].modifyDesc('name','writable',true)  // 让第一项的name可以修改
_personalInfo[1].name = 'zc'   // 这样就不会报错了
console.log(_personalInfo[1].name)  // zc

三.Object.defineProperty实现数据劫持的缺点

关于利用Object.defineProperty实现数据劫持的源码可以参考我之前发过的博客Vue源码解析(1) juejin.cn/post/694941…

这部分我们只赘述其缺点:

无法实现对数组的监听

虽然Object.defineProperty能够劫持对象的属性,但是需要对对象的每一个属性进行遍历劫持并且如果对象上需要新增属性还需要对新增的属性再次进行劫持;如果属性是仍是对象还需要深层遍历再次进行递归劫持。

无法检测数组长度的修改

无法监听新增属性和删除属性

四.利用Proxy实现数据代理(劫持)

//index.js
import {reactive} from './reactive'
const state = reactive({
  name:'zcl',
  hobby:['swim','study'],
  info:{
    job:'teacher',
    students:[
      {name:'zm'},
      {name:'whx'}
    ]
  }
})

// 可以实现响应式获取 修改 新增 以及监听数组的长度变化以及对数组操作进行监听

state.name  //直接走get这个handler
state.name = 'yz'   // 直接走set这个handler
state.age = 12   // 走一次set
state.info = {x:{y:2}}  // 直接走set这个handler
state.info.x.y = 3  // 走两次get 一次set
state.hobby.push(['running','hin'])
state.hobby.length = 5
//reactive.js
import {handler} from './mutableHandle'

function reactive(target){
  return new Proxy(target,handler)
}
export {reactive}
//mutableHandle.js
import {isObject,hasOwnProperty,isEqual} from './utils1'
import {reactive} from './reactive'

// 创建handler
function createGetter(){
  return function get(target,key,receiver){
    console.log(key)
    const res = Reflect.get(target,key,receiver)
    console.log('响应式获取:'+res)
    if(isObject(res)){
      return reactive(res)
    }
    return res
  }
}

function createSetter(){
  return function set(target,key,value,receiver){
    const isKeyExist = hasOwnProperty(target,key),
          oldValue = target[key],
          res = Reflect.set(target,key,value,receiver);
    if(!isKeyExist){
      console.log('响应式新增:'+value)
    }else if(!isEqual(value,oldValue)){
      console.log('响应式修改'+key+'='+value)
    }
    return res
  }
}

const get = createGetter(),
      set = createSetter();

const handler = {get,set,del}
export {handler}
function isObject(value){
  return typeof value === 'object' && value!==null
}

function hasOwnProperty(target,key){
  return Object.prototype.hasOwnProperty.call(target,key)
}

function isEqual(newValue,oldValue){
  return newValue === oldValue
}
export {
  isObject,hasOwnProperty,isEqual}

image.png

我们从结果中可以看出Proxy可以完善definpeProperty带来的缺点,对于删除你们可以自己去实现一下其实就是利用deleteProperty这个handler就可以实现。

五.总结

相较于Object.defineProperty劫持某个属性,Proxy则更彻底,不在局限某个属性,而是直接对整个对象进行代理