JavaScript - overwriting .onload prototype of HTML

2019-01-28 22:57发布

问题:

Is it possible to bind an onload event to each image, declaring it once? I tried, but can't manage to get it working... (this error is thrown: Uncaught TypeError: Illegal invocation)

HTMLImageElement.prototype.onload = function()
{
        console.log(this, "loaded");
};

P.S: I also tried returning this, but doesn't seem to be the issue here... any suggestions / explanations on why my current code isn't working?

回答1:

You can't set a handler on the prototype, no.

In fact, I'm not aware of any way to get a proactive notification for image load if you haven't hooked load on the specific image element, since load doesn't bubble.

I only two know two ways to implement a general "some image somewhere has loaded" mechanism:

  1. Use a timer loop, which is obviously unsatisfying on multiple levels. But it does function. The actual query (document.getElementsByTagName("img")) isn't that bad as it returns a reference to the continually updated (live) HTMLCollection of img elements, rather than creating a snapshot like querySelectorAll does. Then you can use Array.prototype methods on it (directly, to avoid creating an intermediary array, if you like).

  2. Use a mutation observer to watch for new img elements being added or the src attribute on existing img elements changing, then hook up a load handler if their complete property isn't true. (You have to be careful with race conditions there; the property can be changed by the browser even while your JavaScript code is running, because your JavaScript code is running on a single UI thread, but the browser is multi-threaded.)



回答2:

You get that error because onload is an accessor property defined in HTMLElement.prototype.

You are supposed to call the accessor only on HTML elements, but you are calling the setter on HTMLImageElement.prototype, which is not an HTML element.

If you want to define that function, use defineProperty instead.

Object.defineProperty(HTMLImageElement.prototype, 'onload', {
  configurable: true,
  enumerable: true,
  value: function () {
    console.log(this, "loaded");
  }
});
var img = new Image();
img.onload();

Warning: Messing with builtin prototypes is bad practice.

However, that only defines a function. The function won't be magically called when the image is loaded, even if the function is named onload.

That's because even listeners are internal things. It's not that, when an image is loaded, the browser calls the onload method. Instead, when you set the onload method, that function is internally stored as an event listener, and when the image is loaded the browser runs the load event listeners.

Instead, the proper way would be using Web Components to create a custom element:

var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
  var img = document.createElement('img');
  img.src = this.getAttribute('src');
  img.addEventListener('load', function() {
    console.log('loaded');
  });
  this.appendChild(img);  
};
document.registerElement('my-img', {prototype: proto});
<my-img src="/favicon.ico"></my-img>

There is not much browser support yet, though.



回答3:

I've written something similar some time ago to check if an image is loaded or not, and if not, show a default image. You can use the same approach.

$(document).ready(function() {
    // loop every image in the page
    $("img").each(function() {
        // naturalWidth is the actual width of the image
        // if 0, img is not loaded
        // or the loaded img's width is 0. if so, do further check
        if (this.naturalWidth === 0) { // not loaded
            this.dataset.src = this.src; // keep the original src
            this.src = "image404.jpg";
        } else {
            // loaded
        }
    });
});


回答4:

This provides a notification for any image loading, at least in Opera (Presto) and Firefox (haven't tried any other browser). The script tag is placed in the HEAD element so it is executed and the event listener installed before any of the body content is loaded.

document.addEventListener('load', function(e) {
    if ((!e.target.tagName) || (e.target.tagName.toLowerCase() != 'img')) return;
    // do stuff here
}, true);

Of course, by changing the filtering on tagName it will also serve to respond to the loading of any other element that fires a load event, such as a script tag.