【问题标题】:JavaScript limits to prototype-based inheritance?JavaScript 限制基于原型的继承?
【发布时间】:2021-01-27 14:36:50
【问题描述】:

这不是关于如何扩展 JS 原生的问题,我很清楚此类活动所涉及的“危险”。我只是想更深入地了解 JavaScript 的工作原理。为什么如果我写以下内容:

CSSStyleSheet.prototype.sayHi = function() {
    console.log('CSSStyleSheet says hi!');
};

var test = new CSSStyleSheet;
test.sayHi();  // Console output: CSSStyleSheet says hi!

我从 sayHi 函数中得到了预期的输出。但是,如果我随后查询一个样式元素并生成一个 CSSStyleSheet 对象 通过 sheet 属性从中获取,未定义 sayHi 函数:

var styleElm = document.querySelector('style'),
    sheet = styleElm.sheet;
console.log('sheet', sheet)  // Console output: sheet CSSStyleSheet {ownerRule: null,...
sheet.sayHi();  // Console output: Uncaught TypeError: sheet.sayHi is not a function

这是什么原因?我需要做些什么才能使 sayHi 函数可用于通过 sheet 属性生成的 CSSStyleSheet 对象 - 甚至可能吗?

测试在 Chrome 中运行。

编辑:

我之所以对此进行研究,是因为在简化现有代码方面,我试图权衡我的选择。我制作了一个 API 来操作 iFrame 中加载的文档的内部样式。它按预期工作,但我想尽可能简化代码。它建立在 CSSOM API 之上,允许通过数字索引访问单个 CSS 样式规则。将数字索引作为访问 CSS 规则的唯一方法似乎非常初级,因为除非您知道索引指向的规则,否则您永远不会请求特定的索引。也就是说,您总是需要有关选择器文本的信息。但考虑到 CSS 的级联特性(您可以根据需要多次使用相同的选择器文本),这是在广泛的上下文中有意义的唯一方法。

但是,我的 API 使事情井井有条,因此每个选择器文本都是唯一的。因此,索引规则是有意义的,以便访问它们的主要方式是通过它们的选择器文本,而我的 API 就是这样做的。但是,当有多个规则在使用时,事情很快就会变得不那么优雅,即,如果您有许多包含自己的 CSS 规则索引的媒体查询规则。

所以我只是想知道我是否可以做任何事情来简化代码,我必须承认,如果不是因为在这个线程中说明了托管对象的问题,我可能会考虑扩展 CSSStyleSheet 对象。

还有其他方法,我可以考虑吗?

【问题讨论】:

  • 您的代码应该可以使用 AFAICT。请检查并重新运行。
  • 你确定是同一个上下文吗?
  • @raina77ow,根据上下文,您指的是范围吗?
  • 不,同样的window
  • @raina77ow,我想你可能在这里查明了实际问题。实际上,查询的样式元素存在于 iframe 中,但发出查询的脚本位于父窗口中。所以,如果这是问题的关键,我想我应该从 iframe 文档中加载的脚本扩展 CSSStyleSheet。但这似乎也没有解决它?

标签: javascript prototypal-inheritance


【解决方案1】:

您的代码应该可以工作。

您也可以使用sheet.__proto__.sayHi() 调用它

