1

This is not a question about how to extend JS natives and I am well aware of the "dangers" involved with such activity. I am only trying to get a deeper understanding of how JavaScript works. Why is it that if I write the following:

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

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

I get the expected output from the sayHi function. But if I then query a style element and produce a CSSStyleSheet object from it via the sheet property, the sayHi function is not defined:

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

What is the reason for this? What would I have to do to make the sayHi function available to a CSSStyleSheet object produced via the sheet property - is it even possible?

The test was run in Chrome.

EDIT:

The reason I am looking into this is because I am trying to weigh my options when it comes to simplifying existing code. I have made an API to manipulate internal styles of a document loaded in an iFrame. It works as intended, but I would like to simplify the code if possible. It builds on the CSSOM API, which allows access to individual CSS style rules via numerical indexes. Having numerical indexes as the only way to access CSS rules seems quite rudimentary since you would never request a particular index unless you knew what rule the index pointed to. That is, you would always need to have info about the selector text. But it is the only way that makes sense in a broad context given the cascading nature of CSS (where you can have the same selector text as many times as you like).

However, my API keeps things in order so that every selector text is unique. Therefore, it makes sense to index the rules so that the main way of accessing them is via their selector text and my API does just that. However, things can quickly get less elegant when more than one level of rules are in play, i.e. if you have a number of media query rules containing their own index of CSS rules.

So I am just wondering if I can do anything to simplify the code and I must admit that were it not for the in this thread illustrated problems with hosted objects, i might have considered extending the CSSStyleSheet object.

Are there any other approaches, I might consider?

