JS的代理与反射

2 阅读6分钟

一、Java 和 JavaScript 中的代理(Proxy)和反射(Reflection)有一些相似之处,但也有一些重要的异同点。

相似点:

  1. 代理(Proxy) :在 Java 和 JavaScript 中,代理都是一种机制,允许你在访问对象之前或之后执行一些额外的逻辑。代理模式可以用于实现拦截、修改、验证等功能。
  2. 反射(Reflection) :在 Java 和 JavaScript 中,反射都是一种机制,允许程序在运行时检查对象、类、方法等的信息,以及在运行时操作对象、类、方法等。这种能力使得程序能够动态地创建对象、调用方法、访问属性等。

异同点:

  1. 语言类型
  • Java 是一种静态类型语言,反射机制在 Java 中是由类 java.lang.reflect 包提供支持的。
  • JavaScript 是一种动态类型语言,其反射机制是更为灵活的,并且内置在语言本身中,比如通过 Reflect 对象。
  1. 语法和用法
  • Java 中的代理和反射需要通过特定的语法和 API 来实现,比如使用 java.lang.reflect.Proxy 类创建代理对象,使用 java.lang.reflect 包中的类来获取类信息、方法信息等。
  • JavaScript 中的代理和反射则更为简单和直接,代理可以使用内置的 Proxy 对象来创建,而反射可以使用内置的 Reflect 对象来获取和操作对象的信息。
  1. 权限和安全性
  • 在 Java 中,反射可能会涉及到对私有成员的访问,因此可能需要额外的权限配置。
  • 在 JavaScript 中,代理和反射相对更加灵活,但也可能因此带来一些安全风险,比如可能会破坏对象的封装性。
  1. 性能
  • 由于 Java 是一种静态类型语言,其反射机制可能会带来一些性能开销,比如方法调用、类型转换等。
  • JavaScript 中的代理和反射通常更为轻量级,因为 JavaScript 是一种动态类型语言,运行时信息更加灵活。

二、js中常见的代理

// 创建一个目标对象
const target = {
  name: "Alice",
  age: 30
};
​
// 创建一个代理对象
const handler = {
  get(target, prop, receiver) {
    // 在获取目标对象的属性时,添加一些额外的逻辑
    console.log("Getting property:", prop);
    // 返回目标对象的属性值
    return target[prop];
  }
};
​
// 使用 Proxy 对象创建代理
const proxy = new Proxy(target, handler);
​
// 通过代理对象访问目标对象的属性
console.log(proxy.name); // 输出: Getting property: name  Alice
console.log(proxy.age);  // 输出: Getting property: age   30

java中的代理(动态代理)示例

import java.lang.reflect.*;
​
// 定义一个接口
interface Subject {
    void request();
}
// 创建一个目标类实现接口
class RealSubject implements Subject {
    public void request() {
        System.out.println("Real Subject: Handling request.");
    }
}
// 创建一个代理类实现接口
class ProxyHandler implements InvocationHandler {
    private Subject realSubject;
    public ProxyHandler(Subject realSubject) {
        this.realSubject = realSubject;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Proxy: Preprocessing.");
        Object result = method.invoke(realSubject, args);
        System.out.println("Proxy: Postprocessing.");
        return result;
    }
}
// 使用动态代理
public class Main {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        InvocationHandler handler = new ProxyHandler(realSubject);
        Subject proxy = (Subject) Proxy.newProxyInstance(
            realSubject.getClass().getClassLoader(),
            realSubject.getClass().getInterfaces(),
            handler);
        proxy.request();
    }
}

三、js代理的一些注意点

3.1、Proxy.prototype是undefined,不能使用instanceof。
3.2、捕获器不变式:防止捕获器定义出现过于反常的行为。比如一个数据属性是不可配置和不可写的属性,那么捕获器在返回时,是不能修改这个属性值的,否则抛TypeError。
3.3、代理是支持撤销的,前提是在创建代理时,使用如下方法:
// 创建一个目标对象
const target = {
  name: "Alice",
  age: 30
};
// 创建一个可撤销的代理对象
const { proxy, revoke } = Proxy.revocable(target, {
  get(target, prop) {
    console.log("Getting property:", prop);
    return target[prop];
  }
});
// 访问代理对象的属性
console.log(proxy.name); // 输出: Getting property: name  Alice
console.log(proxy.age);  // 输出: Getting property: age   30// 撤销代理对象
revoke();
// 再次访问代理对象的属性将会抛出错误
try {
  console.log(proxy.name);
} catch (error) {
  console.error("Proxy has been revoked.");
}
3.4、反射的使用场景

1、反射api并不仅仅用于捕获处理程序,大多数反射的api在Object类型是有对应的方法,用于更细粒度的对象控制与操作。

2、状态标记,比如定义一个属性或者设置属性,至于是否成功,可以用反射api来重构,比try catch要好。

        const o = {};
        try {
            Object.defineProperties(o, 'foo', 'bar');
            console.log('success');
        } catch (e) {
            console.log('failure');
        }
        // 使用Reflect.defineProperty可以获取到状态标记是否定义成功
        if(Reflect.defineProperty(o,'foo',{value:'bar'})){
            console.log("success");
        }else{
            console.log("failure");
        }

