var array = [];
document.querySelectorAll("a").forEach(array.push); //Uncaught TypeError: Cannot convert undefined or null to object
Why does this fail? what does this error mean? It seems perfectly reasonable to pass an array method as a function, what am I not understanding/seeing?
array.push loses context (its this), so you need to pass a function with context captured.
But, even if what you wanted worked - you still would not get the result you want, since NodeList:forEach passes 3 arguments to the callback function, so you would fill your array with elements, indexes and the list of nodes.
So a solution would be to do
var array = [];
document.querySelectorAll("a").forEach(e => array.push(e));
You can rebind the this context
var arr = []; document.querySelectorAll("a").forEach(arr.push.bind(arr));
console.log(arr);
one
two
three
BUT the big issue here is the fact that push() when given multiple arguments, will add all of those arguments to the array.
So push(1,2,3) will add 1, 2, and 3 to the array. So the above code would have 3 links, but it will add 9 entries into the array because forEach has 3 arguments element, index, array. So you will need to use a function to do it. Or just use Array.from() to create the array.
var arr = [];
document.querySelectorAll('a').forEach(el => arr.push(el))
console.log(arr);
var arr2 = Array.from(document.querySelectorAll('a'))
console.log(arr2);
one
two
three
The problem at first instance is not passing array.push, the problem is you are iterating over a NodeLists and this structure is not an array, you can use Array.from in next way:
const array =Array.from(document.querySelectorAll("a"));
When you extract a function from an object, it loses its context, so when you call it and it accesses this, its original value has been lost.
To fix the issue you need to use Function.prototype.bind() to keep the this reference pointing to the right object.
You can see the problem and how bind works in this example:
const obj = {
prop: 'there',
print(prefix) {
console.log(`${ prefix }: Hello ${ this.prop }.`);
}
};
obj.print('obj.print');
// Context lost:
const extractedPrint = obj.print;
extractedPrint('extractedPrint');
// Context preserved to the original object:
const bindedPrint = obj.print.bind(obj);
bindedPrint('bindedPrint');
// Context replaced:
const alsoBindedPrint = obj.print.bind({ prop: 'bro' });
alsoBindedPrint('alsoBindedPrint');
Wondering where is this pointing when it's "lost"? It points to window:
const obj = {
prop: 'there',
print(prefix) {
console.log(`${ prefix }: Hello ${ this.prop }.`);
}
};
const extractedPrint = obj.print;
window.prop = 'window';
extractedPrint('extractedPrint');
In your case, you need to make sure that when push is called by forEach its context is preserved, that is, its this value should still be referencing the original array:
links.forEach(array.push.bind(array));
Anyway, that won't work as expected because NodeList.prototype.forEach() calls its callback with 3 arguments: currentValue, currentIndexand listObj and Array.prototype.push() accepts multiple arguments at once, so you could do:
const array = [];
const links = document.querySelectorAll('a');
links.forEach(array.push.bind(array));
console.log(array.length);
<a>1</a>
<a>2</a>
<a>3</a>
But for each Node or your NodeList, you would be calling push with 3 arguments instead of 1, ending up getting some unwanted elements on the list.
To convert a NodeList to an Array you could use Array.from() instead:
console.log(Array.from(document.querySelectorAll('a')).length);
<a>1</a>
<a>2</a>
<a>3</a>
Although there are other ways to do it, like pushing all the elements one by one defining your own callback:
const links = document.querySelectorAll('a');
const arr = [];
// Note we only define a single argument, so we ignore the other 2:
links.forEach((link) => {
arr.push(link);
});
console.log(arr);
<a>1</a>
<a>2</a>
<a>3</a>
Or the the same thing using a loop:
const links = document.querySelectorAll('a');
const arr = [];
for (const link of links) {
arr.push(link);
}
// Also valid:
// for (let i = 0; i < links.length; ++i) {
// arr.push(links[i]);
// }
console.log(arr);
// Note this won't work, as NodeList has some other iterable
// properties apart from the indexes:
const arr2 = [];
for(const i in links) {
arr2.push(links[i])
}
console.log(arr2);
<a>1</a>
<a>2</a>
<a>3</a>
I'm not 100% sure what you're trying to do, but I'm going to guess that you'd like to push the elements returned by querySelectorAll into 'array'. In which case, you can't just pass the function push to forEach, you must call push on array with each element as the argument, like so:
document.querySelectorAll("a").forEach(item => array.push(item));
What are you trying to push in your example?
Maybe you are missing some parameters?
Try it out:
document.querySelectorAll("a").forEach(function(item) {
array.push(item);
});
Related
Are there any substantial reasons why modifying Array.push() to return the object pushed rather than the length of the new array might be a bad idea?
I don't know if this has already been proposed or asked before; Google searches returned only a myriad number of questions related to the current functionality of Array.push().
Here's an example implementation of this functionality, feel free to correct it:
;(function() {
var _push = Array.prototype.push;
Array.prototype.push = function() {
return this[_push.apply(this, arguments) - 1];
}
}());
You would then be able to do something like this:
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
}
someFunction(value, someArray.push({}));
Where someFunction modifies the object passed in as the second parameter, for example. Now the contents of someArray are [{"someKey": "hello world"}].
Are there any drawbacks to this approach?
See my detailed answer here
TLDR;
You can get the return value of the mutated array, when you instead add an element using array.concat[].
concat is a way of "adding" or "joining" two arrays together. The awesome thing about this method, is that it has a return value of the resultant array, so it can be chained.
newArray = oldArray.concat[newItem];
This also allows you to chain functions together
updatedArray = oldArray.filter((item) => {
item.id !== updatedItem.id).concat[updatedItem]};
Where item = {id: someID, value: someUpdatedValue}
The main thing to notice is, that you need to pass an array to concat.
So make sure that you put your value to be "pushed" inside a couple of square brackets, and you're good to go.
This will give you the functionality you expected from push()
You can use the + operator to "add" two arrays together, or by passing the arrays to join as parameters to concat().
let arrayAB = arrayA + arrayB;
let arrayCD = concat(arrayC, arrayD);
Note that by using the concat method, you can take advantage of "chaining" commands before and after concat.
Are there any substantial reasons why modifying Array.push() to return the object pushed rather than the length of the new array might be a bad idea?
Of course there is one: Other code will expect Array::push to behave as defined in the specification, i.e. to return the new length. And other developers will find your code incomprehensible if you did redefine builtin functions to behave unexpectedly.
At least choose a different name for the method.
You would then be able to do something like this: someFunction(value, someArray.push({}));
Uh, what? Yeah, my second point already strikes :-)
However, even if you didn't use push this does not get across what you want to do. The composition that you should express is "add an object which consist of a key and a value to an array". With a more functional style, let someFunction return this object, and you can write
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
return obj;
}
someArray.push(someFunction(value, {}));
Just as a historical note -- There was an older version of JavaScript -- JavaScript version 1.2 -- that handled a number of array functions quite differently.
In particular to this question, Array.push did return the item, not the length of the array.
That said, 1.2 has been not been used for decades now -- but some very old references might still refer to this behavior.
http://web.archive.org/web/20010408055419/developer.netscape.com/docs/manuals/communicator/jsguide/js1_2.htm
By the coming of ES6, it is recommended to extend array class in the proper way , then , override push method :
class XArray extends Array {
push() {
super.push(...arguments);
return (arguments.length === 1) ? arguments[0] : arguments;
}
}
//---- Application
let list = [1, 3, 7,5];
list = new XArray(...list);
console.log(
'Push one item : ',list.push(4)
);
console.log(
'Push multi-items :', list.push(-9, 2)
);
console.log(
'Check length :' , list.length
)
Method push() returns the last element added, which makes it very inconvenient when creating short functions/reducers. Also, push() - is a rather archaic stuff in JS. On ahother hand we have spread operator [...] which is faster and does what you needs: it exactly returns an array.
// to concat arrays
const a = [1,2,3];
const b = [...a, 4, 5];
console.log(b) // [1, 2, 3, 4, 5];
// to concat and get a length
const arrA = [1,2,3,4,5];
const arrB = [6,7,8];
console.log([0, ...arrA, ...arrB, 9].length); // 10
// to reduce
const arr = ["red", "green", "blue"];
const liArr = arr.reduce( (acc,cur) => [...acc, `<li style='color:${cur}'>${cur}</li>`],[]);
console.log(liArr);
//[ "<li style='color:red'>red</li>",
//"<li style='color:green'>green</li>",
//"<li style='color:blue'>blue</li>" ]
var arr = [];
var element = Math.random();
assert(element === arr[arr.push(element)-1]);
How about doing someArray[someArray.length]={} instead of someArray.push({})? The value of an assignment is the value being assigned.
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
}
someFunction(value, someArray[someArray.length]={});
console.log(someArray)
Suppose I have an array var arr = [1,2,3] and if I do var result = arr.filter(callback) I want value of result would be [2,4,6] by the use of filter. I want to only define callback function in order to do so. It can be easily done with map but I want to use only filter.
Array.prototype.filter only filters existing values in an array, effectively creating a new array that can hold the same values, their subset, or an empty array, based on the filtering function.
You could do it using filter():
const arr = [1, 2, 3];
arr.filter((c, i) => {
arr[i] = +arr[i] * 2;
return true;
});
console.log(arr)
we are always returning true, so filter() makes no sense in this case.
As stated many times, there is no reason why you should do it.
It is impossible to do it like map because map returns a new array. You can either alter on the original array or you have to make a clone of the original array so you do not change it.
// this modifies the orginal array
var arr1 = [1,2,3]
arr1.filter((v,index,arr)=>{
arr[index] = v * 2;
return true
})
console.log(arr1)
// or you can clone it and do the same thing
// this modifies the cloned array
var arr2 = [1,2,3]
var arr3 = arr2.slice()
arr3.filter((v,index,arr)=>{
arr[index] = v * 2;
return true
})
console.log(arr2, arr3)
So no, you can not recreate map with filter since you HAVE to modify the original array or cheat and use a copy of the array.
So I'm not sure I understand the second part of your question, but as for the first part:
The callback for filter has three arguments, two of which are optional.
The first argument is the current element in the traversal, and the second and third arguments (the optional ones) are the 0-based index of the current element, and a reference to the original array.
This third parameter is useful for what you're trying to do.
let myArr = [1, 2, 3];
myArr.filter((el, ind, orig) => {
orig[ind] = orig[ind] + 1; // Or, whatever map logic you want to use
return true; // since a Boolean condition must be returned here
});
This way you can do it without even even having to break the scope of the function!
If you want to do it without necessarily having a variable to originally call filter on (you do have to pass an array), you can use the prototype and the call method:
Array.prototype.filter.call([1, 2, 3], (el, ind, orig) => {
orig[ind] = orig[ind] + 1;
return true;
});
I created an array with many elements with a loop:
myArray = [c1, c2, c3...]
now I want to make each element into an object and assign different key values:
c1 = {image = path, value = number)
I tried to run forEach() but can't figure out the correct way to do so and I have not succeeded in finding the answer to it.
My guess was:
myArray.forEach(function() {
let name = {
image = path,
value = number,
}
return name;
});
but there's no change in the elements in the log.
Any help or link to an answer that can help me here. First time coding here.
UPDATE: an easier solution was to .push all the keys and values of the objects when I created the array with the loop in the first place.
array.push({image: pathx, value: numberx})
You can, but you'd be better off with a simple for loop:
for (let i = 0; i < myArray.length; ++i) {
let entry = myArray[i];
myArray[i] = {image: entry.path, value: entry.number};
}
Or making a new array with map.
newArray = myArray.map(entry => ({image: entry.path, value: entry.number}));
Or if you prefer non-arrow functions:
newArray = myArray.map(function(entry) {
return {image: entry.path, value: entry.number};
});
You could theoretically push to a new array but this is the exact usecase for Array#map. Array#map maps old values to new values. The returned object from the callback is the new object and the returned array is the new array containing the new objects.
Semantically, Array#forEach is to plainly iterate over each element in an array, and possibly execute something with side-effects (which may include pushing to another array). But with Array#map, it's specifically used to transform old array values to new ones. Use the one that is specifically designed because it conveys a clear message to anyone else who reads your code.
const newArray = myArray.map(({ path, number }) => ({
image: path,
value: number
}));
Array#map maps old values to new values. You may need to use the follwing instead of arrow functions as it is not supported in IE.
I just added dummy data in the object, you can change it.
myArray = ["c1", "c2", "c3"]
myArray = myArray.map(function(elem) {
return {"image":"path"+elem,"value":"value"+elem};
});
console.log(myArray);
A general question on JavaScript. If I have a function which modifies an array, such as:
var some_array = [];
function a_function(input, array){
//do stuff to array
return array;
}
In most languages:
var some_array = [];
some_array = a_function(input, some_array);
console.log(some_array);
works, however in js the below works as well:
var some_array = [];
a_function(input, some_array);
console.log(some_array);
Is this correct and how is this working?
Arrays in JS are objects and are passed into functions by value, where that value is a reference to the array. In other words, an array passed as an argument to a function still points to the same memory as the outer array.
This means that changing the array contents within the function changes the array passed from outside the function.
function f(arr) {
arr.push(1);
return arr;
}
var array = [];
// both function calls bellow add an element to the same array and then return a reference to that array.
// adds an element to the array and returns it.
// It overrides the previous reference to the array with a
// new reference to the same array.
array = f(array);
console.log(array); // [1]
// adds an element to the array and ignores the returned reference.
f(array);
console.log(array); // [1, 1]
I am just wondering why it is not possible to make forEach on array of undefined.
Code:
var arr = new Array(5); // [undefined x 5]
//ES5 forEach
arr.forEach(function(elem, index, array) {
console.log(index);
});
//underscore each
_.each(arr, function(elem, index, array) {
console.log(index);
});
Both examples do not execute function.
Now to make foreach, I have to make:
var arr = [0,0,0,0,0];
Then make forEach on it.
I am trying to make an array with specified size and loop through it, avoiding for loop. I think that it is clearer using forEach than for loop.
With array with length 5 it is not a problem, but it would be ugly with bigger arrays.
Why there is a problem looping through array of undefined values ?
Array(5) is essentialy equivalent to
var arr = [];
arr.length = 5;
In javascript changing array's length doesn't set any values for it's numeric properties nor does it define those properties in the array object. So numeric properties are undefined instead of having undefined value. You can check it by using:
Object.keys(arr)
When iterating javascript iterates through numeric properties of the array, so if these don't exist, there is nothing to iterate over.
You can check it by doing:
var arr = Array(5)
//prints nothing
arr.forEach(function(elem, index, array) {
console.log(index);
});
arr[3] = 'hey'
//prints only 'hey' even though arr.length === 5
arr.forEach(function(elem, index, array) {
console.log(index);
});
The following code:
var arr = [undefined, undefined];
creates and array of length ===2 and sets the both numeric properties 0 and 1 to undefined.
Looking at a simplified implementation of .forEach() may help.
Array.prototype.my_for_each = function(callback, thisArg) {
for (var i = 0; i < this.length; i++) {
if (i in this) {
callback.call(thisArg, this[i], i, this);
}
}
};
So you can see that what happens is that the method does iterate the entire Array (according to the spec), but it only invokes the callback if the member actually exists. It checks by looking for the property (the index) using the in operator, which tests to see if the object either has or inherits the property.
If in shows that the index exists, the callback is invoked.
So given this Array:
var arr = ["foo", "bar", "baz"];
This will output all 3 items:
arr.my_for_each(function(item) {
console.log(item);
});
// "foo" "bar" "baz"
But if we use delete to remove a member, it leaves a hole in the Array, which will now get passed over.
delete arr[1];
arr.my_for_each(function(item) {
console.log(item);
});
// "foo" "baz"
When you created an Array using Array(5), it created one without any members, but with the .length set to 5. So this is an example of a sparse Array (very sparse in this instance). Because none of the indices will be found by in, the callback is never invoked.
You can use Array.from to create an array and pass lambda function that will be invoked on each item in the array.
detailed documentation
const arr = Array.from(
{ length: 5 },
() => 0
)
console.log(arr)
Other answers have explained the problem, but not provided solutions.
ES6 Spread syntax fills in sparse values with undefined. So does Array.apply(null, sparseArray), which has the benefit of working in older browsers, but takes a bit more work to understand.
const sparseArray = new Array(5);
const unsparseArray1 = Array.apply(null, sparseArray);
const unsparseArray2 = [...sparseArray];
function arrayApply() {
// ES5 forEach works with either approach
unsparseArray1.forEach(function(elem, index, array) {
console.log(index);
});
}
function es6Spread() {
// Lodash each works with either approach
_.each(unsparseArray2, function(elem, index, array) {
console.log(index);
});
}
<html><head>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
<title>Making sparse arrays unsparse</title>
</head><body>
<p><button onclick="arrayApply();">Array.apply(null, sparseArray)</button></p>
<p><button onclick="es6Spread();">[...sparseArray]</button></p>
</body>
</html>
In my case, I was looking for an elegant solution to creating an array of digits starting with 0 to X.
In an elegant manner with arrow functions, it comes up with a 1 line of code
const arrayLength = 10;
const arrayOfDigits = Array.apply(null, Array(arrayLength)).map((_, index) => index);
Appeared to me quite a sophisticated one, much more than a separate block of code with a for cycle.