本文深入解析 JavaScript 中的 Reflect Metadata 规范,介绍如何通过装饰器为类和方法添加自定义元数据。详细说明了 metadata、target、property 等核心概念,对比了 getMetadata 与 getOwnMetadata 的区别,并揭示了其基于 WeakMap 的内部存储机制。
引言
引言
ES6 规范中已包含 Reflect API,其主要功能是将变量操作函数化,即实现反射机制。关于该 API 的详细内容,可参考相关教程。
本文重点讨论 Reflect 中尚未正式纳入规范的部分,即 Reflect Metadata。
Metadata
对于熟悉 Java 或 C# 的开发者而言,Metadata(元数据)并不陌生。最典型的应用是通过反射获取类属性上的注解(在 JavaScript 中对应装饰器),从而更优雅地控制代码逻辑。
JavaScript 目前支持装饰器,尽管仍处于 Stage2 阶段。现有的装饰器主要用于对函数或属性进行操作,例如修改值、代理变量或自动绑定 this 等。
因此,下文将使用 TypeScript 进行讲解,因为 TypeScript 已完整实现了装饰器功能。
虽然 Babel 也能实现,但需要复杂的配置,出于便捷考虑,本文不采用此方案。
然而,现有装饰器无法通过反射获取添加到类或方法上的所有装饰器列表。
为此,Reflect Metadata 应运而生。
Reflect Metadata 核心概念
Reflect Metadata
简单来说,Reflect Metadata 允许你通过装饰器向类添加自定义信息,并通过反射提取这些信息;同样也可以通过反射添加这些信息。示例如下:
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}
Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
// 这里为什么要用 new A(),用 A 不行么?后文会讲到
看起来非常简单,下面进行简要介绍。
概念
首先需要区分四个核心概念:
- Metadata Key {Any},后文简写为 k。这是元数据的键,一个对象可以拥有多个元数据,每个元数据对应一个 Key。例如,可以设置名为 'name' 的 Key 来存储名字,或用 'created time' 的 Key 表示创建时间。Key 可以是任意类型,其内部本质是一个 Map 对象。
- Metadata Value {Any},后文简写为 v。这是元数据的值,可以是任意类型。
- Target {Object},后文简写为 o。表示要在哪个对象上添加元数据。
- Property {String|Symbol},后文简写为 p。用于指定在哪个属性上添加元数据。虽然可以在对象层面添加元数据,但也可以在对象的特定属性上添加。当仅指定对象时,相当于默认将 Property 设为 undefined。
理解上述概念后,前文的示例便一目了然。
安装与使用
安装/使用
接下来介绍如何开始使用 Reflect Metadata。
首先,需要安装 reflect-metadata polyfill,引入后 Reflect 对象下会出现多个关于 Metadata 的函数。由于该规范尚未正式进入协议,因此需要安装垫片(polyfill)才能使用。
Reflect 只是一个全局变量。
无需担心垫片的质量,因为 Angular 等框架也在广泛使用。
随后,即可在 TypeScript 中运行前文提到的示例代码。
类/属性/方法 装饰器与查找规则
类/属性/方法 装饰器
请看以下示例。
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const objs = [A, new A, A.prototype]
const res = objs.map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
// 大家猜测一下 res 的值会是多少?
思考一下,给你 10 秒钟。
10 9 8 7 6 5 4 3 2 1
res
[
['A', undefined, 'A', undefined],
[undefined, 'hello', undefined, undefined],
[undefined, 'hello', undefined, 'hello'],
]
下面解释为何会产生这样的结果。
所有对类的修饰都定义在类对象本身,而所有对类属性或方法的修饰都定义在类的原型上,并以属性或方法的 key 作为 property。这就是 getMetadata 产生上述效果的原因。
那么,带有 'Own' 前缀的函数又是什么情况呢?
这需要从元数据的查找规则讲起。
原型链查找
类似于类的继承,元数据的查找也是通过原型链进行的。
如前例所示,即使实例化了一个 new A(),依然可以找到其原型链上的元数据。
举例说明。
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'
Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'
主要用途
用途
所有用途的核心目的都是给对象添加额外信息,且不改变对象结构。这一点至关重要:添加元数据后,对象不会新增 property,也不会修改现有 property。但由此可以衍生出多种其他用途。
- Angular 中对特殊字段进行修饰(如 Input),从而提升代码可读性。
- 使装饰器具备真正‘装饰’对象而不改变对象的能力,赋予对象更多语义功能。
API 概览
API
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void
// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void
// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
// 删除元数据
deleteMetadata(k, o, p?): boolean
}
注意,部分操作有带 'Own' 的函数,这是因为某些操作可通过原型链进行,后文将详细讲解。
实现原理与源码分析
深入 Reflect Metadata
实现原理
查阅官方文档可知,所有元数据都存在于对象下的 [[Metadata]] 属性中。起初我也认为新建一个 Symbol('Metadata'),并将元数据放入该 Symbol 对应的 Property 中即可。但查看源码后发现并非如此,请看示例:
@Reflect.metadata('name', 'A')
class A {}
Object.getOwnPropertySymbols(A) // []
实际上并不存在所谓的 Symbol,那么这些元数据究竟存储在哪里?
它们存储在内部的一个 WeakMap 中。正是利用了 WeakMap 不增加引用计数的特性,将对象作为 Key,元数据集合作为 Value,存入 WeakMap 中。
若深入探究,会发现其内部数据结构如下:
WeakMap<any, Map<any, Map<any, any>>>
虽然结构看似复杂,但从调用角度思考,逻辑并不复杂。
weakMap.get(o).get(p).get(k)
先根据对象获取,再根据属性,最后根据元数据的 Key 获取最终数据。
End
由于 Reflect Metadata 相对简单,此处不再赘述。更多内容请查阅 Spec。
题外话与总结
题外话
阅读源码后令人惊讶的是,许多 polyfill 要求提供前置 polyfill 后才能使用,但 reflect-metadata 内部自行实现了包括 Map、Set、WeakMap、UUID 在内的多种 polyfill 和算法。最令人惊讶的是 WeakMap 的实现,若不仔细查阅,可能会误以为它仍会增加引用计数。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。