3、用一等函数替代操作符。

下面是一些常见的操作符和相应的 Reflect API 方法的对应关系:
赋值操作符 =:
操作符:obj[prop] = value
Reflect APIReflect.set(obj, prop, value)
​
属性访问操作符 . 或 []:
操作符:obj[prop] 或 obj.prop
Reflect APIReflect.get(obj, prop)
​
删除操作符 delete:
操作符:delete obj[prop]
Reflect APIReflect.deleteProperty(obj, prop)
​
属性检测操作符 in:
操作符:prop in obj
Reflect APIReflect.has(obj, prop)
​
原型访问操作符 instanceof:
操作符:obj instanceof Constructor
Reflect APIReflect instanceof obj, Constructor
​
函数调用操作符 ():
操作符:func(...)
Reflect APIReflect.apply(func, thisArg, args)
​
构造函数操作符 new:
操作符:new Constructor(...)
Reflect APIReflect.construct(Constructor, args)
​
类型转换操作符 typeof:
操作符:typeof obj
Reflect APIReflect.getPrototypeOf(obj)

4、安全的应用函数。

这里是说通过apply调用函数时,被调函数也重写了apply属性(可能性小),可以用以下方式解决安全问题。(函数也是对象,也可以被代理的)

      Function.prototype.apply.call(myFunc,thisVal,argumentList);
      Reflect.apply(myFunc,thisVal,argumentList);
3.5、代理嵌套也是可以的
        const o = {
            foo: 'bar'
        };
        const firstProxy = new Proxy(o, {
            get(targe, value, receiver) {
                console.log("first")
                return Reflect.get(...arguments);
            }
        })
        const secondProxy = new Proxy(firstProxy, {
            get(targe, value, receiver) {
                console.log("second")
                return Reflect.get(...arguments);
            }
        })
        console.log(secondProxy.foo);
3.6、代理的问题与不足
代理最大的问题是来源this。目前Date类型代理有问题。
      let vm = new WeakMap();
        class User {
            constructor(userid) {
                vm.set(this, userid);
            }
            set id(userid) {
                vm.set(this, userid);
            }
            get id() {
                return vm.get(this);
            }
        }
        const user = new User(123);
        console.log(user.id);
        const userProxy = new Proxy(user, {});
        console.log(userProxy.id);// undefined;

因为代理里面this是userProxy,而vm种的this是user,导致取不到id值。

解决以上问题就是使用User取代user。
     let vm = new WeakMap();
        class User {
            constructor(userid) {
                vm.set(this, userid);
            }
            set id(userid) {
                vm.set(this, userid);
            }
            get id() {
                return vm.get(this);
            }
        }
        const UserClassProxy = new Proxy(User, {});
        const proxyUser = new UserClassProxy (456);
        console.log(proxyUser.id);// 456

UserClassProxy 是一个代理对象,代理了 User 类。尽管 UserClassProxy 是一个代理对象,但是你仍然可以使用 new 关键字来实例化它,这样会触发代理对象的 construct 捕获器,从而模拟了目标对象的构造函数行为。

四、代理捕获器与反射方法

get/set/has() in操作符/defineProperty/getOwnPropertyDescriptor/deleteProperty/

ownKeys/getPrototypeof/setPrototypeof/isExtensible/preventExtensions/apply/construct/

明白一点:new是操作符,会触发constrct方法调用,代理可以传入类而不一定是类对象。apply传入的是Function对象。

五、代理模式常用的编程模式

跟踪属性访问、隐藏属性、属性验证、函数与构造函数参数验证、数据绑定与可观察对象。

// 函数参数验证代理
function createProxyFunc(func) {
  return new Proxy(func, {
    apply: function(target, thisArg, args) {
      if (!args.every(arg => typeof arg === 'number')) {
        throw new Error('参数必须为数字');
      }
      return Reflect.apply(target, thisArg, args);
    }
  });
}
const add = createProxyFunc(function(a, b) {
  return a + b;
});
console.log(add(2, 3)); // 输出: 5
//console.log(add('2', 3)); // 抛出错误
// 构造函数参数验证代理
function createProxyConstructor(Constructor) {
  return new Proxy(Constructor, {
    construct: function(target, args) {
      const [name, age] = args;
      if (typeof name !== 'string' || typeof age !== 'number') {
        throw new Error('参数类型不正确');
      }
      return Reflect.construct(target, args);
    }
  });
}
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  getInfo() {
    return `姓名:${this.name},年龄:${this.age}`;
  }
}
const ProxyUser = createProxyConstructor(User);
const user1 = new ProxyUser('Alice', 25);
console.log(user1.getInfo()); // 输出: 姓名:Alice,年龄:25
//const user2 = new ProxyUser(25, 'Alice'); // 抛出错误
     const userList = [];
     class User{
        constructor(id){
            this.id = id;
        }
     }
     const proxy = new Proxy(User,{
        construct(id){
            const newUser = Reflect.construct(...arguments);
            userList.push(newUser);
            return newUser;
        }
     })
     new proxy("11"); // new操作符会调用proxy的construct方法,Reflect.construct会调用User的构造。
     new proxy("xxx");
     console.log(userList);