首页 > 解决方案 > JavaScript 限制基于原型的继承?

问题描述

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

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

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

我从 sayHi 函数中得到了预期的输出。但是,如果我随后查询样式元素并通过 sheet 属性从中生成 CSSStyleSheet 对象,则未定义 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 对象。

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

标签: javascriptprototypal-inheritance

解决方案


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

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

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

当一个窗口被加载到 中时<iframe>, <object>, or <frame>,它的父窗口是包含嵌入窗口的元素的窗口。

例如:

// 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并不是真正的 cors-friendly


如果您的 iframe 来自另一个城堡域,那么事情会变得更加有趣。特别是,任何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 并event.origin在 eventListener 中检查;没有那个,永远不要在生产中使用 postMessage !
  • eval可以替换new Function(...)为对该处理程序代码的一些额外解析;由于该原型函数应该在页面出现之前一直存在,因此 GC 应该不是问题。

尽管如此,使用这个桥梁可能并不比您现在使用的方法特别简单。


推荐阅读