Proxy、Reflect、Reflect Metadata
摘要:
关于ES6新增的Proxy、Reflect的使用及注意事项,以及扩展项Reflect Metadata,设置对象的元数据。
Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
语法
js
let proxy = new Proxy(target, handler);
- target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数。
内部方法
Handler 方法 | 何时触发 | 说明 |
---|---|---|
get | 读取属性 | 读取对象的属性值 |
set | 写入属性 | 写入对象的属性值 |
has | in 操作符 | 判断对象是否存在属性 |
deleteProperty | delete 操作符 | 删除对象的属性 |
apply | 函数调用 | 调用函数 |
construct | new 操作符 | 调用构造函数 |
getPrototypeOf | Object.getPrototypeOf | 获取对象的原型 |
setPrototypeOf | Object.setPrototypeOf | 设置对象的原型 |
isExtensible | Object.isExtensible | 判断对象是否可扩展 |
preventExtensions | Object.preventExtensions | 防止对象扩展 |
defineProperty | Object.defineProperty, Object.defineProperties |
定义对象的配置 |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries |
获取自身属性的配置 |
ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries |
获取自身的属性 |
不变量
捕获器必须满足的条件,其中大部分应用于返回值,捕捉器可以拦截这些操作,但是必须遵循下面面这些规则。
- [[Set]] 如果值已成功写入,则必须返回 true,否则返回 false。
- [[Delete]] 如果已成功删除该值,则必须返回 true,否则返回 false。
- 应用于代理(proxy)对象的 [[GetPrototypeOf]],必须返回与应用于被代理对象的 [[GetPrototypeOf]] 相同的值。换句话说,读取代理对象的原型必须始终返回被代理对象的原型。
- ……其他的捕获器依此类推
实例
- 当handler为空时
js
let target = {
name: '张三',
age: 18
}
let proxy = new Proxy(target, {});
proxy.age = 28
console.log(target.age); // 28
console.log(target.name); // '张三'
因为没有对任何行为进行代理,所以proxy是target的透明包装器。由于没有捕捉器,所有对 proxy 的操作都直接转发给了 target。
- get捕获器
js
let dictionary = {
Hello: '你好',
world: '世界'
}
let translator = new Proxy(dictionary, {
get(target, property) {
if(property in target) {
return target[property]
}
return property
}
});
console.log(translator.Hello); // '你好'
console.log(translator.Hi); // 'Hi'
console.log(dictionary.Hi); // undefined
上面实现了一个翻译机,对于词典存在的单词,返回翻译后的结果,对于词典不存在的单词,返回单词本身。
- set捕获器
js
let numbers = [0,1,2];
numbers = new Proxy(numbers, {
set(target, prop, val) {
if(typeof val === 'number') {
target[prop] = val
return true
}
return false
}
})
numbers.push(3)
console.log(numbers); // [0,1,2,3]
console.log(numbers.length) // 4
numbers.push('Hello') // Uncaught TypeError: 'set' on proxy: trap returned falsish for property '4'
通过set捕获器,我们可以对数组的元素进行类型限制,只能添加数字类型的元素。并且数组中的内建方法依然有效。
- 使用 “ownKeys” 和 “getOwnPropertyDescriptor” 控制迭代
js
let user = {
name: 'John',
age: 30,
_password: '***',
checkPassword(password) {
return password === this._password
}
}
let proxy1 = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter((key) => !key.startsWith('_'))
},
})
console.log(Object.keys(proxy1)) // [ 'name', 'age', 'checkPassword' ]
通过ownKeys
捕获器,我们可以控制对象的属性遍历,只返回非下划线开头的属性。(具体何时触发此捕获器,可以参照上面的表格)
js
let proxy2 = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c']
},
})
console.log(Object.keys(proxy2)) // []
如果ownKeys
返回的数组中不包含目标对象的属性,会发现代理中并不能返回这些属性名。
原因很简单:
Object.keys
仅返回带有enumerable
标志的属性。为了检查它,该方法会对每个属性调用内部方法[[GetOwnProperty]]
来获取 它的描述符(descriptor)。在这里,由于没有属性,其描述符为空,没有enumerable
标志,因此它被略过。
js
let proxy2 = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c']
},
getOwnPropertyDescriptor(target, prop) {
return {
writable: true,
enumerable: true,
configurable: true
}
}
})
console.log(Object.keys(proxy2)) // [ 'a', 'b', 'c' ]
可以通过getOwnPropertyDescriptor
捕获器设置新增的属性的描述符,从而使遍历器返回新增的属性。
- 使用ownKeys、get、set、deleteProperty实现类似私有属性
js
let user = {
name: 'John',
age: 30,
_password: '***',
checkPassword(password) {
return password === this._password
}
}
user = new Proxy(user, {
get(target, key) {
if (key.startsWith('_')) {
throw new Error('Access denied')
}
const value = target[key]
// 如果是函数,将this指向原对象,不然如果函数内使用到了拦截的属性,也会过捕获器提示Access denied
return typeof value === 'function' ? value.bind(target) : value
},
set(target, key, value) {
if (key.startsWith('_')) {
throw new Error('Access denied')
}
target[key] = value
return true
},
deleteProperty(target, key) {
if (key.startsWith('_')) {
throw new Error('Access denied')
}
delete target[key]
return true
},
ownKeys(target) {
return Object.keys(target).filter((key) => !key.startsWith('_'))
},
})
try {
console.log(user._password)
} catch (e) {
console.log(e.message) // Access denied
}
try {
user._password = '123'
console.log(user._password)
} catch (e) {
console.log(e.message) // Access denied
}
try {
delete user._password
} catch (e) {
console.log(e.message) // Access denied
}
console.log(Object.keys(user)) // [ 'name', 'age' ]
console.log(JSON.stringify(user)) // {"name":"John","age":30}
console.log(user.checkPassword('***')) // true
console.log(user.checkPassword('123')) // false
- has捕获器(实现数字是否在范围内)
js
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
has捕获器可以拦截in
操作符,返回一个布尔值。
- apply捕获器(函数执行拦截)
js
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy 将“获取 length”的操作转发给目标对象
sayHi("John"); // Hello, John!(3 秒后)
apply捕获器可以拦截函数的调用。
Reflect
Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。
Reflect 对象的设计目的有这样几个。
- 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
- 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
- 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和 Reflect.deleteProperty(obj, name) 让它们变成了函数行为。
- Reflect的每个方法都与Proxy的静态方法一一对应,方便Proxy的创建。
Reflect和Object的对应关系
Reflect 方法 | Object 方法 | 说明 |
---|---|---|
Reflect.apply(target, thisArg, args) | Function.prototype.apply() | 调用一个函数并指定this |
Reflect.construct(target, args[, newTarget]) | new target(...args) | 作为构造函数调用 |
Reflect.has(target, name) | name in target | 判断属性是否存在 |
Reflect.get(target, name, receiver) | target[name] | 读取属性 |
Reflect.set(target, name, value, receiver) | target[name] = value | 设置属性值 |
Reflect.deleteProperty(target, name) | delete target[name] | 删除属性 |
Reflect.defineProperty(target, name, desc) | Object.defineProperty(target, name, desc) | 定义属性 |
Reflect.ownKeys(target) | Object.getOwnPropertyNames(target) .concat(Object.getOwnPropertySymbols(target)) |
获取对象的所有属性名 |
Reflect.getOwnPropertyDescriptor(target, propertyKey) | Object.getOwnPropertyDescriptor() | 获取对象属性描述符 |
Reflect.isExtensible(target) | Object.isExtensible(target) | 判断对象是否可扩展 |
Reflect.preventExtensions(target) | Object.preventExtensions(target) | 防止对象扩展 |
Reflect.getPrototypeOf(target) | Object.getPrototypeOf(target) | 获取对象的原型 |
Reflect.setPrototypeOf(target, prototype) | Object.setPrototypeOf(target, prototype) | 设置对象的原型 |
可以看出
Object
常用的操作在Reflect
中基本都有对应的方法,且Reflect
更加统一。
对于Proxy的构建也可以更加方便
js
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
// ...其他特殊操作
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, val, receiver) {
// ...其他特殊操作
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver);
}
});
let name = user.name; // 显示 "GET name"
user.name = "Pete"; // 显示 "SET name=Pete"
代理一个 getter
js
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
console.log(userProxy.name); // Guest
let admin = {
__proto__: userProxy,
_name: "Admin"
};
console.log(admin.name); // 期待 Admin,但结果为 Guest
-
当我们读取 admin.name 时,由于 admin 对象自身没有对应的属性,搜索将转到其原型。
-
原型是 userProxy。
-
从代理读取 name 属性时,get 捕捉器会被触发,并从原始对象返回 target[prop] 属性。
当调用 target[prop] 时,若 prop 是一个 getter,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 target 的 this._name,即来自 user。
使用Reflect
可以方便解决这个问题
js
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver);
// return Reflect.get(...arguments); // 也可以这样简写
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
console.log(admin.name); // Admin
Proxy 的局限性
代理提供了一种独特的方法,可以在最底层更改或调整现有对象的行为。但是,它并不完美。有局限性。
内建对象:内部插槽(Internal slot)
许多内建对象,例如 Map
,Set
,Date
,Promise
等,都使用了所谓的“内部插槽”。
它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]]
内部方法。所以 Proxy 无法拦截它们。
js
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
在内部,一个 Map 将所有数据存储在其 [[MapData]] 内部插槽中。代理对象没有这样的插槽。内建方法 Map.prototype.set 方法试图访问内部属性 this.[[MapData]],但由于 this=proxy,在 proxy 中无法找到它,只能失败。
可以通过绑定this的方式解决此问题:
js
let map = new Map()
let proxy = new Proxy(map, {
get(target) {
const value = Reflect.get(...arguments)
return typeof value === 'function' ? value.bind(target) : value
}
})
proxy.set('test', 1) // Success
Array 没有内部插槽
内建 Array 没有使用内部插槽。那是出于历史原因,因为它出现于很久以前。
所以,代理数组时没有这种问题。
私有字段
私有字段是一种新的语法,它允许我们在类中定义私有属性。私有属性只能在类的内部访问,外部无法访问。
js
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
console.log(user.getName()); // Error
私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]。
代理对象内没有带有私有字段的插槽,所以无法访问到私有字段。
也可通过绑定this指向来解决这个问题
js
class User {
#name = 'Guest'
getName() {
return this.#name
}
}
let user = new User()
user = new Proxy(user, {
get(target) {
const value = Reflect.get(...arguments)
return typeof value === 'function' ? value.bind(target) : value
}
})
console.log(user.getName()) // Guest
可撤销 Proxy
一个 可撤销 的代理是可以被禁用的代理。
语法:
js
let {proxy, revoke} = Proxy.revocable(target, handler)
例子:
js
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ...我们代码中的其他位置...
revoke = revokes.get(proxy);
revoke();
console.log(proxy.data); // Error(revoked)
Reflect Metadata
Reflect Metadata 是一种元编程技术,它允许我们在运行时获取和设置对象的元数据。元数据是关于数据的数据,它可以用来描述数据的结构、类型、属性等信息。
Reflect metadata 主要用于在代码声明时添加/读取某个对象的元数据(metadata)
安装使用
一般在TypeScript中使用
npm install --save reflect-metadata
- 在
tsconfig.json
里配置emitDecoratorMetadata
、experimentalDecorators
选项 - 在使用的文件引入
import 'reflect-metadata'
或者在index.d.ts
全局引
用法
Reflect.defineMetadata(metadataKey, metadataValue, target, [propertyKey])
设置对象或对象属性的元数据
ts
const user = {
name: 'Job',
age: 18
}
// 添加一个叫`desc`的元数据到user这个对象
Reflect.defineMetadata('desc', '这是一个用户描述', user);
// 添加一个叫`desc`的元数据到user的name上(这里的name可以是其他值或user不存在的属性)
Reflect.defineMetadata('desc', '这是用户的名字', user, 'name');
// 添加一个叫`default`的元数据到user的address上(这里的address可以是其他值或user不存在的属性)
Reflect.defineMetadata('default', '用户无地址', user, 'address');
Reflect.hasMetadata(metadataKey, target, [propertyKey])
检查对象上是否存在元数据
ts
console.log(Reflect.hasMetadata('desc', user)); // 输出: true
console.log(Reflect.hasMetadata('desc', user, 'name')); // 输出: true
console.log(Reflect.hasMetadata('default', user, 'address')); // 输出: true
console.log(Reflect.hasMetadata('desc', user, 'address')); // 输出: false
console.log(Reflect.hasMetadata('desc', user, 'gender')); // 输出: false
Reflect.getMetadata(metadataKey, target, [propertyKey])
获取对象元数据
ts
console.log(Reflect.getMetadata('desc', user)); // 输出: ''这是一个用户描述'
console.log(Reflect.getMetadata('desc', user, 'name')); // 输出: '这是用户的名字'
console.log(Reflect.getMetadata('default', user, 'address')); // 输出: '用户无地址'
console.log(Reflect.getMetadata('desc', user, 'gender')); // 输出: undefined
Reflect.getOwnMetadata(metadataKey, target, [propertyKey])
获取对象或属性自身某个元数据键的元数据值,不包含继承的。
ts
console.log(Reflect.getOwnMetadata('desc', user)); // 输出: ''这是一个用户描述'
console.log(Reflect.getOwnMetadata('desc', user, 'name')); // 输出: '这是用户的名字'
console.log(Reflect.getOwnMetadata('default', user, 'address')); // 输出: '用户无地址'
console.log(Reflect.getOwnMetadata('desc', user, 'gender')); // 输出: undefined
Reflect.getMetadataKeys(target, [propertyKey])
获取对象上所有元数据的keys
ts
console.log(Reflect.getMetadataKeys(user)) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(user, 'name')) // 输出: ['desc']
console.log(Reflect.getMetadataKeys(user, 'address')) // 输出: ['default']
console.log(Reflect.getMetadataKeys(user, 'gender')) // 输出: []
Reflect.getOwnMetadataKeys(target, [propertyKey])
获取对象或属性自身的所有元数据键,不包含继承的。
ts
console.log(Reflect.getOwnMetadataKeys(user)) // 输出: ['desc']
console.log(Reflect.getOwnMetadataKeys(user, 'name')) // 输出: ['desc']
console.log(Reflect.getOwnMetadataKeys(user, 'address')) // 输出: ['default']
console.log(Reflect.getOwnMetadataKeys(user, 'gender')) // 输出: []
Reflect.deleteMetadata(metadataKey, target, [propertyKey])
删除对象上的元数据
ts
// 删除user对象上的`desc`元数据
Reflect.deleteMetadata('desc', user);
// 删除user对象下name属性的`desc`元数据
Reflect.deleteMetadata('desc', user, 'name');
// 删除user对象下address属性的`default`元数据
Reflect.deleteMetadata('default', user, 'address');
应用场景
- 框架开发:框架开发中,我们可以使用 Reflect Metadata 来实现一些框架层面的功能,例如依赖注入、AOP 等。(比如:Nestjs)
- 序列化:在对象序列化时,我们可以使用 Reflect Metadata 来获取对象的元数据,从而实现对象的序列化和反序列化。
- 数据校验:在数据校验时,我们可以使用 Reflect Metadata 来获取对象的元数据,从而实现数据的校验。
参考:
评论
0条评论
暂无内容,去看看其他的吧~