admin管理员组

文章数量:1422446

I'm trying to write a bookmarklet that works on two different pages. I can successfully iterate through and process the elements once I get hold of them, but in order to acmodate the two different pages, I wanted to pile a list of DIVs by two different class names. I started with this:

traces=
  [].concat.apply(document.getElementsByClassName('fullStacktrace'),
                  document.getElementsByClassName('msg'))

but on the first page it results this:

> traces=[].concat.apply(document.getElementsByClassName('fullStacktrace'),
                         document.getElementsByClassName('msg'))
[HTMLCollection[2]]
> traces[0]
[div.fullStacktrace, div.fullStacktrace]
> traces[1]
undefined
> traces[0][0]
<div class=​"fullStacktrace" style=​"height:​ auto;​">​…​</div>​

whereas on the other page

> traces=[].concat.apply(document.getElementsByClassName('fullStacktrace'),
                         document.getElementsByClassName('msg'))
[HTMLCollection[0], div#msg_2.msg.l1,  ... div#msg_6460.msg.l1]
> traces[0]
[]
> traces[1]
<div class=​"msg l1" id=​"msg_2">​…​</div>​

So getElementsByClassName works for both pages, but it looks like concat.apply doesn't iterate its first argument, but does iterate its second argument?!

So I tried using concat twice and going all out with parentheses:

traces=(
  (
    [].concat.apply(document.getElementsByClassName('fullStacktrace'))
  )
  .concat.apply(document.getElementsByClassName('msg'))
)

but it gets even stranger: the first page said:

Array[1]
> 0: HTMLCollection[0]
  length: 0
  > __proto__: HTMLCollection
length: 1
> __proto__: Array[0]

and the other:

Array[1]
> 0: HTMLCollection[283]  // Lots of div#msg_3.msg.l1 sort of things in here
  length: 1
>__proto__: Array[0]

get its full plement of divs.

Since the two groups of elements are only on one or the other page, I can just use a conditional in my particular case, but the behaviour above is very surprising to me, so I would like to understand it. Any ideas?

For reference, this is on Mac Chrome Version 56.0.2924.87 (64-bit)

I'm trying to write a bookmarklet that works on two different pages. I can successfully iterate through and process the elements once I get hold of them, but in order to acmodate the two different pages, I wanted to pile a list of DIVs by two different class names. I started with this:

traces=
  [].concat.apply(document.getElementsByClassName('fullStacktrace'),
                  document.getElementsByClassName('msg'))

but on the first page it results this:

> traces=[].concat.apply(document.getElementsByClassName('fullStacktrace'),
                         document.getElementsByClassName('msg'))
[HTMLCollection[2]]
> traces[0]
[div.fullStacktrace, div.fullStacktrace]
> traces[1]
undefined
> traces[0][0]
<div class=​"fullStacktrace" style=​"height:​ auto;​">​…​</div>​

whereas on the other page

> traces=[].concat.apply(document.getElementsByClassName('fullStacktrace'),
                         document.getElementsByClassName('msg'))
[HTMLCollection[0], div#msg_2.msg.l1,  ... div#msg_6460.msg.l1]
> traces[0]
[]
> traces[1]
<div class=​"msg l1" id=​"msg_2">​…​</div>​

So getElementsByClassName works for both pages, but it looks like concat.apply doesn't iterate its first argument, but does iterate its second argument?!

So I tried using concat twice and going all out with parentheses:

traces=(
  (
    [].concat.apply(document.getElementsByClassName('fullStacktrace'))
  )
  .concat.apply(document.getElementsByClassName('msg'))
)

but it gets even stranger: the first page said:

Array[1]
> 0: HTMLCollection[0]
  length: 0
  > __proto__: HTMLCollection
length: 1
> __proto__: Array[0]

and the other:

Array[1]
> 0: HTMLCollection[283]  // Lots of div#msg_3.msg.l1 sort of things in here
  length: 1
>__proto__: Array[0]

get its full plement of divs.

Since the two groups of elements are only on one or the other page, I can just use a conditional in my particular case, but the behaviour above is very surprising to me, so I would like to understand it. Any ideas?

For reference, this is on Mac Chrome Version 56.0.2924.87 (64-bit)

Share Improve this question asked Mar 17, 2017 at 11:52 android.weaselandroid.weasel 3,4312 gold badges34 silver badges42 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 7

To use concat, the collections would need to be arguments that are Arrays. Any other argument won't get flattened. You can use .slice() for this:

traces= [].concat([].slice.call(document.getElementsByClassName('fullStacktrace')),
                  [].slice.call(document.getElementsByClassName('msg')))

Modern syntax would make it quite a bit nicer. This uses the "spread" syntax to create a new array with the content of both collections distributed into it:

traces = [...document.getElementsByClassName('fullStacktrace'),
          ...document.getElementsByClassName('msg')]

Or to use something that is more easily polyfilled, you can use Array.from() to convert the collections:

traces = Array.from(document.getElementsByClassName('fullStacktrace'))
              .concat(Array.from(document.getElementsByClassName('msg'))))

Overall, I wouldn't use getElementsByClassName in the first place. I'd use .querySelectorAll. You get better browser support and more powerful selection capabilities:

traces = document.querySelectorAll('.fullStacktrace, .msg')

This uses the same selectors that CSS uses, so the above selector is actually passing a group of two selectors, each of which selects the elements with its respective class.


Detailed explanation

First Example:

My explanation of the issue above was too terse. Here's your first attempt:

traces = [].concat.apply(document.getElementsByClassName('fullStacktrace'),
              document.getElementsByClassName('msg'))

The way .concat() works is to take whatever values are given as its this value and its arguments, and bine them into a single Array. However, when the this value or any of the arguments are an actual Array, it flattens its content one level into the result. Because an HTMLCollection isn't an Array, it's seen as just any other value to be added, and not flattened.

The .apply() lets you set the this value of the method being called, and then spread the members of its second argument as individual arguments to the method. So given the above example, the HTMLCollection passed as the first argument to .apply() does not get flattened into the result, but the one passed as the second argument to .apply() does, because it's .apply() doing the spreading, not .concat(). From the perspective of .concat(), the second DOM selection never existed; it only saw individual DOM elements that have the msg class.


Second Example:

traces=(
  (
    [].concat.apply(document.getElementsByClassName('fullStacktrace'))
  )
  .concat.apply(document.getElementsByClassName('msg'))
)

This is a bit different. This part [].concat.apply(document.getElementsByClassName('fullStacktrace')) suffers from the same problem as the first example, but you noticed that the HTMLCollection doesn't end up in the result. The reason is that you actually abandoned the result of that call when you chained .apply.concat(... to the end.

Take a simpler example. When you do this:

[].concat.apply(["bar"], ["baz", "buz"])

...the [] is actually a wasted Array. It's just a short way to get to the .concat() method. Inside .concat() the ["bar"] will be the this value, and "baz" and "buz" will be passed as individual arguments, since again, .apply() spreads out its second argument as individual arguments to the method.

You can see this more clearly if you change the simple example to this:

["foo"].concat.apply(["bar"], ["baz", "buz"])

...notice that "foo" is not in the result. That's because .concat() has no knowledge of it; it only knows if ["bar"], "baz" and "buz".

So when you did this:

[].concat.apply(collection).concat.apply(collection)

You did the same thing. The second .concat.apply basically drops the first and carries on with only the data provided to .apply(), and so the first collection doesn't appear. If you hadn't used .apply for the second call, you'd have ended up with an array with two unflattened HTMLCollections.

[].concat.apply(collection).concat(collection)
// [HTMLCollection, HTMLCollection]

Because the call to document.getElementByClassName returns a HTMLCollection rather than an Array you're getting the strange results you're seeing. What we can do to bat this is convert the HTMLCollection to an array, and then concat them, like this:

traces= [].slice.call(document.getElementsByClassName('fullStacktrace')).concat([].slice.call(document.getElementsByClassName('msg')))

If browser patibility is a concern though, have a look at some of the other solutions from here that may help for earlier versions of IE.

Explanation:

Document.getElementsByClassName returns a live HTMLCollection of elements that match the provided class names. This is an array-like object but not an array.

So your initial attempts were trying to merge two live lists of HTML elements rather than merge arrays of elements.

Suggested Solution:

Create a solid (non-live array) using Array#slice() and then you can merge the two arrays directly:

var fullStacktrace = [].slice.call(document.getElementsByClassName('fullStacktrace'), 0),
  msg = [].slice.call(document.getElementsByClassName('msg'), 0),

  mergedDivs = fullStacktrace.concat(msg);

本文标签: concatenationHow do I concatenate the results of getElementsByClassName in plain JavascriptStack Overflow