likes
comments
collection
share

Rich Harris在twitter关于Proxy的讨论

作者站长头像
站长
· 阅读数 14

前言

最近在学习Proxy,准备做个小东西,应该会在下一篇文章跟大家见面(先打波广告 :) )。在使用Proxy的过程中,遇到了Uncaught TypeError: Illegal invocation报错。刚好搜到Rich Harris六月份在twitter发了一条关于Proxy的推特,仔细看了看评论,觉得有很多值得学习的地方,所以写这篇文章记录一下。

原文如下:

JS lazyweb: i need to make a Proxy of a URL object (for reasons that don't currently matter). i thought the whole point of proxies was that they're identical to the proxied object, minus whatever behaviour you've added, but that's not what i'm seeing. what am i doing wrong?

// attempt 1 
url = new URL(location.href); 
proxy = new Proxy(url, {}); 
proxy.pathname; // Uncaught TypeError: Illegal invocation 

// attempt 2 
url = new URL(location.href); 
proxy = new Proxy(url, { 
    get: (target, property, receiver) => { 
        return Reflect.get(target, property, target); 
    } 
}); 
proxy.pathname; // works 
proxy.toString(); // Uncaught TypeError: Illegal invocation 

// attempt 3 
url = new URL(location.href); 
proxy = new Proxy(url, { 
    get: (target, property) => { 
        return typeof target[property] === 'function' 
            ? (...args) => target[property](...args) 
            : target[property]; 
    } 
}); 
proxy.pathname; // works 
proxy.toString(); // works 
proxy.toString === proxy.toString; // false

大概意思就是Rich Harris要创建一个URL对象的代理。他认为除去添加的行为外,代理的意义在于它们跟代理的对象相同,但是从attempt看,并非如此。所以是哪里出错了呢?

Illegal invocation

调用关键字this而未引用其最初执行的对象时将引发“非法调用”。换句话说,就是原始的“上下文”丢失了。

结合Proxy来看,就是在代理的情况下,目标对象内部的this关键字指向了Proxy代理对象,导致与目标对象的行为不一致。有些原生对象(DateSetMap等)的内部属性,只有通过正确的this才能拿到。

const target = { 
    foo() { 
        return { 
            isTarget: this === target, 
            isProxy: this === proxy
        };
    }
}; 

const proxy = new Proxy(target, {}); 
console.log(target.foo()); // { isTarget: true, isProxy: false } 
console.log(proxy.foo()); // { isTarget: false, isProxy: true }

attempt 1

从上文可知attempt 1报错的问题所在,不再过多解释。

attempt 2

attempt 2通过Reflect.get(target, property, target)解决了pathname属性的获取报错问题。

Reflect.get(target, propertyKey[, receiver])

target:需要取值的目标对象

propertyKey:需要获取的值的键值

receiver:如果target对象中指定了getterreceiver则为getter调用时的this值。

handler.get()中的receiver是Proxy或者继承Proxy的对象。

所以Reflect.get(target, property, target)this重新指向为target目标对象,就解决了属性的获取问题。

但是新的问题出现了,proxy.toString()调用报错。而同时url.toString === proxy.toStringtrue

别急,我们先看看attempt 3。

attempt 3

attempt 3是通过判断获取的属性是否为函数:是的话,返回一个箭头函数,箭头函数的返回值是target对应的函数;否则,返回target对应的属性。

proxy.toString可以正常调用了,但是问题也显而易见,每次调用都会返回一个新的函数,所以proxy.toString === proxy.toString始终为false

问题

所以Rich Harris真正关心的是在url.toString === proxy.toStringtrue的情况下,为什么proxy.toString会调用失败?反而需要重新去绑定this关键字的指向来让proxy.toString正常执行,但不管通过(...args) => target[property](...args)还是target[property].bind(target)也好,都无法解决proxy.toString === proxy.toStringurl.toString === proxy.toStringfalse

也许可以通过WeakMap缓存或者在外部写具名函数来解决proxy.toString === proxy.toString的问题,但是url.toString === proxy.toString始终还是存在问题。

url.toString === proxy.toString

我觉得有两位的回复是解答了关于url.toString === proxy.toString的问题。

Bradley Farias的回答大致意思是Proxy代理了对象,但是也破坏了类似于身份检查的执行Keith Cirkel也表示url.toString会确定调用对象是否为一个实际的URL对象,而不是其他类型的对象。

Rich Harris在twitter关于Proxy的讨论

总结

最后Rich Harris打算使用Object.defineProperty取代Proxy来实现他所设想的功能(拦截hash属性来确保在SSR期间不应被访问)。

Matt Robb提出如果代理的是原型链,而不是顶层对象的话,一切将正常运行

Rich Harris在twitter关于Proxy的讨论

如果大家知道Matt Robb提出的具体实现方法,麻烦在评论区告诉我一下,或者还有其他更好的方法,欢迎提出来讨论!期待大家的评论!

在回复当中,还有一些有趣的解决方案和方法,在这里摘抄记录一下:

WrappedURL

Andrew Courtice提出通过重新封装URL类,来实现类似于Proxy的效果。

class WrappedURL extends URL {
    toString() { 
        return super.toString(); 
    } 
}

proxify

在回复中,Preet Shihn提出的proxify方法也是得到Rich Harris的点赞和认可。不仅解决了proxy.toString的调用问题,还实现了嵌套对象的嵌套代理。

const proxify = (something, root) => new Proxy(something, {
    get(target, prop, receiver) {
        if ((typeof target[prop] === 'function') || (typeof target[prop] === 'object')) {
            return proxify(target[prop], something);
        }
        return Reflect.get(target, prop);
    },
    apply(target, thisArg, argumentsList) {
        return Reflect.apply(target, root || thisArg, argumentsList);
    }
});

const url = new URL('https://twitter.com/Rich_Harris/status/1539375179394056192');
const proxy = proxify(url);
console.log(proxy.pathname); // works
console.log(proxy.toString()); // works

Vue3

最后

刚学Proxy,可能部分概念描述不是很准确,请多多包涵!以上内容如果有说得不对的地方,请各位老哥在评论区指出,我会尽快做出修改。

如果觉得小老弟写得还行,不介意的话,来个三连(点赞、收藏、关注)。

你的认可,是我不断前进的动力。

Rich Harris在twitter关于Proxy的讨论

本人两年多前端开发经验,熟悉Vue相关技术栈。

如果各位老哥的公司里有坑位,麻烦踢踢我,带带老弟。