likes
comments
collection
share

一文详解Proxy与Reflect

作者站长头像
站长
· 阅读数 19

前言

Vue3摒弃了传统的Object.defineProperty,转而使用Proxy来代替响应式,使用前者来完成劫持对象的属性时,不仅需要对每个涉及响应式的对象(以及该对象中的每个属性进行遍历,且如果属性值是对象,还需要深度遍历),而如果是对数组进行响应式,那这就又是另外一个头疼的话题了,但这一切的一切都可以使用“天生不凡”的Proxy来避免

Object.defineProperty

在揭开Proxy的面纱之前,我们先来了解一下Object.defineProperty,该方法接收三个参数

  • obj:添加新属性的对象
  • property:要添加的属性
  • descriptor:新属性的描述

下面我们使用一个例子来简单回顾它的应用

const obj = { name: '鲨鱼辣椒' }
console.log(obj) // {name: '鲨鱼辣椒'}
Object.defineProperty(obj, 'age', {
    value: 3,
    writable: true,
    configurable: true,
    enumerable: true,
})
console.log(obj) // {name: '鲨鱼辣椒', age: 3}

首先obj对象中默认带有name属性,然后通过Object.definePropertyobj增加一个age属性,并添加了对age属性的描述;value表示该属性的值为3,writable表示该属性可以被修改,configurable表示该属性可以被配置,enumerable则表示该属性可以被枚举

一文详解Proxy与Reflect

可以看到使用上述方式为对象添加的属性age是立即可见的,那我们现在使用gettersetter再来为obj添加属性会得到什么结果呢

const obj = { name: '鲨鱼辣椒' }
console.log(obj) // {name: '鲨鱼辣椒'}
Object.defineProperty(obj, 'age', {
    get() {
        return 3
    },
    set(v) {},
})
console.log(obj) // {name: '鲨鱼辣椒'}

一文详解Proxy与Reflect

使用gettersetter这种方式时,新添加的属性并不会立即可见,而是在访问/修改时可见

现在我们来对age属性进行操作

const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    get() {
        console.log('obj.age被访问了')
        return 3
    },
    set(v) {
        console.log(`obj.age被修改了,要想修改的值为:${v}`)
    },
})
console.log('我访问到的obj.age值为:', obj.age) // 3
console.log('接下来修改obj.age')
obj.age = 4
console.log('我访问到的obj.age值为:', obj.age) // 3

在访问age属性时,会触发get函数;在修改age属性时,会触发set函数

好了,现在提出疑问,如果我们的age不再是一个简单的属性,而是一个对象,那么会发生什么呢?

const obj = { name: '鲨鱼辣椒' }
// 假设date是作为某个参数传递进来的(因此我们不能直接在date字面量中使用getter与setter)
const date = {
    birth: 1997,
    current: 2022,
    value: 25
}
Object.defineProperty(obj, 'age', {
    get() {
        console.log('obj.age被访问了')
        return date
    },
    set(v) {
        console.log(`obj.age被修改了,要想修改的值为:${v}`)
    },
})
console.log('我访问到的obj.age值为:', obj.age) // {birth: 1997, current: 2022, value: 25}
console.log('接下来修改obj.age.value')
obj.age.value = 26
console.log('我访问到的obj.age值为:', obj.age) // {birth: 1997, current: 2022, value: 26}
console.log('data对象:', date) // {birth: 1997, current: 2022, value: 26}

现在我们可以看到obj.age的属性值为date对象,而修改obj.age.value也就是修改date.value,如果我想在修改obj.age.value时得到通知,那这种情况下恐怕要对date对象中的value进行Object.defineProperty了,而这仅仅只是age属性,如果还有其它属性呢?如果某个对象中又嵌套了其它对象或是数组,那可能就要递归遍历当前对象实现监听了,毫无疑问,这样进行监听的方式其代码量的复杂程度是不言而喻的,那有没有一种简便的方式来胜任这项工作呢?有,那就是——Proxy

