ES6:玩转Reflect

584 阅读3分钟

Reflect是一个内置对象,它提供可拦截JavaScript操作的方法。这些方法与Proxy处理的方法相同。 Reflect不是函数对象,因此它是不可构造的。

背景及其基本使用

Reflect解决的问题:

  1. 提供函数化的方法,用于取代Object相关的操作符,如deleteinstanceof等。
var obj = { foo: "property" };
// 删除obj的foo的属性
// ES5
delete obj.foo

// ES6
Reflect.deleteProperty(target, propertyKey);
  1. 提供更值得信赖的Function API

ES5中,如何我们想要调用某个function F,同时传入一个可变参数打包成的数组,并且将this的值绑定到某个object上,我们只能这样写:

F.apply(obj, args)

如果这个F是一个object并且有意或无意间定义了自己的apply方法,为了确保apply函数真正被调用,我们可能需要这样写:

// 手动🐶:不管你看不看得懂,反正我看不懂
Function.prototype.apply.call(f, obj, args)

而在ES6中,我们只需要这样调用:

Reflect.apply(f, obj, args)
  1. 提供更友好的返回值
// ES5 
try {
  Object.defineProperty(obj, name, desc);
  // 属性定义成功
} catch (e) {
  // 意外定义失败,捕获错误
}

// ES6 
if (Reflect.defineProperty(obj, name, desc)) {
  // 定义成功
} else {
  // 定义失败
}
  1. 提供原生的代理方法,与Proxy联动。

当我们使用Proxy对某个对象的行为进行拦截时,通常做一些额外逻辑,然后执行默认行为。

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    // 打印对象及其属性
    console.log("get", target, name);
    // 执行默认行为
    return target[name];
  }
});

事实上,ProxyReflect被设计用于联动,每一个Proxy拦截的行为都对应着一个Reflect API来执行默认行为:

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  }
});

Reflect实战

在了解完Reflect的基本特点之后,让我们深入了解一下,在实际开发过程中,如何使用Reflect拦截业务代码调用,延伸出额外的逻辑如:埋点、日志记录、参数预处理等,这里我们使用TypescriptDecorator,通过装饰对象方法来实现。

构建装饰器

export function makeClassMemberDecorator(decorator) {
  return function (target, name, descriptor) {
    const { configurable, enumerable, value, get, initializer } = descriptor;

    // 使用Reflect拦截被装饰方法
    ...
    // 使用decorator添加额外的逻辑
    ...
    // 返回descriptor
    return {
        configurable,
        enumerable,
        value: decorator(value),  // 被装饰的属性/方法
    }
  };
}

拦截方法被装饰的方法

const { configurable, enumerable, value, get, initializer } = descriptor;
// 保留原有的configurable和enumerable
// 普通方法直接装饰
    
    return {
        configurable,
        enumerable,
        get() {
            // 初始化修饰的方法
            const resolvedValue = initializer
                ? Reflect.apply(initializer, this, [])
                : Reflect.apply(get, this, []);
            
            // 装饰方法
            const decoratedValue = Reflect.apply(decorator, this, [resolvedValue]);

          // 覆盖原有方法
          Reflect.defineProperty(this, name, {
            configurable,
            enumerable,
            value: decoratedValue,
          });

          return decoratedValue;
        },
    };

decoratedFunc中添加额外的逻辑

// 创建decorator
const decorator = function(params: any) {
    return  makeClassMemberDecorator((decoratedFunc: Function) => function(...args: any[]) {
    
    // 编写额外的执行逻辑
    const extraFunc = async (...args: any[]) => {
        // 日志上报
        // await logApi.create(args);
        
        // 参数校验
        // const [validation, ...params] = args;
        // cosnt isValid = validation(params);
        // if (!isValid) throw Error("Parameters Invalid!");
    };

    // 调用原函数(后置调用)
    const fn = Reflect.apply(decoratedFunc, this, args);
        // 如果返回Promise
        if (Promise && Promise.resolve(fn) === fn) {
        // 定义额外执行的函数所需的参数
        const packedArgs = [...args, ...params];
          return fn
            .then(() => Reflect.apply(extraFunc, this, packedArgs))
            .then(() => fn)
            .catch((error: Error) => {
              // 无论如何extraFunc都会执行,且对被装饰的函数是透明的
              extraFunc({}, error);
              throw error;
            });
        }
        // 直接调用
        extraFunc();
        // 返回被装饰的方法
        return fn;
    })
};

装饰器使用

React组件中

class MyComponet extends React.Component<IState, IProps> {
    
    @Decorator((props: IProps, state: IState, args: any[], returnArgs: any) => {
        // 处理额外逻辑,不干扰业务逻辑
    })
    private handleClick = (evt: any) => {
        // 业务逻辑
    }
    
    render() {
        return <Button onClick={handleSubmit}>Submit</Button>
    }
}

Node.js应用中,配置Joi对参数进行校验,同时上传日志.

class Controller {
    // 日志上传
    @reportor(async (ctx: App.Context, params: any[]) => {
        // 上传日志
    })
    // 参数校验
    @validate({
        Joi.object({
            username: Joi.string().alphanum().min(3).max(30).required(),
            password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
            repeat_password: Joi.ref('password'),
            access_token: [
                Joi.string(),
                Joi.number()
            ],
            birth_year: Joi.number().integer().min(1900).max(2013),
            email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
        }).with('username', 'birth_year')
    })
    privare myApi(ctx: App.Context) {
        // 校验逻辑
    }
}

总结

Reflect提供了很多直接操作javascript内部属性的途径,可以帮助我们更灵活的来编写代码。希望这篇文章能给你们提供一些灵感。

参考资料