forEach on querySelectorAll not working in recent Microsoft browsers

情到浓时终转凉″ 提交于 2019-11-26 12:31:43

The return value of querySelectorAll isn't an array, it's a NodeList. That only recently got forEach (and compatibility with JavaScript's iteration protocol, letting you use them as the targets of for-of and spread notation).

You can polyfill forEach easily:

if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
    // Yes, there's really no need for `Object.defineProperty` here
    NodeList.prototype.forEach = Array.prototype.forEach;
}

Direct assignment is fine in this case, because enumerable, configurable, and writable should all be true and it's a value property. (enumerable being true surprised me, but that's how it's defined natively on Chrome, Firefox, Edge, and Safari).


When NodeList got forEach, it also became iterable, meaning you could loop through the contents of a NodeList via for-of loops, and use a NodeList in other places where an iterable is expected (for instance, in spread notation in an array initializer).

In practice, a browser that has features that use iterability (like for-of loops) is also likely to already provide these features of NodeList, but to ensure that (perhaps you're transpiling and including a polyfill for Symbol), we'd need to do a second thing: Add a function to its Symbol.iterator property that creates an iterator:

if (typeof Symbol !== "undefined" && Symbol.iterator && typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype[Symbol.iterator]) {
    Object.defineProperty(NodeList.prototype, Symbol.iterator, {
        value: Array.prototype[Symbol.itereator],
        writable: true,
        configurable: true
    });
}

Doing both together:

if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
    // Yes, there's really no need for `Object.defineProperty` here
    NodeList.prototype.forEach = Array.prototype.forEach;
    if (typeof Symbol !== "undefined" && Symbol.iterator && !NodeList.prototype[Symbol.iterator]) {
        Object.defineProperty(NodeList.prototype, Symbol.iterator, {
            value: Array.prototype[Symbol.itereator],
            writable: true,
            configurable: true
        });
    }
}

Here's a live example using both, try this on (for instance) IE11 (although it will only demonstrate forEach), on which NodeList doesn't have these features natively:

// Using only ES5 features so this runs on IE11
function log() {
    if (typeof console !== "undefined" && console.log) {
        console.log.apply(console, arguments);
    }
}
if (typeof NodeList !== "undefined" && NodeList.prototype) {
    // forEach
    if (!NodeList.prototype.forEach) {
        // Yes, there's really no need for `Object.defineProperty` here
        console.log("Added forEach");
        NodeList.prototype.forEach = Array.prototype.forEach;
    }
    // Iterability
    if (typeof Symbol !== "undefined" && Symbol.iterator && !NodeList.prototype[Symbol.iterator]) {
        console.log("Added Symbol.iterator");
        Object.defineProperty(NodeList.prototype, Symbol.iterator, {
            value: Array.prototype[Symbol.itereator],
            writable: true,
            configurable: true
        });
    }
}

log("Testing forEach");
document.querySelectorAll(".container div").forEach(function(div) {
    var html = div.innerHTML;
    div.innerHTML = html[0].toUpperCase() + html.substring(1).toLowerCase();
});

// Iterable
if (typeof Symbol !== "undefined" && Symbol.iterator) {
    // Using eval here to avoid causing syntax errors on IE11
    log("Testing iterability");
    eval(
        'for (const div of document.querySelectorAll(".container div")) { ' +
        '    div.style.color = "blue"; ' +
        '}'
    );
}
<div class="container">
  <div>one</div>
  <div>two</div>
  <div>three</div>
  <div>four</div>
</div>

The HTMLCollection returned by getElementsByTagName (and various other older APIs) isn't defined as iterable, but if you like, you can also do this for HTMLCollection as well. Here's a loop doing boty NodeList (if necessary) and HTMLCollection (if necessary):

for (const ctor of [typeof NodeList !== "undefined" && NodeList, typeof HTMLCollection !== "undefined" && HTMLCollection]) {
    if (ctor && ctor.prototype && !ctor.prototype.forEach) {
        // (Yes, there's really no need for `Object.defineProperty` here)
        ctor.prototype.forEach = Array.prototype.forEach;
        if (typeof Symbol !== "undefined" && Symbol.iterator && !ctor.prototype[Symbol.iterator]) {
            Object.defineProperty(ctor.prototype, Symbol.iterator, {
                value: Array.prototype[Symbol.itereator],
                writable: true,
                configurable: true
            });
        }
    }
}

Just beware that HTMLCollection is live, so changes you make to the DOM which affect what's in the collection get reflected in the collection immediately, which could result in surprising behavior. (NodeList is a disconnected collection, so that behavior doesn't happen.)

OK, let's start from here, in JavaScript, we have some cases which we call it Array-like, means even it looks like an array, it's not a real array...

For example arguments in function or in your case Nodelist...

Even All modern browsers understand which you'de like to change it to Array and work well, in IE and some other browsers it's not supported using array functions on Nodelist for example...

So if you supporting broad range of browsers, it's better to convert them to an array before doing any activity on them...

There are few ways to convert Array-like values to real Array...

One widely used in ES5 is this structure:

Array.prototype.slice.call(YourNodeList);

So you can do:

var allDivs = document.querySelectorAll("div");
var allRealDivsArray = Array.prototype.slice.call(allDivs);

But if you using ES6, there are even neater ways to do it, just make sure you convert them to ES5 using babel for example as those old browsers which not supporting looping over array-like, won't support ES6 as well for sure...

Two very common ways to do them are:

1) Using Array.from

const allDivs = document.querySelectorAll("div");
const allRealDivsArray = Array.from(allDivs);

2) Using [...Array]

const allDivs = document.querySelectorAll("div");
const allRealDivsArray = [...allDivs];
jERCle

While it may look like an array, it's actually a NodeList which doesn't have the same features as an array. Use a for loop instead

color_btns = document.querySelectorAll('#color > p'); 

for (var i = 0; i < color_btns.length; i++) {
    color_btns[i].onclick = function () { 
        for (var j = 0; j < color_btns.length; j++) {
            if(color_btns[j].classList.contains('selected')) { 
                color_btns[j].classList.remove('selected');
            }
        }
    color_btns[i].classList.add('selected'); 
    document.querySelector('#f_color').value = color_btns[i].dataset.id;
    };
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!