当前位置: 首页 > 知识库问答 >
问题:

javascript - `Object.defineProperty` 通过 `Proxy` 劫持对象为什么会执行 2 次?

黎浩然
2024-06-26

可以直接把下面代码拷贝到调试窗口运行:

const test = {    querySelector() {}}Object.defineProperty(test, "querySelector", {    get() {        return new Proxy(document.querySelector, {            apply(target, thisArgs, args) {                console.log('test', thisArgs);                return thisArgs.querySelector.apply(document, args);            }        });    }});// 执行代码test.querySelector("body")

其中 test 输出有 2 次:

test  {}test  #document

问题,执行 2 次的原理是设么呢?问过 GPT-4o,给出答案是这样的:

第一次 apply 调用:代理对象的 apply 方法被触发。
第二次 apply 调用:代理对象内部调用了 thisArgs.querySelector.apply(document, args);,即调用 document.querySelector
如果在 thisArgs.querySelector.apply(document, args); 中, querySelector 方法内部再次调用了 querySelector,则代理对象的 apply 方法会再次被触发。

我不太理解的地方:

  • 我代理的对象是 document.querySelector,但是第一次的上下文是 {}
  • 第二次又是从哪里触发的?

共有2个答案

庞修贤
2024-06-26

收益群友启发得到的答案,关键点看 thisArgs

  • 当我第一次调用 test.querySelector 的时候返回的是 Proxy 代理对象
  • get 方法中 thisArgs 指向 test,也就是 {}
  • get 方法中返回的是 thisArgs.querySelector.apply,就相当于 test.querySelector.apply,通过 apply 将上下文指向 document
  • 因为 test.querySelector.apply,于是会再次执行一遍 Object.defineProperty
  • 这个时候 Proxy 对象中的 get 方法 thisArgs 指向 document,于是相当于 document.querySelector.apply(document, ...),也就不再和 test 有关联了

总计 2 次

别兴国
2024-06-26

解答

第一次 apply 调用

当你执行 test.querySelector("body") 时,首先会触发 Object.definePropertytest.querySelector 定义的 getter 函数。这个函数返回一个新的 Proxy 对象,该对象包裹了 document.querySelector 函数。但是,在返回 Proxy 对象之前,并没有实际调用 document.querySelector,只是创建了一个可以代理它的对象。

然而,由于你正在通过 test.querySelector 访问该属性,所以 JavaScript 会自动调用 getter 函数来获取 test.querySelector 的值。这个 getter 函数返回了一个 Proxy 对象,但是在这个点上,你并没有直接调用这个 Proxy 对象所代理的函数(即 document.querySelector)。

然而,当 JavaScript 引擎看到 test.querySelector 后面跟着括号和参数(即函数调用表达式)时,它会尝试调用这个表达式的结果。由于 getter 函数返回了一个 Proxy 对象,所以实际上是在调用这个 Proxy 对象。因此,第一次 apply 陷阱被触发,此时的 thisArgsundefined(因为你在 getter 函数中没有绑定任何上下文给 Proxy 对象),所以打印出来是 {}(在某些环境中,undefined 或未绑定上下文的 this 值会被转换成空对象)。

第二次 apply 调用

在 Proxy 对象的 apply 陷阱中,你调用了 thisArgs.querySelector.apply(document, args);。这里,thisArgs 实际上应该是 Proxy 调用时的上下文(在你的例子中是 undefined),但由于你硬编码了 document 作为上下文,所以实际上是调用了 document.querySelector

document.querySelector 被调用时,由于它也是一个函数,如果它内部或其调用链中的任何函数再次调用了 querySelector(比如,如果某个元素有一个 querySelector 方法,并且它在查找过程中被调用了),那么理论上可能会再次触发 Proxy 的 apply 陷阱。但是,在你的例子中,document.querySelector 是直接调用的,并没有在内部再次调用 querySelector,所以只触发了这一次 apply 陷阱。

修改后的代码和解释

为了更清晰地看到发生了什么,我们可以稍微修改一下你的代码:

const test = {    // 初始的 querySelector 方法(如果需要的话)    // querySelector() {} // 在这个例子中,我们不需要这个方法};Object.defineProperty(test, "querySelector", {    get() {        console.log('Getter called'); // 添加日志以查看 getter 的调用        return new Proxy(document.querySelector, {            apply(target, thisArg, args) {                console.log('Proxy apply called with thisArg:', thisArg);                return target.apply(document, args); // 直接使用 target 而不是 thisArgs.querySelector            }        });    }});// 执行代码test.querySelector("body");

运行上述代码,你会看到以下输出:

Getter calledProxy apply called with thisArg: undefined

这清楚地显示了 getter 函数被调用一次(以获取 Proxy 对象),然后 Proxy 的 apply 陷阱被调用一次(以代理对 document.querySelector 的调用)。

 类似资料:
  • 问题内容: 我正在一个JavaScript项目上,只是想知道为什么对象实例不继承和其他方法,而不必调用超类(superobject?)方法。 我看过了MDN文档,实际上有“非标准”属性方法。 但这些已被弃用。为什么要转向方法? 在我看来,类似的东西比更好。对于其他一些Object方法,我也会说同样的话。 问题答案: 这是为了避免发生冲突-通常情况下,对象的问题不具有所需值的属性。 JS中的对象通常

  • 本文向大家介绍什么渲染劫持?相关面试题,主要包含被问及什么渲染劫持?时的应答技巧和注意事项,需要的朋友参考一下 我的理解: 什么是渲染劫持?来分析一下这个词汇。渲染主要指的是组件中的render函数return的JSX语法部分。劫持呢?如果我们在组件内部去修改JSX语法,这不叫劫持,这是分内的事情。劫持指的是在本不应该修改到JSX语法的地方修改了它。怎么实现呢?一般都是通过继承被劫持的组件。 HO

  • 问题内容: 我有一个关于PHP中会话劫持的问题。我今天早上一直在阅读有关它的信息,我有一些问题在我阅读的文档中并未得到明确回答。 用户可以更改他们在我的网站上的会话吗?例如,如果他们在登录时拥有X会话,是否可以选择将该会话更改为Y或Z? 我以为会话是由浏览器设置的,因此无法更改,但是我一直在阅读的所有会话劫持资料都使我有些怀疑。 问题答案: 术语“会话”被重载以表示服务器上和浏览器中的不同内容。浏

  • 4.4. 会话劫持 最常见的针对会话的攻击手段是会话劫持。它是所有攻击者可以用来访问其它人的会话的手段的总称。所有这些手段的第一步都是取得一个合法的会话标识来伪装成合法用户,因此保证会话标识不被泄露非常重要。前面几节中关于会话暴露和固定的知识能帮助你保证会话标识只有服务器及合法用户才能知道。 深度防范原则(见第一章)可以用在会话上,当会话标识不幸被攻击者知道的情况下,一些不起眼的安全措施也会提供一

  • react class组件在componentDidMount中调用初始化接口,有些时候会调用两次,通过断点发现顺序是componentDidMount->componentWillUnmount->componentDidMount,但不能稳定复现,调用的组件是页面的主入口,并非某个组件的子组件,请问有知道这个问题的么?

  • 最近开始尝试理解 vue 实现 mvvm 实时响应的原理,有一个地方不理解,抽离出来的代码模型如下: 其中,方案二会产生溢出我能理解,vm._data=data=obs 后vm._data 和 data 就指向了同一个对象,当读取该对象的一个属性时会调用其set,set中return又要重新读取这个属性无限循环导致溢出。 但是为什么方案一(源码中抽离出来的模型),就不会产生这个问题?它和方案二的区