Object.defineProperty和Proxy详细解析

1,304 阅读5分钟

Object.defineProperty()

1. 基本用法

作用:对一个对象定义新属性,或修改现有属性

let singo = {}
let singoName = 'XM'
//singo对象添加属性ite,值为singoName
Object.defineProperty(singo, 'ite', {
    //默认不可枚举且for in打印打印不出来,可设置:enumerable: true
    //默认不可以修改,可设置:wirtable:true
    //默认不可以删除,可设置:configurable:true
    get: function () {
        console.log('调用get')
        return singoName
    },
    set: function (val) {
        console.log('调用set')
        singoName = val
    }
})

//当读取singo对象的namp属性时,触发get方法
console.log(singo.ite)

//singo.ite发现修改成功
singoName = 'XK'
console.log(singo.ite)

// 对singo.ite进行修改,触发set方法
singo.ite = 'XH'
console.log(singo.ite)

我们成功监听了singo上的ite

2.监听多个属性

上面我们只监听单个属性,但是我们是实战项目中会经常会监听多个属性,这时可以通过Object.keys(obj) or (for in)来返回obj对象身上的所有可枚举属性,方法效果这里就不在展示,同时在新增一个方法(obsever),用来让get中return的值并不是直接访问obj[key]而是val。

let singo = {
    name: '',
    age: 0
}
function defineProperty(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问${key}`)
            return val
        },
        set(newVal) {
            console.log(`${key}被修改为${newVal}`)
            val = newVal
        }
    })
}
// observer
function observer(obj) {
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key])
    })
}
observer(singo)
console.log(singo.age)
singo.age = 20
console.log(singo.age)

3.深度监听

上述代码基础上,加上递归就可以解决,可以看到observer方法就是我们的监听函数,我们想要的效果是将一个对象传入传入就能对其监视,那我们在defineProperty()函数中添加一个递归判断对象是属性是否也是一个对象

function defineProperty(obj, key, val) {
    //如果当前属性也是一个对象,递归进入该对象,进行监听
    if(typeof val === 'object'){
        observer(val)
    }
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${key}属性`)
            return val
        },
        set(newVal) {
            // 如果newVal是一个对象,递归进入该对象进行监听
            if(typeof newVal === 'object'){
                observer(key)
            }
            console.log(`${key}被修改为${newVal}`)
            val = newVal
        }
    })
}

observer中也需要添加递归停止条件

function Observer(obj) {
    //如果传入的不是一个对象,return
    if (typeof obj !== "object" || obj === null) {
        return
    }
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key])
    })
}

4.数组监听

那么如果对象的属性是一个数组呢

let arr = [1, 2, 3]
let obj = {}
Object.defineProperty(obj, 'arr', {
    get() {
        console.log('get arr')
        return arr
    },
    set(newVal) {
        console.log('set', newVal)
        arr = newVal
    }
})
console.log(obj.arr)//输出get arr [1,2,3]  正常
obj.arr = [1, 2, 3, 4] //输出set [1,2,3,4] 正常
obj.arr.push(3) //输出get arr 不正常,监听不到push

在数组中通过索引访问或者修改数组中已经存在的元素,是可以出发get和set的,但是对于通过push、unshift增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到,在Vue2.x中,通过重写Array原型上的方法解决了这个问题。

Proxy

在上面的讲述中,我们还有问题没有解决:那就是当我们要给对象新增加一个属性时,也需要手动去监听这个新增属性。

也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
可以看到,通过Object.definePorperty()监听数组是比较复杂的,需要很多手动处理,这也是为什么在Vue3.0转而采用Proxy。接下来让我们一起看一下Proxy是怎么解决这些问题的。

1.基本使用

语法:const a = new Proxy(target, handler)
Proxy对象由两个部分组成:target、handler

  • target:目标对象

  • handler:是一个对象,声明了代理target的指定行为,支持的拦截操作,一共13种

  • Proxy支持的拦截操作

    • get(target,propKey,receiver):拦截对象属性的读取。
    • set(target,propKey,value,receiver):拦截对象属性的设置,返回一个布尔值(修改成功)。
    • has(target,propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
    • deleteProterty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
    • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
    • getOwmPropertyDescript(target,propKey):拦截Object.getOwnPropertyDescriptor(proxy,propKey),返回属性的描述对象。
    • defineProperty(target,propKey,propDesc):拦截Object.defineProperty(proxy,propKey,propDesc)Object.defineProperties(proxy,propDesc),返回一个布尔值。
    • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
    • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
    • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
    • setPrototypeOf(target,proto): 拦截Object.setPrototypeOf(proxy,proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
    • apply(target,object,args):拦截Proxy实例作为函数调用的操作,比如proxy(...args)、proxy.call(object,...args)proxy.apply(...)
    • construct(target,args):拦截Proxy实例作为构造函数调用的操作,比如:new proxy(...args)

通过Proxy,我们可以对设置代理的对象上的一些操作进行拦截,外界对这个对象的各种操作,都要先通过这层拦截。(和defineProperty差不多)

// 需要代理的对象
let singo = {
    age: 10,
    school: '西南大学'
}
let hander = {
    get(obj, key) {
        // 如过存在就返回属性值,如果没有,就返回默认值''
        return key in obj ? obj[key] : ''
    },
    set(obj, key, val) {
        obj[key] = val
        return true
    }
}
//把handler对象传入Proxy
let proxyObj = new Proxy(singo, hander)

// get
console.log(proxyObj.age)//输出10
console.log(proxyObj.school)//输出西电
console.log(proxyObj.name)//输出默认值''

// set
proxyObj.age = 20
console.log(proxyObj.age)//输出20 修改成功

Proxy代理的是整个对象,而不是对象的某个特定属性,之前我们在使用Object.defineProperty()给对象添加一个属性之后,我们对对象属性的读写操作仍然在对象本身,但是一旦使用Proxy,如果想要读写操作生效,我们就要对Proxy的实例对象proxyObj进行操作。

2.数组使用Proxy

let subject = ['数学']
let handler = {
    get(obj, key) {
        return key in obj ? obj[key] : '暂无学科'
    }, set(obj, key, val) {
        obj[key] = val
        //set方法成功时应该返回true,否则会报错
        return true
    }
}

let proxyObj = new Proxy(subject, handler)

// get和set
console.log(proxyObj)//输出  [ '数学' ]
console.log(proxyObj[1])//输出  暂无学科
proxyObj[0] = '语文'
console.log(proxyObj)//输出  [ '语文' ]

// push增加的元素能否被监听
proxyObj.push('英语')
console.log(proxyObj)//输出 [ '语文', '英语' ]