【讨论】:

    【解决方案2】:

    您的代码(如问题中所写)将起作用,因为您正在修改由 CSSStyleSheet 的所有实例链接到的原型对象(无论它们是何时创建的)。

    每次尝试对没有与请求的属性名称匹配的自有属性的对象进行属性查找时,都会动态检查对原型对象的引用(更准确地说:[[Prototype]])。自己的属性是直接位于对象上的属性。

    在您的情况下,您使用的是dot property accessor syntax sheet.sayHi。没有找到属性sayHi 作为自己的属性,因此遍历原型链。然后在您在第 1 行修改的原型对象上找到它。然后您使用() 调用位于该属性上的方法,并打印出'CSSStyleSheet says hi!'

    试试吧!

    CSSStyleSheet.prototype.sayHi = function() {
        console.log('CSSStyleSheet says hi!');
    };
    
    const test = new CSSStyleSheet;
    test.sayHi();  // Console output: CSSStyleSheet says hi!
    
    const styleElm = document.querySelector('style'),
          sheet = styleElm.sheet;
    sheet.sayHi()

    【讨论】:

      【解决方案3】:

      事实证明,问题不在于托管对象原型的限制:虽然确实存在一些限制,但您的特定示例应该可以正常工作。真正的问题是试图在 iframe 中访问这个增强的原型,它有自己的全局对象。虽然 iframe 的 window 与其主机 window 之间存在链接,但它并未用于名称解析机制(除了少数例外)。

      所以真正的挑战是从 iframe 中访问主机属性。现在有两种方法可以做到这一点:简单的一种和通常的一种。

      最简单的方法是假设主机和 iframe 共享同一个域。解决 CORS 问题后,您可以通过 parent 属性将它们连接起来,因为...

      当一个窗口在<iframe>, <object>, or <frame> 中加载时,它的 parent 是元素嵌入窗口的窗口。

      例如:

      // host.html
      <script>
      CSSStyleSheet.prototype.sayHi = (space) => { 
        console.log(`CSSStyleSheet says hi! from ${space}`);
      };
      </script>
      <iframe sandbox="allow-same-origin allow-scripts" src="iframe.html" />
      
      // iframe.html
      <button>Say Hi!</button>
      <script>
        Object.setPrototypeOf(CSSStyleSheet.prototype, parent.CSSStyleSheet.prototype);
        document.querySelector('button').onclick = () => {
          new CSSStyleSheet().sayHi('inner space');
        };
      </script>
      

      ... 它应该可以工作。这里,Object.setPrototypeOf() 用于将父(主机)窗口的CSSStyleSheet.prototype 连接到 iframe 自己的CSSStyleSheet.prototype。是的,垃圾收集器突然有更多工作要做,但从技术上讲,这应该被认为是浏览器编写者的问题,而不是你的问题。

      不要忘记在本地适当的 HTTP(S) 服务器上进行测试,因为基于 file:/// 的 iframe are not really cors-friendly


      如果您的 iframe 来自另一个 castle 域,事情会变得更加有趣。特别是,任何直接访问 parent 的尝试都会被讨厌的 Uncaught DOMException: Blocked a frame with origin "blah-blah" 消息阻止,因此没有免费的 cookie。

      不过,从技术上讲,仍有办法弥合这一差距。以下是一些值得深思的内容,展示了这座桥梁的实际作用:

      console.clear(); // check the browser console; iframe's one won't be visible here
      CSSStyleSheet.prototype.sayHi = (space) => { 
        console.log(`CSSStyleSheet says hi! from ${space}`);
      };
      document.querySelector('button').onclick = () => {
        new CSSStyleSheet().sayHi('outer space');
      };
      
      const html = `<button>Say Inner Hi!</button><br />
      <script>
        parent.postMessage('PING', '*'); // HANDSHAKE
        document.querySelector('button').onclick = () => {
          new CSSStyleSheet().sayHi('inner space');
        };
      
        addEventListener('message', (event) => {
          const { data } = event;
          if (data === null) {
            delete CSSStyleSheet.prototype.sayHi;
          }
          else {
            CSSStyleSheet.prototype.sayHi = eval(data);
          }
        }, false);
      <` + `/script>`;
      
      const iframe = document.createElement('iframe');
      const blob = new Blob([html], {type: 'text/html'});
      iframe.src = window.URL.createObjectURL(blob);
      document.body.appendChild(iframe);
      
      let iframeWindow = null;
      addEventListener('message', event => {
        if (event.origin !== "null") return; // PoC
        if (event.data === 'PING') {
          iframeWindow = event.source;
          console.log('PONG');
        }
      }, false);
      
      document.querySelector('input').onchange = ({target}) => {
        if (!iframeWindow) return;
        iframeWindow.postMessage(target.checked 
          ? CSSStyleSheet.prototype.sayHi.toString()
          : null, 
        '*'); // augment the domain here
      };
      <button>Say Outer Hi!</button> 
      <label><b>INCEPTION MODE</b><input type="checkbox" /></label><br/>

      这里的关键部分是通过 postMessage 机制将字符串化函数从主机传递到 iframe。它可以(并且应该)被强化:

      • 在 postMessage 上使用正确的域而不是 '*' 并在 eventListener 中检查 event.origin 是必须的;没有它,永远不要在生产中使用 postMessage!
      • eval 可以替换为 new Function(...) 并对该处理程序代码进行一些额外的解析;由于该原型函数应该在页面出现之前一直存在,因此 GC 应该不是问题。

      不过,使用此桥可能并不比您现在使用的方法特别简单。

      【讨论】:

      • 我很想接受这个答案(谢谢你),但也许一些修饰会很好:在开头的段落中:“不要忘记......”你将潜在的 CORS 问题作为对“相同域解决方案”的结束语进行讨论。这很令人困惑。什么是“文件系统生成的 iframe”?如果可能的话,最好有一些链接可以让访问者进一步挖掘:父查询在同域 iframe 中产生的对象的性质是什么?为什么需要使用 Object.setPrototypeOf() 来解决我的特定问题并且支持普遍?
      • 我会更新我的答案,试图涵盖你提到的那些事情,但是:主要是链接到这里的其他线程。如果您仍然认为他们值得深入讨论,我建议您开始另一个问题,更具体,更缩小您需要理解的内容。
      猜你喜欢
      • 2010-10-23
      • 1970-01-01
      • 1970-01-01
      • 2012-08-25
      • 2010-11-25
      • 2010-09-28
      • 2018-02-21
      • 1970-01-01
      相关资源
      最近更新 更多