代理 Proxy

定义

在简单回顾完Object.defineProperty后,至于为什么要使用Proxy这个问题,想必各位已经得出答案了。我们先来看下MDN中对于Proxy给出的解释

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

通俗的来讲,Proxy用于给源对象增设一层代理对象,对代理对象的所有操作都会被捕获器所捕获,并由捕获器决定是否将此次行为映射到源对象上。但我们要格外注意,只有通过代理对象进行的操作才会被捕获器捕获到,如果是直接操作源对象则不会被捕获。使用Proxy我们几乎可以捕获对象的所有行为,理论上我们可以通过这种方式来无限扩展对象的特性

Proxy也是存在代理局限性的,因为你操作源对象时,捕获器并不会被触发

换个方式理解Proxy

还记得《一篇文章让你反问面试官Vue/React中的key到底是什么》中所提到的虚拟DOM是对真实DOM的一层抽象吗?因为直接操作真实DOM所带来的负面影响较大,所以考虑在真实DOM上增加一层抽象来得以解决这个问题,而这层抽象就是虚拟DOM。毫不客气的说,虚拟DOM承担了我们对真实DOM所做的一切操作,而Diff算法则会依赖这层抽象来决定是否更新真实DOM

如果把源对象看做“真实DOM”,代理对象看做“虚拟DOM”,捕获器则看做“Diff”算法,这样一来Proxy便与它有了异曲同工之妙。我们对代理对象(虚拟DOM)所做的一切操作,均由捕获器(Diff)决定此次操作是否反映至源对象(真实DOM)中,并由捕获器对此次操作给出一系列反馈

一张图看看Proxy的结构

一文详解Proxy与Reflect

兼容性

如果你决定了在项目中大幅使用Proxy时,那么你应当注意一个问题——兼容性。Proxy是在ES6中新增的标准,目前主流浏览器中均已支持,但总有漏网之鱼(其它浏览器)是不支持该特性的,即便是babel也无能为力

我们知道箭头函数也是ES6的产物,而babel在转换箭头函数时会将箭头函数转为普通函数,但对于Proxy却不会做出任何转换

转换前

一文详解Proxy与Reflect

转换后

一文详解Proxy与Reflect

从上图中我们可以看出Proxy依旧是Proxy,如果我们在低于ES6的环境中去运行上述代码是会报错的,因为它不认识Proxy

下图摘自MDN中的Proxy示例

一文详解Proxy与Reflect

使用

Proxy构造函数

使用Proxy构造函数来创建一个代理对象。该构造函数接收两个参数

  • 源对象
  • 处理对象

缺少任何一个参数则抛出TypeError错误

const origin = {}
const handler = {}

const proxy = new Proxy(origin, handler) // 正确
const proxy = new Proxy() // 错误

另外一个要注意的点就是Proxy.prototypeundefined

console.log('Proxy.prototype:', Proxy.prototype) // undefined

知道了上面的注意点之后,我们现在试着提出一个疑问,既然Proxy.prototypeundefined,那是不是就意味着Proxy构造函数的实例对象的__proto__也是undefined

const origin = { name: '鲨鱼辣椒' }
const handler = {}

Object.prototype.age = 25

const proxy = new Proxy(origin, handler)

console.log(proxy) // Proxy {name: '鲨鱼辣椒'}
console.log(proxy.age) // 25
console.log(proxy.__proto__ === Object.prototype) // true

显然不是,proxy.__proto__会指向Object.prototype,其实打开控制台我们可以发现代理对象中的源对象和处理对象的原型对象都是Object.prototype,这是因为所有对象都是由Object构造函数产生的

前面我们说过所有对代理对象的操作最终都会如实地映射至源对象身上,那proxy.__proto__这个操作就相当于origin.__proto__,而origin.__proto__会指向Object.prototype,所以也就不难解释为什么proxy.__proto__会指向Object.prototype

一文详解Proxy与Reflect