Stephen Miller
  • 503
  • 3
  • 11
  • 2
    Your code should work AFAICT. Please check and re-run it. – Ben Aston Oct 13 '20 at 09:15
  • 1
    Are you sure it's the same context? – raina77ow Oct 13 '20 at 09:28
  • @raina77ow, by context, do you refer to scope? – Stephen Miller Oct 13 '20 at 09:38
  • No, the same `window`. – raina77ow Oct 13 '20 at 09:38
  • @raina77ow, I think you might be pinpointing the actual problem here. Indeed the queried style element exists in an iframe, but the script issuing the query lives in the parent window. So, if that is the key to the problem, I would imagine that I should extend CSSStyleSheet from a script loaded in the iframe document. But that does not seem to solve it either? – Stephen Miller Oct 13 '20 at 12:08
  • That is the key, as iframe`s window is a separate object, and all the global objects there (includign `CSSStyleSheet` prototype) are different from the ones living on the parent's window there. – raina77ow Oct 13 '20 at 13:52
  • A word of caution though: while it's far less complicated and buggy than it was several years ago, messing around with hosted objects is usually _not a good idea_. Perhaps it might be worth the effort sharing the issue you're trying to solve this way? – raina77ow Oct 13 '20 at 13:53
  • Argh, I wrote that comment with messed layout in a hurry - and now don't really feel about dropping it and recreating from scratch. :) Would you mind if I describe the idea in an answer? I think there should be a way of augmenting the iframe's window prototypes on the fly, based on the corresponding properties of parent prototype... but all the cautions from my previous comment are still applicable. – raina77ow Oct 13 '20 at 15:28
  • @raina77ow, I'd be very interested in seeing your idea in an answer. :) As I mentioned in a previous comment, I have already tried to extend CSSStyleSheet via a script loaded in the head of the iframe document and that did not solve the problem. – Stephen Miller Oct 13 '20 at 15:57
  • Well, answer added; hope it'd be as fun to read as it was fun to write. :) I'm still suspicious of your description though; if your script is loaded correctly within iframe, it should be able to modify that iframe's window object. Consider elaborating more on that in a separate question - or just debug what's going on there on your side. – raina77ow Oct 13 '20 at 23:53
  • An interesting read indeed, although, the CORS part is not relevant to my particular problem. But it might be of value to others, although, I wonder: Is it really the "usual" approach as you put it? The one thing that helped me, though, was this line: Object.setPrototypeOf(CSSStyleSheet.prototype, parent.CSSStyleSheet.prototype); I switched the arguments, though, and placed it in my augmentation function that was placed in the iframe script, as mentioned, and it works! I would love to know more about why. Does it have something to do with the name resolving mechanism you mention? – Stephen Miller Oct 14 '20 at 15:04
  • In [Object.setPrototypeOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf), the first argument is an object which prototype you're going to change, and the second argument is a new prototype (for that object). So if you switched those arguments, now host window's StyleSheet has iframe window's StyleSheet in its prototype chain. – raina77ow Oct 14 '20 at 19:45
  • Thanks:) I get how Object.setPrototypeOf works. I am just a bit mindblown that it can be used as in your answer and it is not intuitively clear to me why it works. But I guess the comment section here is long enough as it is. I hope you will succeed in finding additional resources that may shed some additional light on the subjects. Thank you for your efforts. – Stephen Miller Oct 14 '20 at 22:35

3 Answers3

1

As it turned out, the issue is not about hosted object prototype limitations: while there are truly some, your particular example should work fine. The real problem is attempting to access this augmented prototype within iframe, which has its own global object. While there's a link between iframe's window and its host window, it's not used in the name resolving mechanism (few exceptions aside).

So the real challenge is to access host properties from within iframe. Now there are two ways of doing this: the easy one and the usual one.

The easy one is based on assumption that host and iframe share the same domain. With CORS concerns out of the way, you can connect those through parent property, as...

When a window is loaded in an <iframe>, <object>, or <frame>, its parent is the window with the element embedding the window.

For example:

// 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>

... and it should work. Here, Object.setPrototypeOf() is used to connect CSSStyleSheet.prototype of a parent (host) window to iframe's own CSSStyleSheet.prototype. Yes, garbage collector suddenly has got more work to do, but technically this should be considered a problem of browser writers, not yours.

Don't forget to test this on proper HTTP(S) Server locally, as file:/// based iframes are not really cors-friendly.


If your iframe is from another castle domain, things get way more interesting. In particular, any attempt to access parent directly is just blocked with that nasty Uncaught DOMException: Blocked a frame with origin "blah-blah" message, so no free cookies.

Technically, however, there's still a way to bridge that gap. What follows is some food for thought, showing that bridge in action:

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/>

The key part here is passing the stringified function from host to iframe through postMessage mechanism. It can (and should) be hardened:

  • using proper domain instead of '*' on postMessage and checking event.origin within eventListener is A MUST; never ever use postMessage in production without that!
  • eval can be replaced with new Function(...) with some additional parsing for that handler code; as that prototype function should live until the page does, GC shouldn't be a problem.

Still, using this bridge may not be particularly less complicated than the approach you employ right now.

raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • I am tempted to accept this answer (thank you for it), but maybe a few touch-ups would be nice: In the paragraph that begins with: "Don't forget..." you talk about potential CORS issues as concluding remarks to a "same-domain-solution". That's confusing. And what are "filesystem-born iframes" anyways? If possible it would be nice with some links that would allow visitors to dig further into this: What is the nature of objects resulting from a parent querying in a same-domain iframe? Why is it necessary to use Object.setPrototypeOf() to solve my particular problem and is the support universal? – Stephen Miller Oct 14 '20 at 15:28
  • I will update my answer trying to cover those things you mentioned, but: mostly it'll be links to other threads here. If you still feel they deserve a deeper discussion, I would suggest starting another question, more specific and more narrowed over the thing you need understanding. – raina77ow Oct 14 '20 at 19:31
0

Your code should work.

You can also call it using sheet.__proto__.sayHi()

Eason
  • 469
  • 4
  • 14
0

Your code (as written in the question) will work, because you are modifying the prototype object that is linked to by all instances of CSSStyleSheet (no matter when they were created).

The reference to the prototype object (more precisely: the [[Prototype]]) is examined dynamically every time a property look-up is attempted on an object without an own property that matches the requested property name. An own property is a property directly situated on an object.

In your case you are using the dot property accessor syntax sheet.sayHi. Property sayHi is not found as an own property, and so the prototype chain is traversed. It is then found on the prototype object that you modified on line 1. You then invoke the method located on that property using (), and 'CSSStyleSheet says hi!' is printed out.

Try it!

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()
Ben Aston
  • 53,718
  • 65
  • 205
  • 331