我可以用ES2015类来扩展代理吗?

57
我尝试扩展Proxy,像这样:
class ObservableObject extends Proxy {}

我使用Babel将它转译为ES5,然后在浏览器中出现了这个错误:
app.js:15 Uncaught TypeError: Object prototype may only be an Object or null: undefined
我查看了它所指向的代码行。以下是代码的一部分,箭头指向有问题的代码行:
var ObservableObject = exports.ObservableObject = function (_Proxy) {
    _inherits(ObservableObject, _Proxy); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    function ObservableObject() {
        _classCallCheck(this, ObservableObject);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(ObservableObject).apply(this, arguments));
    }

    return ObservableObject;
}(Proxy);

有人知道为什么我会收到这个错误吗?这是 Babel 的一个 bug 吗?当你尝试扩展 Proxy 时应该发生什么?

1
在Firefox中,这会抛出一个“undefined is not an object or null”和有时是“ReferenceError: can't access lexical declaration [class name] before initialization”。 - D. Pardal
有趣的是,在 MDN 的 get-trap 处理程序的描述中,参数 receiver 被描述为“要么是代理,要么是继承自代理的对象”。这听起来好像 Proxy 应该是可扩展的。我记得在规范中看到过类似的评论,但现在找不到了。这似乎与观察结果相矛盾... - Sebastian
7个回答

107

嗯,我已经忘记了这个问题,但最近有人给它点了赞。虽然从技术上讲你不能扩展一个代理对象,但是有一种方法可以强制一个类被实例化为代理,并强制所有它的子类使用相同的属性描述函数来实例化为代理(我只在Chrome中测试过):

class ExtendableProxy {
    constructor() {
        return new Proxy(this, {
            set: (object, key, value, proxy) => {
                object[key] = value;
                console.log('PROXY SET');
                return true;
            }
        });
    }
}

class ChildProxyClass extends ExtendableProxy {}

let myProxy = new ChildProxyClass();

// Should set myProxy.a to 3 and print 'PROXY SET' to the console:
myProxy.a = 3;

6
根据ECMAScript 6/7规范(我忘了具体是哪一个版本),在类层次结构中,基类负责分配对象。因此,在一个层次结构中,那个没有继承除Object以外其他类的类将负责分配。在这种情况下,ExtendableProxy将新实例分配为Proxy的实例。所以这样做是有效的。我认为这不是一个bug。 - Ethan Reesor
1
@WimBarelds 这是完全有意的,事实上这里的答案是其中一个重要原因,基本上允许任意对象,使您可以做一些像在缓存中保存对象、创建Proxy对象来拦截东西,甚至像子类工厂等等的事情。 - Jamesernator
4
纯JS中这个代码可以正常工作,但TypeScript不支持它,并且有一些很好的理由:https://github.com/Microsoft/TypeScript/issues/11588#issuecomment-257437554。在TypeScript中,你应该使用函数而不是es6类来返回新的Proxy并保持正确的返回类型声明。 - Nurbol Alpysbayev
4
2014年,JavaScript 创造者 Brendan Eich 明确确认从构造函数中返回一个值并非错误:“我们在 TC39 会议上讨论了在构造函数中返回表达式的问题,我的感觉是我们想保留这个选项。类只是一种语法糖(大多数情况下),就是这样。” 然而,TC39 的一些成员(负责维护 JavaScript)认为这种技术是反模式。截至2019年,@John L 的 ExtendableProxy 示例可行且是创建代理子类的有效方法。(但如果目标是观察键/值存储的更改,则扩展 Map 可能会引起较少争议。) - colin moock
2
请将 Brendan Eich 源代码的链接转换为 https://esdiscuss.org/topic/should-the-default-constructor-return-the-return-value-of-super 的格式。 - colin moock
显示剩余6条评论

40

不,ES2015类不能继承Proxy1

Proxy对象具有非典型的语义,并被认为是ES2015中的“异类对象”,这意味着它们“不具备所有对象必须支持的一个或多个基本内部方法的默认行为”。它们没有任何原型,这通常是您扩展类型时获取大多数行为的位置。从规范中的“Proxy构造函数的属性”第26.2.2节中可以看到:

Proxy构造函数没有prototype属性,因为代理异类对象没有需要初始化的[[Prototype]]内部槽。

这不是Babel的限制。如果您尝试在Chrome中扩展Proxy,在那里它和类语法都是本地支持的,您仍将获得类似的错误:

Uncaught TypeError:Class extends value does not have valid prototype property undefined

1 “不行”是实际的答案。然而,Alexander O'Mara 指出,如果你给 Proxy.prototype 赋值(很糟糕!),它就变得可能扩展,至少在一些浏览器中。我们做了一些实验。由于奇异代理实例的行为,这不能用于完成比使用函数包装构造函数更多的事情,并且某些行为在不同的浏览器之间似乎并不一致(我不确定规范期望你做什么)。请不要在严肃的代码中尝试类似的事情。


顺便提一下,在原生的、非 babel 实现中,通过补丁 prototype 属性可以绕过这个 TypeError 错误,但是 Proxy 的实现方式似乎会覆盖任何子类,让你只能得到 Proxy 的一个复杂别名。 - Alexander O'Mara
我对子类化其他非this返回构造函数的行为不太确定,但是一个快速测试验证了它与我观察到的代理子类的行为相匹配。(编辑:啊,你在我测试时也评论了这一点。)所以我认为你确实成功创建了一个代理子类,只是这可能不是一个有用的事情,因为你能做的只是修改构造函数,而你可以使用包装函数来实现同样的效果。 - Jeremy
有趣的是,最后一个例子在V8(Chrome)和Spidermonkey(Firefox)之间的行为不同。 - Alexander O'Mara
让我们在聊天中继续这个讨论 - Jeremy
@JeremyBanks,这并不是“恶心的”。子类代理是捕获不支持noSuchMethod的浏览器属性访问的唯一方法。https://dev59.com/PXE85IYBdhLWcg3wqleE - Pacerier
显示剩余8条评论

16

来自@John L.的自我回应:
在构造函数中,我们可以使用Proxy来包装新创建的实例。无需扩展Proxy。

例如,从现有的Point类提供一个被观察的点:

class Point {

    constructor(x, y) {
        this.x = x
        this.y = y
    }

    get length() {
        let { x, y } = this
        return Math.sqrt(x * x + y * y)
    }

}

class ObservedPoint extends Point {

    constructor(x, y) {

        super(x, y)

        return new Proxy(this, {
            set(object, key, value, proxy) {
                if (object[key] === value)
                    return
                console.log('Point is modified')
                object[key] = value
            }
        })
    }
}

测试:

p = new ObservedPoint(3, 4)

console.log(p instanceof Point) // true
console.log(p instanceof ObservedPoint) // true

console.log(p.length) // 5

p.x = 10 // "Point is modified"

console.log(p.length) // 5

p.x = 10 // nothing (skip)

这个很好,在网页上可以工作,但在Node.js上不行。super不能给代理正确的属性,也不能完成任务(只是Node)。 - sdykae

4
class c1 {
    constructor(){
        this.__proto__  = new Proxy({}, {
            set: function (t, k, v) {
                t[k] = v;
                console.log(t, k, v);
            }
        });
    }
}

d = new c1(); d.a = 123;


2
应该使用Object.setPrototypeOf而不是this.__proto__吗? - John L.
这对我来说非常完美!谢谢! - synthet1c

0
Babel不支持Proxy,因为它无法支持。所以在浏览器添加支持之前,它不存在。
从Babel文档中: “不支持的功能 由于ES5的限制,代理无法进行转译或填充”。

你可以接近实现... https://github.com/GoogleChrome/proxy-polyfill - marksyzm
唯一需要注意的是:“这个 polyfill 只支持有限数量的代理 'traps'。它还通过在传递给 Proxy 的对象上调用 seal 来工作。这意味着你想要代理的属性必须在创建时就已知。” - marksyzm

0
class A { }

class MyProxy {
  constructor(value, handler){
    this.__proto__.__proto__  = new Proxy(value, handler);
  }
}


let p = new MyProxy(new A(), {
  set: (target, prop, value) => {
    target[prop] = value;
    return true;
  },
  get: (target, prop) => {
    return target[prop];
  }
});

console.log("p instanceof MyProxy", p instanceof MyProxy); // true
console.log("p instanceof A", p instanceof A); // true