现在我们来为代理对象添加get捕获器,并简单增加一个限制条件,当访问name属性时,返回原汁原味的origin.name,除此之外一律返回null;而'__proto__'在此时则属于其它属性,所以会返回null,故proxy.__proto__ === Object.prototype会返回false

const origin = { name: '鲨鱼辣椒' }
const handler = {
    get(origin, property) {
        return property === 'name' ? origin[property] : null
    }
}

Object.prototype.age = 25

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 鲨鱼辣椒
console.log(proxy.__proto__ === Object.prototype) // false
console.log(proxy.__proto__ === Object.prototype.__proto__) // true

初体验

在代理对象上所作的任何操作都会如实的反应到源对象身上,在这里你可以认为代理对象几乎等同于源对象

const origin = { name: '鲨鱼辣椒' }
const handler = {}

const proxy = new Proxy(origin, handler)

// 访问源对象和代理对象身上的name属性
console.log(origin.name) // 鲨鱼辣椒
console.log(proxy.name) // 鲨鱼辣椒

// 修改其中一个值,源对象和目标对象看到的都是修改后的值
proxy.name = '蜘蛛侦探'
console.log(origin.name) // 蜘蛛侦探
console.log(proxy.name) // 蜘蛛侦探

// hasOwnProperty最终都会在源对象身上被调用
console.log(origin.hasOwnProperty('name')) // true
console.log(proxy.hasOwnProperty('name')) // true

控制台中的Proxy实例对象

[[Handler]]为处理对象,[[Target]]为源对象,[[IsRevoked]]代表当前代理对象是否已经被撤销。如果当前代理对象被撤销了,那么[[IsRevoked]]的值是true,而[[Target]][[Handler]]一律为null

你完全可以将 [[Target]] 称作目标对象,但在本篇文章中使用源对象进行称呼

一文详解Proxy与Reflect

广义的对象

Proxy不仅仅可以代理对象(object),还可以代理其它对象(广义上的对象,因为JS中万物皆对象)

// origin是广义上的对象,此处也可以将数组换为函数
const origin = ['鲨鱼辣椒']
const handler = {
    get(origin, index, proxy) {
        console.log(origin, index, proxy)
    },
    set(origin, index, value, proxy) {
        console.log(origin, index, value, proxy)
    }
}

const proxy = new Proxy(origin, handler)
proxy[0] // 触发get捕获器
proxy[1] = '蟑螂恶霸' // 触发set捕获器

这里的源对象origin是一个数组,我们在读取数组的某个值时会触发get捕获器,会接收到三个参数

  • 数组
  • 当前元素所对应的索引
  • 代理对象

在修改数组的某个值时会触发set捕获器,会接收到四个参数

  • 数组
  • 当前元素所对应的索引
  • 修改后的值
  • 代理对象

但此时必须要注意,如果想使用proxy.push来对数组origin增加元素,则set捕获器必须要返回一个值,否则抛出TypeError

const origin = ['鲨鱼辣椒']
const handler = {
    set(origin, index, value) {}
}

const proxy = new Proxy(origin, handler)

proxy.push('蟑螂恶霸') // TypeError

所以set捕获器必须正确设置其值

const origin = ['鲨鱼辣椒']
const handler = {
    set(origin, index, value) {
        return origin[index] = value
    }
}

const proxy = new Proxy(origin, handler)

proxy.push('蟑螂恶霸')
console.log(proxy) // Proxy {0: '鲨鱼辣椒', 1: '蟑螂恶霸'}

如何区分源对象与代理对象

源对象与代理对象都是引用类型的值,类似的值还有数组、函数,这些引用类型的值都可以通过全等运算符进行区分

const origin = {}
const handler = {}

const proxy = new Proxy(origin, handler)

console.log(origin instanceof Object) // true
console.log(proxy instanceof Object) // true

// 使用全等运算符区分源对象与代理对象
console.log(origin === proxy) // false

处理对象

