如果我在构造函数中使用 Proxy:get,为什么 mocha chai 不应该证明 `return this` 的身份?

Why does mocha chai should not prove identity for `return this`, if I use Proxy:get in the constructor?

我想写一个 class,它处理未定义的属性。我还希望 return this 能够链接方法来创建领域特定语言 (DSL)。

I return 来自构造函数的代理,用于处理未定义的属性。现在在测试实例时,确实发生了 return this 不能证明与实例相同的情况。我担心由此产生的错误,尽管我可以按预期链接这些方法。

这是一个 mocha chai 测试来显示行为。将最后一条指令中的 o.that().should.not.equal(o); 替换为 o.that().should.equal(o);,看看它是如何失败的。

require('chai').should();

describe('chai testing classes using Proxy', () => {
    it('asserts object identity, if an instance method returns this', () => {
        const o = new class NormalClass{ }
        o.that = function() { return this; }
        o.that().should.equal(o);
    });
    it('observes the same behaviour for constructors returning a dummy Proxy', () => {
        const o = new class ProxyClass{
            constructor() { return new Proxy(this, {}); }
        }
        o.that = function() { return this; }
        o.that().should.equal(o);
    });
    it('requires deep.equal on the other hand, if the Proxy handles get', () => {
        const o = new class ProxyClassPlusGet{
            constructor() {
                return new Proxy(this, {
                    get: function(target, prop) { return target[prop]; },
                });
            }
        }
        o.that = function() { return this; }
        o.that().should.deep.equal(o);
        o.that().should.not.equal(o);
    });
});

只要 o.that() === o 产生 true.

,您的实施就可以工作

但它不适用于 getter,这会干扰 chai 的 should。您可以使用

重现此内容

const o = new Proxy({
    get self() { return this; },
    that() { return this; },
}, {
    get(target, prop) { return target[prop]; },
});
console.log(o.self === o);
console.log(o.that() === o);

这是为什么?因为你的get陷阱被打破了,忽略了 of the property access. It will hold the proxy, o, but when you do return target[prop] then target will be the receiver. You can fix it by using Reflect.get:

const o = new Proxy({
    get self() { return this; },
    that() { return this; },
}, {
    get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver);
//                                       ^^^^^^^^
    },
});
console.log(o.self === o);
console.log(o.that() === o);

除了@Bergi 给出的简明答案外,我还添加了另一个测试。事实证明,他的解决方案表现出良好的行为:

  • 它returns在所有情况下都是代理对象的身份。
  • 它将数据存储到底层原始对象中。
  • 它不会与作为接收方的代理形成恶性循环。
  • 它与 chai expectchai should.
  • 一致

我仍然不完全理解代理的内容。做这个测试,已经给出了深刻的见解。感谢@Bergi 提供正确的解决方案。

require('chai').should();
const expect = require('chai').expect;