p是一种MyProxy,同时它被类A扩展。 A不是原始的原型,它被代理了,有点像。


在 Firefox 中,这仅适用于创建的第一个实例。之后,我会收到错误 无法设置此对象的原型 - seanlinsley

0
下面的代码示例展示了如何构建一个代理层次结构,模拟类和子类。它通过使用多个代理对象包装标准对象并巧妙地使用处理程序 get 选项来实现这一点。
代理的处理程序是混入模式的一种形式。我们可以使用混入模式来模拟子类化。
代码包含两种类型的代理“类”:EmitterBase 和 EmitterNet。它们用于收集特定于 vanilla EventEmitter 或其子类 Net 的统计信息。EmitterNet 不会复制 EmitterBase 的功能,而是通过包装 EmitterBase 来重用它。请注意,在示例中,我们包装了 http.Server:Net 的子类和 EventEmitter 的子子类。
传递给代理的处理程序实现了子类代理的所有行为。多个处理程序版本实现子类。例如,在EmitterBase中,我们收集对onemit的调用统计信息(计算调用次数)。EmitterBase还实现幻影成员和方法来跟踪和报告这些计数。这相当于具有这些方法和成员的基类。请参见wrapEmitterBase中的handler

接下来,我们使用另一个处理程序(请参见wrapEmitterNet)创建代理“子类”,该处理程序实现了两个新的幻影成员来计算Net特定调用(listenclose)。它还实现了一个stats()方法,该方法覆盖了来自基类的方法以及调用被覆盖的方法。

代理标准为我们提供了足够的功能,以实现代理子类化,而无需诉诸于类包装器并搞乱this

import * as util from 'node:util';
import http from 'node:http';
import { EventEmitter } from 'node:events';

async function DemoProxyHierarchy()
{
  const greeter = wrapEmitterBase(new EventEmitter());

  greeter.on("hello", (person) => console.log((`Hello, ${person}!`)));
  greeter.emit("hello", "World");
  greeter.emit("hello", "Benjamin");

  console.log(`on   calls: ${greeter.countOn}`);
  console.log(`emit calls: ${greeter.countEmit}`);
  console.log(`statistics: ${JSON.stringify(greeter.stats())}`);

  const stats = new Promise((Resolve, reject) => {
    let steps = 0;
    const server = http.createServer((req, res) => { res.end() });
    const netWrapper = wrapEmitterNet(server) as any;
    const done = () => {
      if (++steps > 2) {
        console.log(`\non   calls: ${netWrapper.countOn}`);
        console.log(`emit calls: ${netWrapper.countEmit}`);
        netWrapper.close(() => Resolve(netWrapper.stats()));
      }
    };

    netWrapper.listen(8080, done);
    http.get('http://localhost:8080', done);
    http.get('http://localhost:8080', done);
  });

  return stats.then(s => console.log(`net  stats: ${JSON.stringify(s)}`));
}

function wrapEmitterBase(ee: EventEmitter)
{
  const stats = { on: 0, emit: 0 };
  const handler = {
    get: (target, key) => {
      switch (key) {
        case "countOn":   return stats.on;
        case "countEmit": return stats.emit;
        case "stats":     return () => ({ ...stats });
        case "on":        { stats.on++;   break; }
        case "emit":      { stats.emit++; break; }
      }

      return target[key];
    },
  }

  return new Proxy(ee, handler);
}

function wrapEmitterNet(ee: EventEmitter)
{
  const stats = { listen: 0, close: 0 };
  const handler = {
    get: (target, key) => {
      switch (key) {
        case "stats":  {
          return () => ({ ...target[key](), ...stats });
        }
        case "listen": { stats.listen++; break; }
        case "close":  { stats.close++;  break; }
      }

      return target[key];
    },
  };

  return new Proxy(wrapEmitterBase(ee), handler);
}

// IIFE
(()=> { await DemoProxyHierarchy() })();

/* Output:

Hello, World!
Hello, Benjamin!
on   calls: 1
emit calls: 2
statistics: {"on":1,"emit":2}

on   calls: 1
emit calls: 5
net  stats: {"on":2,"emit":6,"listen":1,"close":1}
*/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接