处理对象则是由一系列捕获器所构成的对象。这里所说的捕获器就类似于Object.defineProperty中的gettersetter,但捕获器远比它们要强大。每次对代理对象的操作都会如实地被捕获器所捕获,从而由捕获器给出一系列反馈。常用的捕获器有getsethasdeleteProperty

其它捕获器还有:apply、construct、get、set、defineProperty、deleteProperty、has、ownKeys、isExtensible、preventExtensions、getOwnPropertyDescriptor、getPrototypeOf、setPrototypeOf

如果处理对象为空,那就相当于没有设置任何捕获行为

const origin = {}
const handler = {}

const proxy = new Proxy(origin, handler)

proxy.name = '鲨鱼辣椒'
console.log(origin.name) // 鲨鱼辣椒

get

当通过[].Object.create等操作对代理对象中的属性进行访问时,都会触发get捕获器,同时该捕获器会接收到三个参数

  • 源对象
  • 本次访问的属性
  • 代理对象

接收的参数“代理对象”,这个对象也可以是一个由继承得来的代理对象,不仅仅是get,像set等捕获器也一样

const origin = { name: '鲨鱼辣椒' }
const handler = {
    // get可以返回任意值
    get(origin, property, proxy) {
        console.log('origin, property, proxy:')
        console.log(origin, property, proxy)
        // 但是要注意,如果你返回了proxy[property],那就会造成无限递归
        // 因为get一直在被调用
        return origin[property]
    }
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 鲨鱼辣椒

除此之外,你还可以在get中增加一些限制条件来决定本次的返回值

const origin = { name: '鲨鱼辣椒' }
const handler = {
    get(origin, property, proxy) {
        const v = origin[property]
        return v === '鲨鱼辣椒' ? effect() : v
    }
}
const effect = () => {
    // do something
    return '蜘蛛侦探'
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 蜘蛛侦探

get的限制

当源对象上拥有一个不可写且不可配置的属性时,get返回的值始终要与该属性的值保持一致,否则会抛出TypeError

const origin = {}
Object.defineProperty(origin, 'name', {
    value: '鲨鱼辣椒',
    writable: false,
    configurable: false,
})
const handler = {
    get() {
        // 此处应当返回'鲨鱼辣椒'
        return '蜘蛛侦探'
    }
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // TypeError

set

在修改对象的属性值时被触发,接收四个参数

  • 要修改属性的对象
  • 要修改的属性
  • 新的属性值
  • 代理对象

如果源对象中的某个属性不可写且不可配置,那么set捕获器也就不得改变它的值,只能返回相同的值,否则也会抛出TypeError

const origin = { name: '鲨鱼辣椒' }
const handler = {
    set(origin, property, value, proxy) {
        console.log(`${property}被修改了`)
        origin[property] = value
    }
}
const proxy = new Proxy(origin, handler)
proxy.name = '蜻蜓队长'
console.log(origin.name) // 蜻蜓队长

has

has捕获器会在通过in检查一个对象中是否包含某个属性时触发。会收到两个参数

  • 要检查的对象
  • 要检查的属性

需要注意的是,虽然for...in也用到了in操作符,但has却不会捕获for...in的操作

const origin = { name: '鲨鱼辣椒' }
const handler = {
    has() {
        console.log('has被触发了') // 只会输出一次
    }
}

const proxy = new Proxy(origin, handler)

name in proxy // 触发has捕获器
for (const name in proxy) { } //  // 不会触发has捕获器

对源对象创建多层代理

我们可以根据Proxy的特性来对源对象创建多层代理,也就是为一个源对象添加多个处理对象

const origin = { name: '鲨鱼辣椒' }
const handler = descriptor => ({
    get(origin, property) {
        console.log(descriptor)
        return origin[property]
    }
})

const firstProxy = new Proxy(origin, handler('你正在访问第一层代理'))
const secondProxy = new Proxy(firstProxy, handler('你正在访问第二层代理'))

console.log(secondProxy.name) // 鲨鱼辣椒

反向代理

根据JavaScript中有名的原型链机制来让代理对象成为兜底的对象

const origin = {
    sayName(name) {
        return name
    }
}
const handler = {
    get(origin, property, proxy) {
        return effect
    }
}
const effect = function (name) {
    // do Something
    return name
}

const proxy = new Proxy({}, handler)

// 使Proxy的实例作为其它对象的原型对象
origin.__proto__ = proxy

console.log(origin.sayName('鲨鱼辣椒')) // 鲨鱼辣椒
console.log(origin.sayAge(25)) // 25

当我们调用sayName时,发现origin本身就有,所以直接调用;调用sayAge时,发现origin本身没有,于是顺着原型链查找,而origin.__proto__proxy,于是相当于访问proxy.sayAge,随后触发get捕获器,返回effect函数,并且传参为25,所以便输出25;此处的代理对象就是我们的兜底对象

撤销代理

有时候你可能需要撤销代理对象与源对象之前的关联,此时Proxy.revocable就派上用场了,该方法会返回一个对象,返回的这个对象中包含代理对象proxy与撤销函数revoke,需要注意的是,revoke无论调用多少次,其结果总是相同,但如果在调用之后还试图访问代理对象中的属性,则会抛出TypeError。撤销代理之后不会对源对象造成任何负面影响

Proxy.revocable只是Proxy上的一个方法,而不是像Proxy一样是个构造函数,这也就意味着你不能够new Proxy.revocable,同样它也接收两个参数,即源对象与处理对象

const origin = { name: '鲨鱼辣椒' }
const handler = {
    get(origin, property) {
        return origin[property]
    }
}

const { proxy, revoke } = Proxy.revocable(origin, handler)

console.log(origin.name) // 鲨鱼辣椒
console.log(proxy.name) // 鲨鱼辣椒

// 撤销代理
revoke()
console.log(origin.name) // 鲨鱼辣椒
console.log(proxy.name) // TypeError(触发了捕获器,所以返回TypeError)

应用场景

撤销代理一个可能的应用场景是,假设你有一个很重要的global对象,里面存储了各式各样的API,现在你只想把这个对象暴露出去,那每位用户便都拥有了访问和自定义global对象内容的权力,但如果你只想让用户访问,而不给予它们修改的权力,此时你便可以使用撤销代理了,当global被替换或者里面的数据发生改变时,我们就可以撤销代理,当用户再次访问或试图修改数据时会抛出一个TypeError,用户就可以根据当前错误来清楚的知道我们的本意并做出相应的操作

如何利用代理

监听属性操作

通过getsethas等其它捕获器来对代理对象中的属性进行监听,这样我们就可以感知到对象在某个时刻进行的一些操作(读取、修改、删除等)

当用户访问不存在的属性或方法时,你也可以利用get捕获器来抛出一个ReferenceError

const origin = { name: '鲨鱼辣椒' }
const handler = {
    get(origin, property) {
        console.log(`${origin[property]}被读取了`)
        return origin[property]
    },
    set(origin, property, value) {
        console.log(`${origin[property]}被修改了`)
        origin[property] = value
    }
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 鲨鱼辣椒
proxy.name = '蜻蜓队长'
console.log(proxy.name) // 蜻蜓队长

隐藏属性

不借助Symbol来实现“曲线”隐藏属性

const origin = { name: '鲨鱼辣椒', age: 25 }
const handler = {
    get(origin, property) {
        return property === 'age' ? undefined : origin[property]
    }
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 鲨鱼辣椒
console.log(proxy.age) // undefined

属性验证

通过set捕获器来对每次修改的属性进行验证

不仅仅是set,只要在同一个捕获器中,我们都可以设置多个限制条件

const origin = { name: '鲨鱼辣椒', age: 25 }
const handler = {
    set(origin, property, value) {
        if (property !== 'age') return origin[property] = value
        if (value >= 30) return false
        return origin[property] = value
    }
}

const proxy = new Proxy(origin, handler)

proxy.name = '蜻蜓队长'
proxy.age = 18
console.log(proxy.name) // 蜻蜓队长
console.log(proxy.age) // 18
proxy.age = 31
console.log(proxy.age) // 18

代理存在的问题

在JS中任何函数本质上都是通过某个对象来调用的,比如obj.fun,默认情况下fun中的this就是对象obj。当然了,我们其实一直在享受这种特殊机制所带来的便利,但如果将这种机制发挥在代理对象中,可能会出现不符合我们预期的情况,最典型的问题莫过于在源对象中是否依赖于this作为标识

const origin = {
    name: '鲨鱼辣椒',
    say() {
        // 两次的this并不相同
        return this
    }}
const handler = {}

const proxy = new Proxy(origin, handler)

console.log(origin.say()) // {name: '鲨鱼辣椒', say: ƒ}
console.log(proxy.say()) // Proxy {name: '鲨鱼辣椒', say: ƒ}

还有一个特殊的例子就是Date类型了。根据ECMAScript规范,Date类型方法的执行依赖于this上的内部槽位[[ NumberDate ]],但代理对象毫无疑问是不存在这个槽位的,所以在使用代理对象访问Date类上的方法时会抛出TypeError

const origin = new Date()
const handler = {}

const proxy = new Proxy(origin, handler)

console.log(proxy.getDate()) // TypeError

反射 Reflect

一文详解Proxy与Reflect

传统Object所带来的API已经足够我们使用了,但还是有些许不足,例如Object.defineProperty在对一个不可写且不可配置的属性进行gettersetter时会抛出TypeError,通常我们需要使用try catch去捕获这个错误

需要注意的是,在使用Reflect上的方法时,如果第一个参数不是对象,那么此时会立即抛出TypeError

const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    value: 25,
    writable: false,
    configurable: false
})
console.log(obj.age) // 25
Object.defineProperty(obj, 'age', {
    get() { return }
})
console.log(obj.age) // TypeError

但使用Reflect.defineProperty则不会,而是会返回false来代表此次操作失败

const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    value: 25,
    writable: false,
    configurable: false
})
console.log(obj.age) // 25
console.log(Reflect.defineProperty(obj, 'age', {
    get() { return }
})) // false
console.log(obj.age) // 25

所以从某种层面上来讲,Reflect更符合我们的需求,至于为什么,抛开它的优点不谈,或许我觉得是因为Reflect逼格更高吧,比如

const obj = { name: '鲨鱼辣椒' }
console.log(obj.name) // 鲨鱼辣椒
console.log(Reflect.get(obj, 'name')) // 鲨鱼辣椒

通过上述示例我们可以看到,访问name不仅仅可以通过obj.name的这种形式进行访问,还可以通过刚才所说的全局对象Reflect进行访问,而Reflect可不仅仅只有get这一个方法

其它方法还有:Reflect.apply、Reflect.construct、Reflect.get、Reflect.set、Reflect.defineProperty、Reflect.deleteProperty、Reflect.has、Reflect.ownKeys、Reflect.isExtensible、Reflect.preventExtensions、Reflect.getOwnPropertyDescriptor、Reflect.getPrototypeOf、Reflect.setPrototypeOf

下面我们讲解一些常用的方法

get

接收两个参数

  • 要访问的对象
  • 访问的属性
const obj = { name: '鲨鱼辣椒' }
console.log(Reflect.get(obj, 'name')) // 鲨鱼辣椒

set

该方法的返回值为truefalsetrue代表本次操作成功,false代表失败;操作成功是指对于那些可写且可配置的属性。要注意的是,当操作失败时,在严格模式下会抛出TypeError。该方法接收三个参数

  • 要添加新属性的对象
  • 要添加新属性
  • 描述属性
const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    value: 25,
    writable: false,
    configurable: false
})
console.log(obj.age) // 25
console.log(Reflect.set(obj, 'age', 26)) // false
console.log(obj.age) // 25

