JS `Proxy` / `Reflect` 混淆:劫持对象操作与反检测

各位观众老爷们,大家好!我是今天的主讲人,咱们今天的主题是“JS Proxy / Reflect 混淆:劫持对象操作与反检测”,名字听起来有点唬人,但保证各位听完之后,会觉得“就这?”。

咱们先来聊聊JS里的“代理”和“反射”,这两个家伙,单独拿出来可能你都见过,但是合在一起用,那威力可就大了去了。

一、啥是Proxy?别装作很懂的样子!

Proxy,翻译过来就是“代理”,它的作用就像一个门卫,拦截你对某个对象的访问。你想访问某个对象,得先经过它这一关,它想让你进就让你进,不想让你进就给你踢出去,甚至给你换个对象进去。

这可不是随便说说,我们来举个栗子:

const target = {
  name: '张三',
  age: 30
};

const handler = {
  get: function(target, property, receiver) {
    console.log(`有人要访问我的${property}属性了!`);
    return Reflect.get(target, property, receiver); // 默认行为,返回属性值
  },
  set: function(target, property, value, receiver) {
    console.log(`有人要修改我的${property}属性为${value}了!`);
    Reflect.set(target, property, value, receiver); // 默认行为,设置属性值
    return true; // 表示设置成功
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // 输出: 有人要访问我的name属性了! 张三
proxy.age = 35; // 输出: 有人要修改我的age属性为35了!
console.log(target.age); // 输出: 35

在这个例子里,我们创建了一个target对象,然后用Proxy给它加了个门卫handlerhandler里定义了getset方法,分别拦截了对target对象属性的访问和修改操作。

每次我们访问proxy.name或者修改proxy.age,都会先执行handler里的相应方法,console里会打印出一些信息。

重点:Proxy拦截的是对象的操作,而不是对象本身。 你访问的是proxy,而不是直接访问target

handler里都有三个参数:

  • target: 目标对象,也就是被代理的对象。
  • property: 要访问或修改的属性名。
  • receiver: Proxy或者继承Proxy的对象。 在多数情况下,它等于proxy本身,但如果target对象继承了另一个对象,而访问的是继承来的属性时,receiver可能会有所不同。

handler里还有返回值:

  • get:返回属性值。
  • set:返回一个布尔值,表示设置是否成功。

Proxy能拦截哪些操作?

Proxy能拦截的操作可多了,常见的有:

方法名 拦截的操作
get 读取属性
set 设置属性
has in 操作符
deleteProperty delete 操作符
apply 函数调用
construct new 操作符
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor()
defineProperty Object.defineProperty()
preventExtensions Object.preventExtensions()
getPrototypeOf Object.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf()
ownKeys Object.keys()

二、Reflect:站在上帝视角操作对象

Reflect,翻译过来就是“反射”,它提供了一系列静态方法,用来执行对象的基本操作。这些方法和Proxy的handler里的方法一一对应。

你可以把Reflect看作是站在上帝视角,用更底层的方式来操作对象。

比如,Reflect.get(target, property, receiver)target[property] 的作用是一样的,都是用来读取对象的属性值。但是,Reflect.get 更加规范,而且可以灵活地控制 this 的指向。

再比如,Reflect.set(target, property, value, receiver)target[property] = value 的作用也是一样的,都是用来设置对象的属性值。但是,Reflect.set 会返回一个布尔值,表示设置是否成功。

Reflect有啥用?

Reflect 的主要作用是:

  1. 提供了一套与 Proxy handler 方法一一对应的 API,方便在 Proxy handler 中调用默认行为。 就像我们上面例子里用的 Reflect.getReflect.set
  2. 将一些原本属于 Object 对象的方法,放到 Reflect 对象上,更加合理。 比如 Object.definePropertyObject.getPrototypeOf 等。
  3. 让对象操作更加规范,提供更可靠的返回值。 比如 Reflect.set 会返回一个布尔值,表示设置是否成功,而 target[property] = value 则不会。

三、Proxy + Reflect:完美搭档,天下无敌?

ProxyReflect 结合起来用,那才是真正的强大。

我们可以用 Proxy 拦截对象的操作,然后在 Proxy 的 handler 中,用 Reflect 执行默认行为,还可以根据需要修改默认行为。

举个例子:

const target = {
  name: '李四',
  age: 25,
  _private: '秘密' // 约定以下划线开头的属性是私有的
};

const handler = {
  get: function(target, property, receiver) {
    if (property.startsWith('_')) {
      console.warn('禁止访问私有属性!');
      return undefined; // 阻止访问私有属性
    }
    return Reflect.get(target, property, receiver);
  },
  set: function(target, property, value, receiver) {
    if (property.startsWith('_')) {
      console.warn('禁止修改私有属性!');
      return false; // 阻止修改私有属性
    }
    return Reflect.set(target, property, value, receiver);
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // 输出: 李四
console.log(proxy._private); // 输出: 禁止访问私有属性! undefined
proxy.age = 30;
proxy._private = '新秘密'; // 输出: 禁止修改私有属性!
console.log(target._private); // 输出: 秘密 (target._private没有被修改)

在这个例子里,我们用 Proxy 拦截了对 target 对象属性的访问和修改操作。如果访问或修改的是以下划线开头的私有属性,就阻止访问或修改,否则就用 Reflect 执行默认行为。

四、Proxy 的应用场景:花式玩法,秀翻全场

Proxy 的应用场景非常广泛,只要你想控制对对象的访问,就可以用 Proxy

  1. 数据验证: 在设置属性值之前,验证数据的合法性。

    const validator = {
      set: function(target, property, value) {
        if (property === 'age') {
          if (!Number.isInteger(value)) {
            throw new TypeError('年龄必须是整数!');
          }
          if (value < 0 || value > 150) {
            throw new RangeError('年龄必须在0到150之间!');
          }
        }
        target[property] = value;
        return true;
      }
    };
    
    const person = new Proxy({}, validator);
    
    person.age = 30; // 正常
    // person.age = 'abc'; // 报错:TypeError: 年龄必须是整数!
    // person.age = -10; // 报错:RangeError: 年龄必须在0到150之间!
  2. 数据绑定: 当数据发生变化时,自动更新视图。 (Vue3 就是基于 Proxy 实现的)

    const obj = { name: '张三' };
    const handler = {
      set: function(target, prop, value) {
        target[prop] = value;
        updateView(); // 数据更新后,更新视图
        return true;
      }
    };
    const proxy = new Proxy(obj, handler);
    
    function updateView() {
      console.log('视图已更新!');
    }
    
    proxy.name = '李四'; // 输出: 视图已更新!
  3. 隐藏属性: 阻止访问或修改某些属性。 (前面已经演示过了)

  4. 记录日志: 记录对对象的操作日志。

    const logHandler = {
      get: function(target, property, receiver) {
        console.log(`访问了属性:${property}`);
        return Reflect.get(target, property, receiver);
      },
      set: function(target, property, value, receiver) {
        console.log(`设置了属性:${property},值为:${value}`);
        return Reflect.set(target, property, value, receiver);
      }
    };
    
    const data = { name: '小明', age: 18 };
    const logProxy = new Proxy(data, logHandler);
    
    logProxy.name; // 输出: 访问了属性:name
    logProxy.age = 20; // 输出: 设置了属性:age,值为:20
  5. 实现只读对象:

    const readonlyHandler = {
      set: function(target, property, value) {
        console.warn('该对象是只读的,不允许修改!');
        return false; // 阻止修改
      },
      deleteProperty: function(target, property) {
        console.warn('该对象是只读的,不允许删除属性!');
        return false; // 阻止删除
      }
    };
    
    const original = { name: '只读对象' };
    const readonlyProxy = new Proxy(original, readonlyHandler);
    
    readonlyProxy.name = '尝试修改'; // 输出: 该对象是只读的,不允许修改!
    delete readonlyProxy.name; // 输出: 该对象是只读的,不允许删除属性!

五、Proxy 的反检测:猫鼠游戏,其乐无穷

既然 Proxy 这么强大,那有没有办法检测一个对象是不是 Proxy 呢? 答案是: 有的,但也不完全是。

1. 简单粗暴型:检查是否拥有 [[ProxyHandler]] 内部槽

一些比较底层的 API 可能会暴露对象的内部槽(internal slot),我们可以尝试检查对象是否拥有 [[ProxyHandler]] 内部槽,来判断它是否是 Proxy

但是,这种方法依赖于具体的 JavaScript 引擎实现,不同的引擎可能使用不同的内部槽名称,而且,这种方法很容易被绕过。

2. 行为分析型:观察对象的行为是否符合 Proxy 的特征

我们可以通过观察对象的行为,来判断它是否是 Proxy。比如,我们可以尝试访问或修改对象的属性,看是否会触发 Proxy 的 handler。

function isProxy(obj) {
  let isProxyObj = false;
  try {
    const handler = {
      get: function() {
        isProxyObj = true;
        return undefined;
      }
    };
    const proxy = new Proxy({}, handler);
    Object.getPrototypeOf(proxy); // 触发 getPrototypeOf trap
  } catch (e) {
    // 忽略错误
  }
  return isProxyObj;
}

const target = {};
const proxy = new Proxy(target, {});

console.log(isProxy(target)); // 输出: false
console.log(isProxy(proxy)); // 输出: true

这种方法也不是万无一失的,因为 Proxy 的 handler 可以被配置成不触发任何行为,或者触发一些和普通对象一样的行为。

3. 利用 toStringTag

Symbol.toStringTag 是一个内置的 symbol,可以用来修改 Object.prototype.toString.call() 方法的返回值。 我们可以通过修改 Proxy 对象的 Symbol.toStringTag 属性,来欺骗检测代码。

const target = {};
const proxy = new Proxy(target, {});
Object.defineProperty(proxy, Symbol.toStringTag, {
  value: 'Object' // 伪装成普通对象
});

console.log(Object.prototype.toString.call(proxy)); // 输出: [object Object]

4. 间接检测:

如果无法直接检测对象是否是 Proxy,可以尝试检测对象的某些行为是否符合预期。例如,如果一个对象声称自己是数组,但却无法通过 Array.isArray() 检测,那它很可能是一个被 Proxy 修改过的对象。

反检测的思路:

  1. 隐藏 Proxy 的特征: 尽量让 Proxy 对象的行为和普通对象一样,避免触发 Proxy 的 handler。
  2. 修改 Proxy 的行为: 修改 Proxy 的 handler,让它返回一些和普通对象一样的结果。
  3. 伪装成普通对象: 修改 Proxy 对象的 Symbol.toStringTag 属性,让它看起来像一个普通对象。

总结:

ProxyReflect 是一对强大的搭档,可以用来劫持对象的操作,实现各种各样的功能。但是,Proxy 也有一些缺点,比如性能开销比较大,而且容易被检测。

在实际开发中,我们需要根据具体的场景,权衡利弊,选择合适的方案。

最后,希望各位观众老爷们,以后在使用 Proxy 的时候,能够更加得心应手,玩转 Proxy,秀翻全场!

今天的讲座就到这里,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注