This question already has answers here:
Modifying a copy of a JavaScript object is causing the original object to change
(13 answers)
Closed 3 months ago.
Sorry for the strange title, but I've come across an issue that is plain weird. To give some background, i'm working on a booking system that takes a time range as an input from admin, generates available times based on it, and then reduces the available times based on already made bookings (i.e admin specifies availability from 10:00 to 12:00, booking has been made to 11:30, available times will be times = [10:00, 10:30, 11:00, 12:00]).
I have an object that contains per month for each day the available times.
availableTimesPerDay: {
1: ["10:00","10:30","11:00","11:30","12:00"],
2: ["10:00","10:30","11:00","11:30","12:00"],
3: ["10:00","10:30","11:00","11:30","12:00"],
....
}
Where the number represents the date for the given month.
Bookings are represented as an array of objects, format is:
bookedTimes = [
{
date: "2022-12-01T11:30:00.000+02:00"
}
];
I planned to have a function which would iterate through each booking and remove the availability for that time on a given date (based on example above, 11:30 would need to be removed from availableTimesPerDay[1] leaving the value for it as ["10:00","10:30","11:00","12:00"]
The function itself is defined as such:
function reduceAvailableTimesBasedOnDateTime(availableTimesPerDay,bookedTimes){
console.log(JSON.stringify(availableTimesPerDay));
bookedTimes.forEach((bookedDateObject) => {
let bookedDate = new Date(bookedDateObject.date);
// 1
let currentAvailableTimesOnDate = availableTimesPerDay[bookedDate.getDate()];
// ["10:00","10:30","11:00","11:30","12:00"]
let bookedTime = bookedDate.toLocaleTimeString('et');
// "13:30:00"
let time = bookedTime.substring(0,bookedTime.length - 3);
// "13:30"
let index = currentAvailableTimesOnDate.indexOf(time);
// 3
if (index > -1) {
currentAvailableTimesOnDate.splice(index, 1);
// ["10:00","10:30","11:00","12:00"]
}
})
console.log(JSON.stringify(availableTimesPerDay));
return availableTimesPerDay;
}
The way I understand this function is that i've extracted a specific array of available times into a new variable and removed a specific time from that array. I have done no modifications on an original data and I would expect at this stage the availableTimesPerDay to remain unmodified. However, when I run my code, the availableTimesPerDay is modified even though I do no operations with availableTimesPerDay object itself.
What's even stranger is that the modification is not just strictly done on the 1st element, but on all specific dates that have the same day of the week. Here's output from the console for the console.log(availableTimesPerDay) defined in the function (note that 11:30 value is removed on dates 1st of December, 8th of December, 15th of December etc.
booking-helper.js:94 {"1":["10:00","10:30","11:00","11:30","12:00"],"2":[],"3":[],"4":[],"5":[],"6":[],"7":[],"8":["10:00","10:30","11:00","11:30","12:00"],"9":[],"10":[],"11":[],"12":[],"13":[],"14":[],"15":["10:00","10:30","11:00","11:30","12:00"],"16":[],"17":[],"18":[],"19":[],"20":[],"21":[],"22":["10:00","10:30","11:00","11:30","12:00"],"23":[],"24":[],"25":[],"26":[],"27":[],"28":[],"29":["10:00","10:30","11:00","11:30","12:00"],"30":[],"31":[]}
booking-helper.js:105 {"1":["10:00","10:30","11:00","12:00"],"2":[],"3":[],"4":[],"5":[],"6":[],"7":[],"8":["10:00","10:30","11:00","12:00"],"9":[],"10":[],"11":[],"12":[],"13":[],"14":[],"15":["10:00","10:30","11:00","12:00"],"16":[],"17":[],"18":[],"19":[],"20":[],"21":[],"22":["10:00","10:30","11:00","12:00"],"23":[],"24":[],"25":[],"26":[],"27":[],"28":[],"29":["10:00","10:30","11:00","12:00"],"30":[],"31":[
What's even more interesting is that if I copy the same function to codepen with same data or call it directly from the browsers console it works as expected - it removes the specific time from a specific date.
The way I understand this function is that I've extracted a specific array of available times into a new variable and removed a specific time from that array. I have done no modifications on an original data and I would expect at this stage the availableTimesPerDay to remain unmodified.
But that's not what is happening. A mere assignment of an array to a new variable does not create a new array. The new variable will reference the same array. So whatever mutation you bring to that array will be visible whether you look at that array via currentAvailableTimesOnDate or via availableTimesPerDay[bookedDate.getDate()]: they are just different ways to see the same array object.
If you don't want that splice to affect availableTimesPerDay[bookedDate.getDate()], then you must take a copy:
let currentAvailableTimesOnDate = [...availableTimesPerDay[bookedDate.getDate()]];
What's even stranger is that the modification is not just strictly done on the 1st element, but on all specific dates that have the same day of the week.
This would suggest that you have initialise availableTimesPerDay with a similar misunderstanding, so that all entries in that array reference the same array. This could for instance happen when you had initialised it as follows:
let availableTimesPerDay = Array(7).fill( ["10:00","10:30","11:00","11:30","12:00"]);
This creates one array ["10:00","10:30","11:00","11:30","12:00"] and populates the outer array with duplicate references to that array.
You should solve that too, and do something like this:
let availableTimesPerDay = Array.from({length: 7}, () =>
["10:00","10:30","11:00","11:30","12:00"]
);
Now that array literal is evaluated 7 times, each time producing a new array.
It seems like you might be under the mistaken assumption that this code:
let currentAvailableTimesOnDate = availableTimesPerDay[bookedDate.getDate()];
makes a copy of the array and you are then operating on the copy, not the original array. But that's not the case. You're essentially just aliasing the same array and then operating on it. To demonstrate:
const availableTimesPerDay = {
1: ["10:00","10:30","11:00","11:30","12:00"],
2: ["10:00","10:30","11:00","11:30","12:00"],
3: ["10:00","10:30","11:00","11:30","12:00"],
};
const currentAvailableTimesOnDate = availableTimesPerDay[1];
currentAvailableTimesOnDate.splice(0, 100);
console.log(availableTimesPerDay[1]);
If you run this code in the browser console, it will log an empty array, even though you "do no operations with availableTimesPerDay object itself."
To copy the array, you have at least a few options:
const currentAvailableTimesOnDate = availableTimesPerDay[1].slice();
// OR
const currentAvailableTimesOnDate = [...availableTimesPerDay[1]];
// OR
const currentAvailableTimesOnDate = Array.from(availableTimesPerDay[1]);
Using any of the above code, you would then be operating on a copy of the array, not the original one.
Regarding the day-of-week thing, that sounds to me like you are using getDay() instead of getDate() somewhere, though I do not see that in your code, and in fact you say you do not see that in the browser console. I don't have a clear answer for that but could it be that at one point you had getDay() and you are accidentally running an older version of the code that is different from what you are showing here and testing in the console?
Is there any clean and efficient way to add the contents of one array directly to another array without making an intermediate/temporary copy of all the data?
For example, you can use .push() to add the contents of one array directly onto another like this:
// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6]
base.push(...data);
But, that seemingly makes a copy of all the items in data as it makes them arguments to .push(). The same is true for .splice():
// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6]
base.splice(base.length, 0, ...data);
Both of these seem inefficient from a memory point of view (extra copy of all the data) and from an execution point of view (the data gets iterated twice).
Methods like .concat() don't add the contents of one array to another, but rather make a new array with the combined contents (which copies the contents of both arrays).
I've got some big arrays with lots of manipulations and I'm trying to ease the burden on the CPU/garbage collector by avoiding unnecessary intermediate copies of things and I find it curious that I haven't found such a built-in operation. So far, my best option for avoiding unnecessary copies has been this:
// imagine these are a both large arrays
let base = [1,2,3];
let data = [4,5,6];
for (let item of data) {
base.push(item);
}
which seems like it's probably not as efficient in execution as it could be if it was one operation and obviously it's multiple lines of code when you'd like it to be one line.
Per, Sebastian's helpful comments, the ideal case would be the fast-path optimization for code like this:
array.push(...data)
that V8 now deploys where it detects data as a known type of iterable and then takes optimized shortcuts to grow the target array once and then copy the data over without making the intermediate copy on the stack.
But, apparently the current V8 does not apply such an optimization to this specific case. When I tried this in node v14.3:
const targetCnt = 100_000;
const sourceCnt = 100_000_000;
// create initial arrays
let target = new Array(targetCnt);
let source = new Array(sourceCnt);
target.fill(1);
source.fill(2);
let b1 = new Bench().markBegin();
target.push(...source);
b1.markEnd();
console.log(`.push(...source): ${b1.formatNs()}`);
I got this error:
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
And, when I reduced the sourceCnt to 1_000_000, then I got this error:
RangeError: Maximum call stack size exceeded
So, I guess that optimization only applies to other circumstances mentioned in the article, not to this one.
So, it seems that until that fast path optimization is considered ubiquitous in all possible targets and you know exactly which situations it is applicable to in your code (I wonder if it would ever be codified in a spec?) and there's no danger of passing an unknown iterable in your code that wouldn't get such preferential treatment, perhaps the best option is to just make your own mini-version of the optimization as a function:
function appendToArray(targetArray, sourceArray) {
// grow the target
let targetIndex = targetArray.length;
let sourceLen = sourceArray.length;
targetArray.length = targetIndex + sourceLen;
// copy the data
for (let sourceIndex = 0; sourceIndex < sourceLen; sourceIndex++, targetIndex++) {
targetArray[targetIndex] = sourceArray[sourceIndex];
}
return targetArray;
}
db = new Array("myserver", "myfolder\\mydb.nsf")
dir = getComponent("Dir").value;
div = getComponent("Div").value;
lu = #DbLookup(db, "ManagerAccess", dir + "PP" + div, "DTManagers");
var a = [];
a.push(lu);
var item:NotesItem = docBackEnd.replaceItemValue('FormReaders', #Unique(a));
item.setReaders(true);
That code is on the querySaveDocument ssjs. The result I get from the #DbLookup (when I put in a computed field) look like this:
Pedro Martinez,Manny Ramirez,David Ortiz,Terry Francona
I tried doing an #Explode(#Implode) thing on it, but it doesn't seem to work.
The error I get in the browser just tells me that the replaceItemValue line is broken.
To test it, I pushed several strings one at a time, and it worked correctly populating my FormReaders field with the multiple entries.
What am I doing wrong?
I see several problems here:
A. In cases as described by you #Dblookup in fact would return an array. If you push an array into a plain computedField control it will exactly look as that you wrote:
value1, value2, ..., valueN
A computedField doesn't know anything about multiple values etc, it just can display strings, or data that can be converted to strings.
If you want to test the return value you could try to return something like lu[0]; you then should receive the array's 1st element, or a runtime error, if lu is NOT an array. Or you could ask for the array's size using lu.length. That returns the number of array elements, or the number of characters if it's just a plain string.
B. your code contains these two lines:
var a = [];
a.push(lu);
By that you create an empty array, then push lu[] to the first element of a[]. The result is something like this:
a[0] = [value1, value2, ..., valueN],
i.e. a is an array where the first element contains another array. Since you don't want that, just use #Unique(lu) in your replaceItemValue-method.
C. I don't see why replaceItemValue would throw an error here, apart from what I wrote in topic B. Give it a try by writing lu directly to the item (first without #Unique). That should work.
D. for completeness: in the first line you used "new Array". A much better way to define your db parameters is
var db = ["myserver", "myfolder/mydb.nsf"];
(see Tim Tripcony's comment in your recent question, or see his blog entry at http://www.timtripcony.com/blog.nsf/d6plinks/TTRY-9AN5ZK)
I have defined a function called Node which stores the properties of nodes in a graph data structure. The function is something like this:
function Node(){
...
this.outEdges = [];
this.inEdges = [];
...
}
where the inEdges and outEdges store elements of type Edge which is another function I have defined. During the program these arrays are filled with elements.
At some point in my code I need to reset these two arrays so I write:
nodes[i].outEdges.length = 0;
nodes[i].inEdges.length = 0;
where nodes is an array of elements of type Node and I am accessing an element in a for loop.
The problem is, after setting outEdges and inEdges to 0, I expected them to be [] in the nodes[i] property list. However, when I output nodes[i] into console, the outEdges and inEdges still have the elements in them. The stranger thing is that when I output nodes[i].outEdges to console, it prints [] , which is correct, but clicking on [ ] again opens the list of the elements! I can't really figure out why the nodes[i] variables don't change?
That happens (probably) because the browser prints out the empty array but by the time you check it, it has content again. So when you click to expand the browser shows the actual content.
As you can see the values [1,3,7] were added after the command console.log(o) but they are shown on the screen (even though the length shown is 0).
You're not supposed to set the length field. Just re-initialize them:
nodes[i].outEdges = [];
nodes[i].inEdges = [];
Edit: My bad, setting the length should work. It does work for me on Chrome at least. However, I still think it's safer and better style to re-init.
Just create a new object with the same name
nodes[i].outEdges = new Array();