has

检查一个对象中是否包含(继承)某个属性,相当于in操作符。接收两个参数

  • 要检查的对象
  • 要检查的属性

返回一个布尔值,代表是否检测到了当前属性

const origin = {age: 25}
const obj = { name: '鲨鱼辣椒' }
obj.__proto__ = origin
console.log('age' in obj) // true
console.log(Reflect.has(obj,'age')) // true

defineProperty

用法基本同Reflect.set一致

const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    value: 25,
    writable: false,
    configurable: false
})
console.log(obj.age) // 25
console.log(Reflect.defineProperty(obj, 'age', {
    get() { return }
})) // false
console.log(obj.age) // 25

deleteProperty

相当于 delete property,该方法接收两个参数

  • 要删除属性的对象
  • 要删除的属性

返回一个布尔值,代表是否删除成功,删除成功是指对于那些可写且可配置的属性

const obj = { name: '鲨鱼辣椒' }
Object.defineProperty(obj, 'age', {
    value: 25,
    writable: false,
    configurable: false
})
console.log(Reflect.deleteProperty(obj, 'name')) // true
console.log(Reflect.deleteProperty(obj, 'age')) // false

ownKeys

接收一个对象作为参数,并将该对象中自有属性符号值不可枚举属性作为数组返回,该数组中的每个成员都是字符串或符号值