describe('general Proxy behaviour', () => {
    describe('Proxy with empty handler', () => {
        it('shows that original and proxy are two different objects' , () => {
            const original = {};
            const proxy = new Proxy(original, {});
            expect(proxy).not.equal(original);
        });
        it('evaluates proxy and origianl to be deep equal' , () => {
            const original = {};
            const proxy = new Proxy(original, {});
            expect(proxy).to.deep.equal(original);
        });
        it('sets and gets down to the original', () => {
            const original = {};
            const proxy = new Proxy(original, {});
            proxy.x = 1;
            expect(original.x).to.equal(1);
            original.x = 2;
            expect(proxy.x).to.equal(2);
        });
        describe('consistent when accessing this', () => {
            const original = {
                getThat() { return this; },
                get that() { return this; },
            };
            const proxy = new Proxy(original, { });
            it('evaluates getters to the proxy', () => {
                expect(proxy.that).to.be.equal(proxy)
            });
            it('evaluates methods to the proxy, too', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
            });
            it('chai should works like chai expect', () => {
                proxy.that.should.equal(proxy)
                proxy.getThat().should.equal(proxy)
            });
        });
    });
    describe('Proxy without reflection', () => {
        it('sets and gets down to the original like the empty handler', () => {
            const original = {};
            const proxy = new Proxy(original, {
                set: function(target, prop, value) { target[prop]=value; },
                get: function(target, prop) { return target[prop]; },
            });
            proxy.x = 1;
            expect(original.x).to.equal(1);
            original.x = 2;
            expect(proxy.x).to.equal(2);
        });
        it('detects original as the target in get and set',
            () => {
                const original = {};
                let targetInSet;
                let targetInGet;
                const proxy = new Proxy(original, {
                    set: function(target, prop, value, receiver) {
                        targetInSet = target;
                        target[prop]=value;
                    },
                    get: function(target, prop, receiver) {
                        targetInGet = target;
                        expect(target).to.equal(original);
                        return target[prop];
                    },
                });
                proxy.x = 1;
                proxy.x;
                expect(targetInSet).to.equal(original);
                expect(targetInGet).to.equal(original);
        });
        it('detects proxy as the receiver in get and set',
            () => {
                const original = {};
                let receiverInSet;
                let receiverInGet;
                const proxy = new Proxy(original, {
                    set: function(target, prop, value, receiver) {
                        receiverInSet = receiver;
                        target[prop]=value;
                    },
                    get: function(target, prop, receiver) {
                        receiverInGet = receiver;
                        return target[prop];
                    },
                });
                proxy.x = 1;
                proxy.x;
                expect(receiverInSet).to.equal(proxy);
                expect(receiverInGet).to.equal(proxy);
        });
        describe('chaos when accessing this', () => {
            const original = {
                getThat() { return this; },
                get that() { return this; },
            };
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    receiverInGet = receiver;
                    return target[prop];
                },
            });
            it('evaluates getters to the original', () => {
                expect(proxy.that).to.be.equal(original)
            });
            it('evaluates methods to the proxy', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
            });
            it('chai should differs from chai expect', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
                proxy.getThat().should.equal(original)
            });
            it('chai should evaluates to original in both cases', () => {
                proxy.getThat().should.equal(original)
                proxy.that.should.equal(original)
            });
        });
    });
    describe('Reflect in Proxy without the receiver being set', () => {
        it('sets and gets down to the original like the empty handler', () => {
            const original = {};
            const proxy = new Proxy(original, {
                get: function(target, prop) {
                    return Reflect.get(target, prop);
                },
                set: function(target, prop, value) {
                    Reflect.set(target, prop, value);
                },
            });
            proxy.x = 1;
            expect(original.x).to.equal(1);
            original.x = 2;
            expect(proxy.x).to.equal(2);
        });
        describe('chaos when accessing this', () => {
            const original = {
                getThat() { return this; },
                get that() { return this; },
            };
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop);
                },
            });
            it('evaluates getters to the original', () => {
                expect(proxy.that).to.be.equal(original)
            });
            it('evaluates methods to the proxy', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
            });
            it('chai should differs from chai expect', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
                proxy.getThat().should.equal(original)
            });
            it('chai should evaluates to original in both cases', () => {
                proxy.getThat().should.equal(original)
                proxy.that.should.equal(original)
            });
        });
    });

    describe('Reflect in Proxy with the receiver being set to original', () => {
        it('sets and gets down to the original like the empty handler', () => {
            const original = {};
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop, target);
                },
                set: function(target, prop, value, receiver) {
                    Reflect.set(target, prop, value, target);
                },
            });
            proxy.x = 1;
            expect(original.x).to.equal(1);
            original.x = 2;
            expect(proxy.x).to.equal(2);
        });
        describe('chaos when accessing this', () => {
            const original = {
                getThat() { return this; },
                get that() { return this; },
            };
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop, target);
                },
            });
            it('evaluates getters to the original', () => {
                expect(proxy.that).to.be.equal(original)
            });
            it('evaluates methods to the proxy', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
            });
            it('chai should differs from chai expect', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
                proxy.getThat().should.equal(original)
            });
            it('chai should evaluates to original in both cases', () => {
                proxy.getThat().should.equal(original)
                proxy.that.should.equal(original)
            });
        });
    });
    describe('Reflect in Proxy with the receiver being set to proxy', () => {
        it('sets and gets down to the original like the empty handler', () => {
            const original = {};
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop, receiver);
                },
                set: function(target, prop, value, receiver) {
                    Reflect.set(target, prop, value, receiver);
                },
            });
            proxy.x = 1;
            expect(original.x).to.equal(1);
            original.x = 2;
            expect(proxy.x).to.equal(2);
        });
        it('does not cause a vicious circle in the proxy', () => {
            const original = {};
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop, receiver);
                },
                set: function(target, prop, value, receiver) {
                    Reflect.set(target, prop, value, receiver);
                },
            });
            proxy.x = 1;
            expect(proxy.x).to.equal(1);
        });
        describe('consistent when accessing this', () => {
            const original = {
                getThat() { return this; },
                get that() { return this; },
            };
            const proxy = new Proxy(original, {
                get: function(target, prop, receiver) {
                    return Reflect.get(target, prop, receiver);
                },
            });
            it('evaluates getters to the proxy', () => {
                expect(proxy.that).to.be.equal(proxy)
            });
            it('evaluates methods to the proxy, too', () => {
                expect(proxy.getThat()).to.be.equal(proxy)
            });
            it('chai should works like chai expect', () => {
                proxy.that.should.equal(proxy)
                proxy.getThat().should.equal(proxy)
            });
        });
    });
});