I am attempting to merge two arrays of objects to so I can validate a form. The usual concat method does not appear to work in this circumstance. Concat works with ordinary numerical and string arrays but doesn't with object arrays. The line var allTags = allInputs.concat(allSelects); does not work.
var allInputs = document.getElementsByTagName("input");
alert("Inputs: " + allInputs.length);
var allSelects = document.getElementsByTagName("select");
alert("Selects: " + allSelects.length);
var allTags = allInputs.concat(allSelects);
alert("allTags: " + allTags.length);
Concat works with ordinary numerical and string arrays but doesn't with object arrays.
Actually, it does, but NodeList instances don't have a concat method, and Array#concat doesn't have a means of identifying that you want to flatten those (because they're not arrays).
But it's still fairly easy to do (see caveat below, though). Change this line:
var allTags = allInputs.concat(allSelects);
to
var allTags = [];
allTags.push.apply(allTags, allInputs);
allTags.push.apply(allTags, allSelects);
Live Example | Source
That works by using a bit of a trick: Array#push accepts a variable number of elements to add to the array, and Function#apply calls the function using the given value for this (in our case, allTags) and any array-like object as the arguments to pass to it. Since NodeList instances are array-like, push happily pushes all of the elements of the list onto the array.
This behavior of Function#apply (not requiring the second argument to really be an array) is very clearly defined in the specification, and is well-supported in modern browsers.
Sadly, IE6 and 7 don't support the above (I think it's specifically using host objects — NodeLists — for Function#apply's second argument), but then, we shouldn't be supporting them, either. :-) IE8 doesn't, either, which is more problematic. IE9 is happy with it.
If you need to support IE8 and earlier, sadly, I think you're stuck with a boring old loop:
var allInputs = document.getElementsByTagName('input');
var allSelects = document.getElementsByTagName('select');
var allTags = [];
appendAll(allTags, allInputs);
appendAll(allTags, allSelects);
function appendAll(dest, src) {
var n;
for (n = 0; n < src.length; ++n) {
dest.push(src[n]);
}
return dest;
}
Live Example | Source
That does work on IE8 and earlier (and others).
document.get[something] returns a NodeList, which looks very similar to an Array, but has numerous distinctive features, two of which are:
It does not contain the concat method
The list of items is live. This means they change as the document changes in real time.
If you don't have any issues with turning your NodeLists into actual arrays, you could do the following to achieve the effect you desire:
var allInputs = document.getElementsByTagName("input")
, allSelects = document.getElementsByTagName("select")
;//nodeLists
var inputList = makeArray(allInputs)
, selectList = makeArray(allSelects)
;//nodeArrays
var combined = inputList.concat(selectList);
function makeArray(list){
return Array.prototype.slice.call(list);
}
You will lose any and all behaviors of the original NodeList, including it's ability to update in real time to reflect changes to the DOM, but my guess is, that's not really an issue.
What you're dealing with are HTMLCollections, and they're live collections (i.e., when you add a node to the DOM, it's automatically added to the collection.
And sadly, it doesn't have a concat method... for a reason. What you need to do is to convert it to an array, losing its live behaviour:
var allInputsArray = [].slice.call(allInputs),
allSelectsArray = [].slice.call(allSelects),
allFields = allInputsArray.concat(allSelectsArray);
I found a simple way to collect dom objects in the order they appear on a page or form.
Firstly, create a dummy class style 'reqd' on the object then use the following which creates a list in the order they appear on the page. Just add the class to any objects you wish to collect.
var allTags = document.querySelectorAll("input.reqd, select.reqd");
Looked into underscore.js at all? You could do
var allSelectsAndInputs = _.union(_.values(document.getElementsByTagName("input")),_.values(document.getElementsByTagName("select")));
For more information see: http://underscorejs.org/#extend
The function is intended for objects, but works in this setting.
Related
Summary: Is there a faster way to hash objects than JSON.stringify?
Details: I have a Ruby and JavaScript library (NeatJSON) that provides pretty-printing of JavaScript values. I recently fixed a problem where deeply-nested objects caused O(n!) performance (n being the nesting level) using memoization based on the object being serialized and the indentation amount.
In Ruby, the fix was really easy, because you can index hashes by arrays of unique sets of objects:
build = ->(object,indent) do
memoizer[[object,indent]] ||= <all the rest of the code>
end
In JavaScript, however, I can't index an object by another object (in a unique way). Following the lead of several articles I found online, I decide to fix the problem generically, using JSON.stringify on the full set of arguments to the function to create a unique key for memoization:
function memoize(f){
var memo = {};
var slice = Array.prototype.slice;
return function(){
var args = slice.call(arguments);
var mkey = JSON.stringify(args);
if (!(mkey in memo)) memo[mkey] = f.apply(this,args);
return memo[mkey];
}
}
function rawBuild(o,indent){ .. }
var build = memoize(rawBuild);
This works, but (a) it's a little slower than I'd like, and (b) it seems wildly inefficient (and inelegant) to perform (naive) serialization of every object and value that I'm about to serialize smartly. The act of serializing a large object with many values is going to store a string and formatting result for EVERY unique value (not just leaf values) in the entire object.
Is there a modern JavaScript trick that would let me uniquely identify a value? For example, some way of accessing an internal ID, or otherwise associating complex objects with unique integers that takes O(1) time to find the identifier for a value?
If you are looking to memoise your objects by identity (not by content), then you'll want to use a WeakMap which is designed for exactly this purpose. They don't work for primitive values though, so you'll need a different solution for such arguments.
Using #Bergi's suggestion of a WeakMap I found out about Map, which allows using any value type as the key (not just objects). Because I needed a compound key—uniquely memoizing the combination of the value passed in and the indentation string—I created a hierarchical memoization structure:
function memoizedBuild(){
var memo = new Map;
return function(value,indent){
var byIndent=memo.get(value);
if (!byIndent) memo.set(value,byIndent={});
if (!byIndent[indent]) byIndent[indent] = rawBuild(value,indent);
return byIndent[indent];
}
}
This proved to be about 4× faster than the memoization code I had been using when serializing a large 270kB JSON object.
Note that in the above code I'm able to use !byIndent[indent] only because I know that rawBuild will never return a falsey value (null, undefined, false, NaN, 0, ""). The safer code line would look something like:
if (!(indent in byIndent)) byIndent[indent] = rawBuild(value,indent);
If you just need to memoise objects then it makes sense to assign some unique ID to your objects .
var gID = 0;
function createNode() {
var obj = ...
obj.id = (++gID).toString();
}
and use those obj.id's as keys in your memo collection.
That would be fastest and least greedy solution.
Update:
If you want that id property to do not clash with existing properties
then you can create non-enumerable properties using standard ES5.1 Object.createProperty() (with some unique name) or to use ES6 symbols:
var gID = 0;
var gUidSym = Symbol("uid");
function getUidOf(obj) {
return obj[gUidSym]
|| (obj[gUidSym] = (++gID).toString());
}
I am working with an svg file which has a number of text elements within it. The text elements are all numbers. I am able to get the list of values and put them into an array with the following line of code.
var fretdata = document.getElementById("fretinformation").getElementsByTagName("text");
I am able to access .length property and also the access the array elements by index such as [0].textContent. However, when I try to use the .indexOf() function on the array, I receive an error message that the object (my array) does not support the property or method of indexOf.
I am able to setup a for loop to iterate through the array checking each value looking for the presence or absence of a certain value. I would like something with the simplicity of the indexOf functionality which tells me whether or not something is present within the array and where it is if present. Is there a to get .indexOf() working with the svg text element array? Or is there a similar alternative which does not require the use of loops and flags?
I think the problem lies in the fact that I have an array of text elements and not an array of strings. But I'm not sure how to directly get the array of the text element's textContent
var fretdata = document.getElementById("fretinformation").getElementsByTagName("text");
//var fretdata = document.getElementById("fretinformation").getElementsByTagName("text").textcontent;
//18th fret is the upper fret limit
//0 fret (open string) is the lower fret limit
//var zerolocation=fretdata.indexOf("0");
for (fd=0;fd<fretdata.length;fd++){
if(fretdata[fd].textContent=="0"){
document.getElementById("downkey").setAttribute("onclick",null);
document.getElementById("downkey").getElementsByTagName("polygon")[0].style.fill="#D3D3D3";
}
}
Iterating in the loop works. The two lines commented out using the .indexOf do not.
Thanks, --christopher
What you have is not an array, it's a nodeList.
A nodeList has length, and is array-like, but array methods like indexOf, forEach etc. doesn't work on nodeLists.
You can convert a nodeList to an array like this
var array = Array.prototype.slice.call(fretdata);
but in your case you really shouldn't, you should stick to the iteration instead.
Iterating the elements is really an option, but if you don't like it, you may have 2 more options depending on your setup:
The following code requires map function (check compatibility here, it basically requires IE9+) and slice function compatibility (same, IE9+).
var fretdata = document.getElementById("fretinformation").getElementsByTagName("text");
alert([].slice.call(fretdata).map(function(o) { return o.textContent; }).indexOf("1"));
The other one requires jQuery, it handles a lot of stuff for you.
alert($( "#fretinformation > text" ).filter(function() { return $(this).text() === "1"; } ).length);
use ES6 spread operators
var fretdata = [...(document.getElementById("fretinformation").getElementsByTagName("text"))]
This internally works as
var array = Array.prototype.slice.call(fretdata);
I have the following javascript code:
var objectArray = [];
var allInputObjects = [];
var allSelectObjects = [];
var allTextAreaObjects = [];
//following returns 3 objects
allInputObjects = document.getElementById("divPage0").getElementsByTagName("INPUT");
//following returns 1 object
allSelectObjects = document.getElementById("divPage1").getElementsByTagName("SELECT");
//following returns 0 objects
allTextAreaObjects = document.getElementById("divPage2").getElementsByTagName("TEXTAREA");
//and following statement does not work
objectArray = allInputObjects.concat(allSelectObjects);
And my problem is that the last line is throwing an error.
I tried the above code in Firefox and it says allInputObjects.concat is not a function.
Any clues, I believe the script is not treating allInputObjects as an Array!
Any help will be appreciated.
getElementsByTagName returns a NodeList, which is similar to an Array except that it does not support all those prototype functions.
To seamlessly convert such an array-like object into an Array, use:
var arr = Array.prototype.slice.call(somenodelist, 0);
arr will almost be identical, except that it now has the Array prototype functions supported, like concat.
What the function actually does is returning a partial Array containing the elements of somenodelist, to be precise everything from index 0 and after. Obviously, this are just all elements, therefore this is a trick to convert array-like objects into real Arrays.
Why do you think that allSelectObjects is an array?
It is initially assigned to an empty array, sure. But then it's overwritten by the getElementsByTagName call on line 6(ish). Variables in Javascript are not strongly typed, so the initial assignment does not force later assignments to also be arrays.
I suspect you'll have some type of NodeList or similar, rather than an array, in the variable when you invoke the final line. (That's what's returned by the method in Firefox, at least.)
As Andezej has pointed out, you're not dealing with an array here, you're dealing with a kind of node list.
Here's one way you can create an array to work with based on the result of getElementsByTagName
var tags = obj.getElementsByTagName('input');
for (var j=0;j<tags.length;j++) {
resultArray.push(tags[j]);
}
Just another funny way to convert your NodeList to an array:
var tags = obj.getElementsByTagName('input'),
tags2Arr = (
function toArr(i){
return i ? toArr(i-1).concat(tags[i]) : [tags[0]];
}(tags.length-1)
);
Now if you add a method to the Array.prototype:
Array.prototype.clone = function() {
var arr = this;
return (function clone(i){
return i ? clone(i-1).concat(arr[i]) : [arr[0]];
})(this.length-1);
};
You can convert a NodeList to an array, using this oneliner:
Array.prototype.clone.call(obj.getElementsByTagName('input'));
Is there a more efficient way to convert an HTMLCollection to an Array, other than iterating through the contents of said collection and manually pushing each item into an array?
var arr = Array.prototype.slice.call( htmlCollection )
will have the same effect using "native" code.
Edit
Since this gets a lot of views, note (per #oriol's comment) that the following more concise expression is effectively equivalent:
var arr = [].slice.call(htmlCollection);
But note per #JussiR's comment, that unlike the "verbose" form, it does create an empty, unused, and indeed unusable array instance in the process. What compilers do about this is outside the programmer's ken.
Edit
Since ECMAScript 2015 (ES 6) there is also Array.from:
var arr = Array.from(htmlCollection);
Edit
ECMAScript 2015 also provides the spread operator, which is functionally equivalent to Array.from (although note that Array.from supports a mapping function as the second argument).
var arr = [...htmlCollection];
I've confirmed that both of the above work on NodeList.
A performance comparison for the mentioned methods: http://jsben.ch/h2IFA
not sure if this is the most efficient, but a concise ES6 syntax might be:
let arry = [...htmlCollection]
Edit: Another one, from Chris_F comment:
let arry = Array.from(htmlCollection)
I saw a more concise method of getting Array.prototype methods in general that works just as well. Converting an HTMLCollection object into an Array object is demonstrated below:
[].slice.call( yourHTMLCollectionObject );
And, as mentioned in the comments, for old browsers such as IE7 and earlier, you simply have to use a compatibility function, like:
function toArray(x) {
for(var i = 0, a = []; i < x.length; i++)
a.push(x[i]);
return a
}
I know this is an old question, but I felt the accepted answer was a little incomplete; so I thought I'd throw this out there FWIW.
For a cross browser implementation I'd sugguest you look at prototype.js $A function
copyed from 1.6.1:
function $A(iterable) {
if (!iterable) return [];
if ('toArray' in Object(iterable)) return iterable.toArray();
var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}
It doesn't use Array.prototype.slice probably because it isn't available on every browser. I'm afraid the performance is pretty bad as there a the fall back is a javascript loop over the iterable.
This works in all browsers including earlier IE versions.
var arr = [];
[].push.apply(arr, htmlCollection);
Since jsperf is still down at the moment, here is a jsfiddle that compares the performance of different methods. https://jsfiddle.net/qw9qf48j/
To convert array-like to array in efficient way we can make use of the jQuery makeArray :
makeArray: Convert an array-like object into a true JavaScript array.
Usage:
var domArray = jQuery.makeArray(htmlCollection);
A little extra:
If you do not want to keep reference to the array object (most of the time HTMLCollections are dynamically changes so its better to copy them into another array, This example pay close attention to performance:
var domDataLength = domData.length //Better performance, no need to calculate every iteration the domArray length
var resultArray = new Array(domDataLength) // Since we know the length its improves the performance to declare the result array from the beginning.
for (var i = 0 ; i < domDataLength ; i++) {
resultArray[i] = domArray[i]; //Since we already declared the resultArray we can not make use of the more expensive push method.
}
What is array-like?
HTMLCollection is an "array-like" object, the array-like objects are similar to array's object but missing a lot of its functionally definition:
Array-like objects look like arrays. They have various numbered
elements and a length property. But that’s where the similarity stops.
Array-like objects do not have any of Array’s functions, and for-in
loops don’t even work!
This is my personal solution, based on the information here (this thread):
var Divs = new Array();
var Elemns = document.getElementsByClassName("divisao");
try {
Divs = Elemns.prototype.slice.call(Elemns);
} catch(e) {
Divs = $A(Elemns);
}
Where $A was described by Gareth Davis in his post:
function $A(iterable) {
if (!iterable) return [];
if ('toArray' in Object(iterable)) return iterable.toArray();
var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}
If browser supports the best way, ok, otherwise will use the cross browser.
I suppose that calling Array.prototype functions on instances of HTMLCollection is a much better option than converting collections to arrays (e.g.,[...collection] or Array.from(collection)), because in the latter case a collection is unnecessarily implicitly iterated and a new array object is created, and this eats up additional resources. Array.prototype iterating functions can be safely called upon objects with consecutive numeric keys starting from [0] and a length property with a valid number value of such keys' quantity (including, e.g., instances of HTMLCollection and FileList), so it's a reliable way. Also, if there is a frequent need in such operations, an empty array [] can be used for quick access to Array.prototype functions; or a shortcut for Array.prototype can be created instead. A runnable example:
const _ = Array.prototype;
const collection = document.getElementById('ol').children;
alert(_.reduce.call(collection, (acc, { textContent }, i) => {
return acc += `${i+1}) ${textContent}` + '\n';
}, ''));
<ol id="ol">
<li>foo</li>
<li>bar</li>
<li>bat</li>
<li>baz</li>
</ol>
Sometimes, Even You have written code the correct way, But still it doesn't work properly.
var allbuttons = document.getElementsByTagName("button");
console.log(allbuttons);
var copyAllButtons = [];
for (let i = 0; i < allbuttons.length; i++) {
copyAllButtons.push(allbuttons[i]);
}
console.log(copyAllButtons);
you get empty array.
Like, This
HTMLCollection []
[]
Console_javascript
For Solving this problem, You have to add link of javascript file after body tag in html file.
<script src="./script.js"></script>
As you can see below,
html_file
Final Output
HTMLCollection(6) [button.btn.btn-dark.click-me, button.btn.btn-dark.reset, button#b, button#b, button#b, button#b, b: button#b]
(6) [button.btn.btn-dark.click-me, button.btn.btn-dark.reset, button#b, button#b, button#b, button#b]
What could be the problem with reversing the array of DOM objects as in the following code:
var imagesArr = new Array();
imagesArr = document.getElementById("myDivHolderId").getElementsByTagName("img");
imagesArr.reverse();
In Firefox 3, when I call the reverse() method the script stops executing and shows the following error in the console of the Web Developer Toolbar:
imagesArr.reverse is not a function
The imagesArr variable can be iterated through with a for loop and elements like imagesArr[i] can be accessed, so why is it not seen as an array when calling the reverse() method?
Because getElementsByTag name actually returns a NodeList structure. It has similar array like indexing properties for syntactic convenience, but it is not an array. For example, the set of entries is actually constantly being dynamically updated - if you add a new img tag under myDivHolderId, it will automatically appear in imagesArr.
See http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-536297177 for more.
getElementsByTag() returns a NodeList instead of an Array. You can convert a NodeList to an Array but note that the array will be another object, so reversing it will not affect the DOM nodes position.
var listNodes = document.getElementById("myDivHolderId").getElementsByTagName("img");
var arrayNodes = Array.slice.call(listNodes, 0);
arrayNodes.reverse();
In order to change the position, you will have to remove the DOM nodes and add them all again at the right position.
Array.prototype.slice.call(arrayLike, 0) is a great way to convert an array-like to an array, but if you are using a JavaScript library, it may actually provide a even better/faster way to do it. For example, jQuery has $.makeArray(arrayLike).
You can also use the Array methods directly on the NodeList:
Array.prototype.reverse.call(listNodes);
this problem can Actually be solved easily with array spread operator.
let elements = document.querySelectorAll('button');
elements = [...elements];
console.log(elements) // Before reverse
elements = elements.reverse(); // Now the reverse function will work
console.log(elements) // After reverse
<html>
<body>
<button>button1</button>
<button>button2</button>
<button>button3</button>
<button>button4</button>
<button>button5</button>
</body>
</html>
getElementsByTag() returns a NodeList instead of an Array. You need to convert the NodeList to an array then reverse it.
var imagesArr = [].slice.call(document.getElementById("myDivHolderId").getElementsByTagName("img"), 0).reverse();
I know this question is old but I think it needs a bit of clarification as some of the answers here are outdated as W3C changed the definition, and consequently the return value of these methods getElementsByTagName() and getElementsByClassName()
These methods as of the time of writing this answer return an object - empty or not - of type HTMLCollection and not NodeList.
It's like the difference between the properties children which returns an object of type HTMLCollection since it's only composed of elements and excluding text or comment nodes, and childNodes which returns an object of type NodeList since it could contain other node types like text and comments as well.
Note: I'd go on tangent here and express my lack of insight on why querySelectorAll() method currently returns a NodeList and not an HTMLCollection since it exclusively works on element nodes in the document and nothing else.
Probably it has something to do with potential coverage of other node types in the future and they went for a more future proof solution, who knows really? :)
EDIT: I think I got the rationale behind this decision to opt for a NodeList and not an HTMLCollection for the querySelectorAll().
Since they constructed HTMLCollection to be exclusively and entirely live and since this method doesn't need this live functionality, they decided for a NodeList implementation instead to best serve its purpose economically and efficiently.
Your first line is irrelevant, since it doesn't coerce the assignment to the variable, javascript works the other way. imagesArr, is not of Type Array(), its of whatever the return type of getElementsByTagName("img") is. In this case, its an HtmlCollection in Firefox 3.
The only methods on this object, are the indexers, and length. In order to work in reverse, just iterate backwards.
This worked for me, I did a reverse for loop and allocated the nodes to an array
var Slides = document.getElementById("slideshow").querySelectorAll('li');
var TempArr = [];
for (var x = Slides.length; x--;) {
TempArr.push(Slides[x]);
}
Slides = TempArr;