类似于Object.getOwnPropertyNames和Object.getOwnPropertySymbols

const origin = { bigName: 'SYLJ' }
const obj = {
    name: '鲨鱼辣椒',
    [Symbol.for('age')]: 25,
}
obj.__proto__ = origin
Object.defineProperty(obj, 'gender', {
    value: '男',
    writable: false,
    configurable: false,
    enumerable: false
})
console.log(Reflect.ownKeys(obj)) // ['name', 'gender', Symbol(age)]

属性排序

一般来说,当我们列举对象中的键(属性名)时,其顺序由于不同引擎的实现所以总是飘忽不定的,有可能这一次列举时A属性在B属性前面,而又有可能在下一次列举时B属性跑到了A属性的前面。为了避免这种尴尬的情况,我们可以使用Reflect.ownkeys来列举对象中的属性,这个方法会遵循以下顺序

  1. 按照数字上升排序
  2. 按照创建顺序列举字符串属性名
  3. 按照创建顺序列举符号属性名
const obj = {
    1: '我的键是整数1',
    one: '我的键是字符串1',
    [Symbol.for('s1')]: '我的键是符号值1',
}
obj.two = '我的键是字符串2'
obj[Symbol.for('s2')] = '我的键是符号值2'
obj[2] = '我的键是整数2'

