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]] 相同的值。换句话说,读取代理对象的原型必须始终返回被代理对象的原型。
  • ……其他的捕获器依此类推

实例

  1. 当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。

  1. 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

上面实现了一个翻译机,对于词典存在的单词,返回翻译后的结果,对于词典不存在的单词,返回单词本身。

  1. 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捕获器,我们可以对数组的元素进行类型限制,只能添加数字类型的元素。并且数组中的内建方法依然有效。

  1. 使用 “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捕获器设置新增的属性的描述符,从而使遍历器返回新增的属性。

  1. 使用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
  1. 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操作符,返回一个布尔值。

  1. 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 对象的设计目的有这样几个。

  1. 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
  2. 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
  3. 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和 Reflect.deleteProperty(obj, name) 让它们变成了函数行为。
  4. 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
  1. 当我们读取 admin.name 时,由于 admin 对象自身没有对应的属性,搜索将转到其原型。

  2. 原型是 userProxy。

  3. 从代理读取 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)

许多内建对象,例如 MapSetDatePromise 等,都使用了所谓的“内部插槽”。

它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,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中使用

  1. npm install --save reflect-metadata
  2. tsconfig.json 里配置 emitDecoratorMetadataexperimentalDecorators 选项
  3. 在使用的文件引入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 来获取对象的元数据,从而实现数据的校验。

参考:

https://zh.javascript.info/proxy

https://juejin.cn/post/7061405036475056158

评论

0条评论

logo

暂无内容,去看看其他的吧~