JS的反射学习和应用

发表于 2017-06-22
更新于 2024-05-23
分类于 技术专栏
阅读量 12553
字数统计 8756

前言

今天我们要聊的s是一个比较生僻的概念-反射,在JS中至少我之前没听过,直到在后来的一个项目中看到TL写的代码才知道还有这么一个概念。可能Pyhton的童鞋会反驳,因为这个概念在他们的语言中是经常被使用的,无奈偶是C语言的。。。。

在国内的技术文章中你去搜索"JS 反射"得到的大部分的内容都是在说“利用JS的for(…in…)语句实现反射机制”,但其实反射机制在如今的ES6中可以得到更大的延伸以及运用的,这个在后续会讲解。不过这些文章都用一句比较通俗的话来说什么叫反射机制:

反射机制指的是程序在运行时能够获取自身的信息

明白这句话对后面的应用会受益很多。

1 JS的反射对象

反射机制我们在前言中提过了,那么在ES6中JS提供了一个叫做Reflect的对象。

MDN上的反射对象是这样定义的:

Reflect是一个内建的对象,用来提供方法去拦截JavaScript的操作。Reflect不是一个函数对象,所以它是不可构造的,也就是说它不是一个构造器,你不能通过new操作符去新建或者将其作为一个函数去调用Reflect对象。Reflect的所有属性和方法都是静态的。

可是MDN并没有说明为什么需要这么一个对象?

1.1 为什么需要Reflect对象

因为我们刚才说过了,利用JS的for..in可以实现,还有比如Array.isArray/Object.getOwnPropertyDescriptor,或者甚至Object.keys都是可以归类到反射这一类中。那么ECMA为什么还要这个呢?

当然是为了让JS更加强大了,相当于说提供Reflect对象将这些能够实现反射机制的方法都归结于一个地方并且做了简化,保持JS的简单。于是我们再也不需要调用Object对象,然后写上很多的代码。

比如:

1var myObject = Object.create(null) // 此时myObject并没有继承Object这个原型的任何方法,因此有: 2 3myObject.hasOwnProperty === undefined // 此时myObject是没有hasOwnProperty这个方法,那么我们要如何使用呢?如下: 4 5Object.prototype.hasOwnProperty.call(myObject, 'foo') // 是不是很恐怖,写这么一大串的代码!!!! 6

而如果使用Reflect对象呢?

1var myObject = Object.create(null) 2Reflect.ownKeys(myObject)

再比如当你对象里有Symbol的时候,如何遍历对象的keys?

1var s = Symbol('foo'); 2var k = 'bar'; 3var o = { [s]: 1, [k]: 1 }; 4// getOwnPropertyNames获取到String类型的key,getOwnPropertySymbols获取到Symbol类型的key 5var keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));

而使用Reflect的话:

1var s = Symbol('foo'); 2var k = 'bar'; 3var o = { [s]: 1, [k]: 1 }; 4Reflect.ownKeys(o)

相比较之下,Reflect对象的作用凸显出来了吧?

另外Reflect还提供了一些Object对象没有的方法,比如Reflect.apply

1.2 Reflect方法

除了上面提到的ownKeys的方法(并不会去获取那些继承的key)之外,该对象还提供了以下方法:

1.2.1 Reflect.apply(target, thisArgument [, argumentsList])

该方法类同于Function.prototype.apply(),二者的对比如下:

1var ages = [11, 33, 12, 54, 18, 96]; 2 3// Function.prototype style: 4var youngest = Math.min.apply(Math, ages); 5var oldest = Math.max.apply(Math, ages); 6var type = Object.prototype.toString.call(youngest); 7 8// Reflect style: 9var youngest = Reflect.apply(Math.min, Math, ages); 10var oldest = Reflect.apply(Math.max, Math, ages); 11var type = Reflect.apply(Object.prototype.toString, youngest);

上面的Math.min.apply可以参考这篇文章:call&apply&bind的学习

Reflect提供这个方法的最大好处可以避免别人也写了一个同名的apply函数的时候,我们不会需要去写一大长串的代码,比如:

Function.prototype.apply.call(context, ...args)/Function.apply.call(context, ...args)

而是依然是简单的:

Reflect.apply()

1.2.2 Reflect.construct(target, argumentsList [, constructorToCreateThis])

这个方法等价于调用new target(...args)

二者的对比实现如下:

1class Greeting { 2 constructor(name) { 3 this.name = name; 4 } 5 greet() { 6 return `Hello ${name}`; 7 } 8 9} 10 11// ES5 style factory: 12function greetingFactory(name) { 13 var instance = Object.create(Greeting.prototype); 14 Greeting.call(instance, name); 15 return instance; 16} 17 18// ES6 style factory 19function greetingFactory(name) { 20 return Reflect.construct(Greeting, [name], Greeting); 21} 22 23// Or, omit the third argument, and it will default to the first argument. 24function greetingFactory(name) { 25 return Reflect.construct(Greeting, [name]); 26} 27 28// Super slick ES6 one liner factory function! 29const greetingFactory = (name) => Reflect.construct(Greeting, [name]);

1.2.3 Reflect.defineProperty ( target, propertyKey, attributes )

类同于Object.defineProperty(),不同的是该方法返回的是布尔值,而不需要你像以前那样去捕捉异常(因为Object.defineProperty是在执行出错的时候直接抛错的)

1.2.4 Reflect.getOwnPropertyDescriptor ( target, propertyKey )

类同于Object.getOwnPropertyDescriptor(),如果对应的属性存在则返回给定属性的属性描述符,否则返回未定义。比如:

1var myObject = {}; 2Object.defineProperty(myObject, 'hidden', { 3 value: true, 4 enumerable: false, 5}); 6var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden'); 7Reflect.getOwnPropertyDescriptor(1, 'foo')

1.2.5 Reflect.deleteProperty ( target, propertyKey )

等同于调用delete target[name]

1.2.6 Reflect.getPrototypeOf ( target )

等同于Object.getPrototypeOf(),唯一不同的是当传参target不是一个对象的时候:前者会强制将target转为一个对象。

1// Number {constructor: function, toExponential: function, toFixed: function, toPrecision: function, 2// toString: function…} 3Object.getPrototypeOf(1); 4 5// Uncaught TypeError: Reflect.getPrototypeOf called on non-object 6// at Object.getPrototypeOf (<anonymous>) 7// at <anonymous>:1:9 8Reflect.getPrototypeOf(1); // TypeError

1.2.7 Reflect.setPrototypeOf ( target, proto )

等同于Object.setPrototypeOf,差别的地方和刚才的getPrototypeOf是一样的。如果传参没有错,那么Reflect是直接返回布尔值来标识是否成功,而后者则直接抛错来表明失败。

1.2.8 Reflect.isExtensible (target)

等同于Object.isExtensible(),区别也是在于返回值。

1.2.9 Reflect.preventExtensions ( target )

类同于Object.preventExtensions()。区别也是在于返回值。

1.2.10 Reflect.get ( target, propertyKey [ , receiver ])

该方法用来获取对象中某个属性的方法。这是一个全新的方法,不过该方法也是很简单的,如下:

1const testObject = { 2 a: 'you', 3 b: 'like' 4} 5Reflect.get(testObject, 'a') === 'you' // true 6Reflect.get(testObject, 'b') === 'like' // true

1.2.11 Reflect.set ( target, propertyKey, V [ , receiver ] )

类同于上面的get方法。比如:

1const testObject = { 2 a: 'you', 3 b: 'like' 4} 5Reflect.set(testObject, 'c', 'javascript') // true 6Reflect.get(testObject, 'c') === 'javascript' // true

1.2.12 Reflect.has ( target, propertyKey )

该方法类似于in操作符,返回布尔值来表明该属性是否存在该对象上或者其原型链上。比如:

1let testObject = { 2 foo: 1, 3}; 4Object.setPrototypeOf(testObject, { 5 get bar() { 6 return 2; 7 }, 8 baz: 3, 9}); 10 11Reflect.has(myObject, 'foo') === true 12Reflect.has(myObject, 'baz') === true

1.2.13 Reflect.ownKeys ( target )

该方法在之前说过了,它返回了目标对象已有的所有属性(不包括原型链)的一个数组。

2 JS反射机制在Nodejs中的运用

说了反射机制和反射对象这么多,我们会如何应用反射机制?

假设有这么一个场景:你需要在一个从一个类中获取额外的元信息,或者希望给类中的一些方法加入注解(Annotation),然后在实时的运行中获取到对应的元数据信息,那么这种场合就适合使用反射机制(配合修饰器的使用),下面我们介绍关于这方面应用的一个开源包---reflect-metadata,以及对应的一个小demo js-reflect-demo

按照作者的思路,命名为元数据反射API是因为他的目的是:

  1. 绝大部分的场景(组合/依赖注入,实时类型断言,反射/镜像,测试)都会希望能够添加额外的元数据到一个类中。
  2. 各种工具和库都需要一种一致的方法来推理元数据
  3. 元数据修饰器(也就是注解)通常需要和变化的修饰器组合
  4. 元数据不仅仅在一个对象上生效,而且在Proxy以及对应的traps上也应该有效
  5. 定义新的元数据修饰器对于开发者来说不应该过于复杂和麻烦
  6. 元数据应该与ECMAScript的运行时特性以及其他语言一致

更多关于该软件的说明参考: README.md

使用该软件时注意只需要Require一次该包,然后就可以在之前说的Reflect对象上使用这些元数据操作方法。具体代码如下:

1// patch global Reflect 2(function (__global) { 3 if (typeof __global.Reflect !== "undefined") { 4 if (__global.Reflect !== Reflect) { 5 for (var p in Reflect) { 6 if (hasOwn.call(Reflect, p)) { 7 __global.Reflect[p] = Reflect[p]; 8 } 9 } 10 } 11 } 12 else { 13 __global.Reflect = Reflect; 14 } 15})(typeof global !== "undefined" ? global : 16 typeof self !== "undefined" ? self : 17 Function("return this;")());

可以看到该包将所有的方法挂载到了全局的Reflect对象上了。

在demo中我们在reflect.js中使用修饰器往类中定义一个元数据:attr,然后在内部的方法中注解对应的方法参数的更多基本信息,于是在index.js中,我们便可以通过Reflect-metadata提供的API去获取这些元数据。

当然这只是一个简单的demo,但也许可以给你提供一些思路在解决JS注解以及元数据注入这方面的灵感。

作者的Reflect.ts写的很优雅,建议有时间的童鞋可以看看。

目前我们基于这套思路实现了一个比较好的前后端分离的方案,后续有时间可以分享给大家。

参考

感谢Keith Cirkel提供了很多有用的代码,上面的一些demo都是参考借鉴于文章4

  1. reflect-metadata
  2. Ecma-262
  3. MDN
  4. Metaprogramming in ES6: Part 2 - Reflect
  5. ES6 Reflection in Depth

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 JS的反射学习和应用 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github