console.log(Reflect.ownKeys(obj)) // ['1', '2', 'one', 'two', Symbol(s1), Symbol(s2)]

总结Proxy与Reflect

Proxy可以最大限度的弥补Object.defineProperty带来的缺点,而Proxy也不仅仅只能代理对象,还可以代理数组等其它对象

代理可以捕获13种不同的操作,而每种操作都会有一个所对应的ReflectApi,这就使Proxy对象可以方便的调用对应的Reflect方法来完成默认行为。我们前面已经对Proxy与Reflect进行了讲解,现在就让它们结合起来吧

const origin = {
    name: '鲨鱼辣椒',
    age: 25,
    gender: '男'
}
const handler = {
    // 相当于 get(o, p, p){return Reflect.get(o, p, p)}
    // 也可以简写为 get(){return Reflect.get(...arguments)}
    get: Reflect.get,
    set: Reflect.set,
    has: Reflect.has,
}

const proxy = new Proxy(origin, handler)

console.log(proxy.name) // 鲨鱼辣椒
console.log(proxy.age) // 25
proxy.age = 26
console.log(proxy.age) // 26
console.log('gender' in proxy) // true

每个处理对象中的捕获器都有一个对应的Reflect方法,比如get捕获器对应着Reflect.get,对于这种行为,换句话说ProxyReflect总是这么的协同工作