Cloning arrays of objects with Object.assign - javascript

I discovered a bug on a project I'm working on that can be replicated by this snippet:
const original = [ { value: 1 } ];
function test() {
const copy = Object.assign([], original);
copy.forEach(obj => obj.value = obj.value + 1);
}
console.log(original[0].value); // -> 1, expected 1
test();
console.log(original[0].value); // -> 2, expected 1
test();
console.log(original[0].value); // -> 3, expected 1
I do not understand why this is the case. In the MDN web docs, the following statements can be found in the deep copy warning section:
For deep cloning, we need to use alternatives, because Object.assign() copies property values.
If the source value is a reference to an object, it only copies the reference value.
How do these notes apply to arrays / in this case? Are array values somehow considered as properties?
Looking back now, the method was probably not intended to work with arrays, so I guess I reap what I sow... but I'd still like to understand what's going on here. The intent was to deep copy the array in order to mutate the objects inside while keeping the original intact.

Are array values somehow considered as properties?
Yes. In JavaScript, arrays are objects (which is why Object.assign works with them), and properties with a special class of names called array indexes (strings defining decimal numbers in standard form with numeric values < 232 - 1) represent the elements of the array. (Naturally, JavaScript engines optimize them into true arrays when they can, but they're defined as objects and performing object operations on them is fully supported.) I found this sufficiently surprising when getting deep into JavaScript that I wrote it up on my anemic old blog.
Given:
const obj = {a: 1};
const arr = [1];
these two operations are the same from a specification viewpoint:
console.log(obj["a"]);
console.log(arr["0"]); // Yes, in quotes
Of course, we don't normally write the quotes when accessing array elements by index, normally we'll just do arr[0], but in theory, the number is converted to a string and then the property is looked up by name — although, again, modern JavaScript engines optimize.
const obj = {a: 1};
const arr = [1];
console.log(obj["a"]);
console.log(arr["0"]); // Yes, in quotes
console.log(arr[0]);
If you need to clone an array and the objects in it, map + property spread is a useful way to do that, but note the objects are only cloned shallowly (which is often sufficient, but not always):
const result = original.map((value) => ({...value}));
For a full deep copy, see this question's answers.

Here we can use structuredClone for deep copy.

Related

how to prevent spread operator from maintaining original reference?

I'm setting an array to a property like this:
originalFacilityList: [...maintenanceInfo.Facilities]
However, when I check originalFacilityList downstream, it reflects the updates that were made to the maintenanceInfo.Facilities array. I was thinking that the spread operator was used to break that reference. Am I doing something incorrectly in this example?
When you do this originalFacilityList: [...maintenanceInfo.Facilities] you are effectively cloning the array. More precisely, you are doing a shallow clone, as opposed to deep clone.
As a result, when you add or remove items from the new array, the changes do not reflect on the original array:
const arr = [1, 2, 3];
const shallowClone = [...arr];
shallowClone.push(4);
console.log(shallowClone, arr);
What you have probably noticed is that the objects contained in the array are actually referenced by the old and the new array:
const arr = [{ property: 'value' }];
const shallowClone = [...arr];
arr[0].newProperty = 'newValue';
console.log(shallowClone);
If you want to avoid that, then you need to deep clone the array. There is no native solution for this, except some solutions like JSON.parse(JSON.stringify( that are very hacky and only work with serializable objects (does not preserve functions, prototypal inheritance, etc...), so either implement it yourself, or use utility libraries like Lodash's cloneDeep.

Check if array of objects includes an object

I am trying to check if an array of objects includes a object. I want it to return true when there is a object in the array that has the same values and the object id should not matter. This is how i thought it would work:
let arr = [{r:0, g:1}];
let obj = {r:0, g:1}
console.log(arr.includes(obj));
But it returns false and I need it to return true. Do I have to convert every object in the array to a string with JSON.stringify() and the object I am searching for like this:
let arr = [JSON.stringify({r: 0, g: 1})]
let obj = {r: 0, g: 1}
console.log(arr.includes(JSON.stringify(obj)));
Is there another easier and more efficient way to do it with more objects?
You get false because objects are compared by a reference to the object, while you got there 2 separate object instances.
Wile JSON.stringify might work, keep in mind that the order of properties is not guaranteed and it may fail if the order is not the same, because you get a different string.
you can check for an id property or compare several properties to match against, if you must you can compare all properties with a loop.
If you have an access to the object's reference, you can use a Map or a Set which allows you to store and check references
const obj = {r:0, g:1};
const obj2 = {r:0, g:1};
const mySet = new Set();
// given the fact that you do have access to the object ref
mySet.add(obj);
const isObjInList = mySet.has(obj);
const isObj2InList = mySet.has(obj2);
console.log('is obj in list - ', isObjInList);
console.log('is obj2 in list - ', isObj2InList);
JSON.stringify doesn't work as expected if you change the order of properties in one of the objects.
You can use .some in combination with isEqual from lodash (or other alternatives). Or you can write it by yourself, but be careful, there are too many edge cases, that's why I recommend using an existing approach. There is no need to reinvent the wheel.
let arr = [JSON.stringify({r: 0, g: 1})]
let obj = {g: 1, r: 0}
console.log(arr.includes(JSON.stringify(obj)));
let arr2 = [{r:0, g:1}];
let obj2 = {g:1, r:0};
console.log(arr2.some(item => _.isEqual(item, obj2)));
console.log(_.some(arr2, item => _.isEqual(item, obj2))); // more canonical way
<script src="https://cdn.jsdelivr.net/lodash/4/lodash.min.js"></script>
I like to use Set() for this purposes, read from the documentation:
The Set object lets you store unique values of any type, whether primitive values or object references.
See the below example:
let obj = {r:0, g:1};
const set = new Set();
set.add(obj);
console.log(set.has(obj));
I hope that helps!
You can use the JavaScript some() method to find out if a JavaScript array contains an object.
This method tests whether at least one element in the array passes the test implemented by the provided function. Here's an example that demonstrates how it works:
// An array of objects
var persons = [{name: "Harry"}, {name: "Alice"}, {name: "Peter"}];
// Find if the array contains an object by comparing the property value
if(persons.some(person => person.name === "Peter")){
alert("Object found inside the array.");
} else{
alert("Object not found.");
}
Note that if try to find the object inside an array using the indexOf() method like persons.indexOf({name: "Harry"}) it will not work (always return -1). Because, two distinct objects are not equal even if they look the same (i.e. have the same properties and values). Likewise, two distinct arrays are not equal even if they have the same values in the same order.
The some() method is supported in all major browsers, such as Chrome, Firefox, IE (9 and above), etc. See the tutorial on JavaScript ES6 Features to learn more about arrow function notation.

Cloning any javascript object by copying all own properties

If I wanted to clone any javascript object (that's not null), I would think I could just copy all of its own properties (enumerable and non-enumerable) -- using Object.getOwnPropertyNames -- onto a new empty object.
But I've noticed that an example of a deep cloning function provided by Dojo toolkit (https://davidwalsh.name/javascript-clone) treats RegExp, Date, and Node objects as special cases, and lodash.cloneDeep also has a lot of logic that is a lot more complicated than simply copying properties, including having some special cases of its own and apparently not supporting all types of objects: (https://github.com/lodash/lodash/blob/master/.internal/baseClone.js).
Why is simply copying the object properties not sufficient? What else is there to a javascript object besides its properties that I don't know about?
EDIT: to be clear, I'm talking about deep cloning an object. Sorry for the confusion.
If the top level properties are all value objects like strings and numbers then just copying the top level properties is fine for a clone of an object. If there are any reference objects such as dates, arrays or other objects then all your are doing is copying a reference from one object to another. If you change the reference object on the clone you will mutate the original object.
Take a look at my clone function at https://stackblitz.com/edit/typescript-qmzgf7
If it is an array it clones every item in the array, if it is a date it creates a new date with the same time, if it is an object it clones every property else if just copies the property.
The cloned object can now be mutated without worrying about effects it might have on the original object.
const clone = obj =>
Array.isArray(obj)
? obj.map(item => clone(item))
: obj instanceof Date
? new Date(obj.getTime())
: (typeof obj === 'object') && obj
? Object.getOwnPropertyNames(obj).reduce((o, prop) => ({ ...o, [prop]: clone(obj[prop]) }), {})
: obj;
let original = { prop1: "Original", objProp: { prop1: "Original" } };
let swallowCopy = { ...original };
let clonedObj = clone(original);
clonedObj.prop1 = "Changed";
clonedObj.objProp.prop1 = "Changed";
console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);
swallowCopy.prop1 = "Changed";
swallowCopy.objProp.prop1 = "Changed";
console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);
Notice how modifying the property on the object property shallow copy causes the original to change as well.
The easiest way to clone an object in JS is by using the ... spread operator.
Let's say you have this object:
const object = { foo: 1, bar: 2 }
To clone it, you can simply declare:
const objectClone = {...object}.
This will create all the properties present in the original object onto the clone, as well as their values.
Now the problem is, if you have any object nested in there, the copies will be made by reference. Suppose the original object is this instead:
const student = { studentID: 1, tests: { test1: 90, test2: 95}}
If you create a copy of that object by using the spread operator(or Object.assign, spread is just syntactic sugar), the nested object will actually point to the object inside the original object! So repeating this:
const studentClone = {...student}
And now you edit a property of the nested object inside the clone:
studentClone.tests.test1 = 80
This will change the value in both clone, and original object, as the nested object is really just pointing to 1 object in memory.
Now what those utilities, like _.cloneDeep will do, is iterate through all inner objects in the object you're cloning, and repeat the process. You could technically do it yourself, but you wouldn't be able to do it on objects with many nested objects easily. Something like this:
const studentClone = {...studentClone, tests: {...studentClone.tests}}
This would create new objects, with no reference problems.
Hope this helped!
EDIT: Just adding, object spreading would only work properly for prototype objects, of course. Each instantiated objects,such as arrays, Date objects etc, would have their own way of cloning.
Arrays can be copied similarly, through [...array]. It does follow the same rules regarding to references. For dates, you can simply pass the original date object into the Date constructor again:
const clonedDate = new Date(date)
This is where the third-party utilities will come in handy, as they'll usually handle most use cases.
This answer does a good job of explaining two of the problems with cloning a normal JavaScript object: prototype properties and circular references. But to answer your question regarding certain built-in types, the TL;DR answer is that there are 'under the hood' properties that you have no programmatic access to.
Consider:
let foo = [1, 2];
let bar = {};
Object.assign(bar, foo);
Object.setPrototypeOf(bar, foo.constructor.prototype); // aka Array.prototype
bar[0]; // 1
bar instanceof Array; // true
bar.map(x => x + 1); // [] ????
Empty array? Why? Just to make sure we're not crazy
foo.map(x => x + 1); // [2, 3]
The reason why map (and the other array methods) fail to work is that an Array isn't simply an object: it has internal slot properties for the stuff you put in it that you don't get to see as the JavaScript programmer. As another example, every JavaScript object has an internal [[Class]] property that says what kind of object it is. Fortunately for us, there's a loophole in the spec that allows us indirect access to it: the good ol Object.prototype.toString.call hack. So let's see what that has to say about various stuff:
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(3); // [object Number]
Object.prototype.toString.call({}); // [object Object]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(/\w/); // [object RegExp]
Object.prototype.toString.call(JSON); // [object JSON]
Object.prototype.toString.call(Math); // [object Math]
Let's see what it says about our foo and bar:
Object.prototype.toString.call(foo); // [object Array]
Object.prototype.toString.call(bar); // [object Object] Doh!
There's no way to 'convert' a random object to an Array... or a Date... or an HTMLElement... or a regex. Now, there are in fact ways to clone all of those things, but they require special logic: you can't just copy properties, or even set the prototype, because they have internal logic you can't access or directly replicate.
In normal everyday JavaScript programming we don't worry too much about this stuff, it's the kind of thing that's generally of interest to library authors (or language implementers). We everyday working stiffs just use a library to cover the edge cases and call it a day. But every once in a while the abstractions we use leak and the ugly bubbles through. This is however a great illustration of why you should probably use battle-tested libraries rather than trying to roll your own.
An object in javascript includes fields and functions together, and every field could be another object (Like Date type). If you copy a date field, it will be a reference type assignment.
Example:
var obj1 = { myField : new Date('2018/9/17') };
var obj2 = {};
obj2.myField = obj1.myField;
Now, if we change "obj2.myField" like this:
obj2.myField.setDate(obj2.myField.getDate() + 2);
console.log(obj1.myField); // Result =====> Wed Sep 19 2018 00:00:00 GMT+0430
As you see, obj1 and obj2 still are linked.
Correct way to copy a date field:
obj2.myField = new Date(obj1.myField.getTime());
Most native objects(like you have mentioned - I don't know for is the correct naming for them; maybe built-in?) are treated as "simple": it does not make sense to copy Date object property-by-property. In the same time they all are mutable in some way.
let a = {test: new Date(1)}; // test === Thu Jan 01 1970 00:00:00GMT
let copy_a = {test: a.test}; // looks like cloned
a.test.setDate(12); // let's mutate original date
console.log(copy_a.test); // Thu Jan 12 1970 00:00:00GMT ooops modified as well
So you either should handle that exceptions(special cases) explicitly or take a risk of side effects for some cases.

What is happening under the hood in javascript when using the Array brackets notation

I have generally found javascript to be transparent, in that there are very few black boxes where "magic" just happens and you should just accept and look the other way, however I have not found any answer to how the Array brackets [] notation actually works under the hood.
let arr = [4, 5, 6, 7]
console.log(arr[3]) // <- How does this work?
What is javascript doing to access the item at index 3. Does it internally call some method on the Array.prototype?
With an object, the [] is a shortcut for a property accessor.
let obj = {
a: 'hello',
b: 'world'
}
obj['a'] === obj.a // true
Is an array then just an object with a long list of integer based properties?
let objArray = {
0: 'hello',
1: 'world'
}
let realArray = ['hello', 'world']
objArray[0] === 'hello' // true
realArray[0] === 'hello' // true
objArray.0 // SyntaxError: Unexpected number
realArray.0 // SyntaxError: Unexpected number
I have seen many many online discussions that all come to the conclusion that you cannot overload the brackets notation to truly subclass an Array but I have never seen an explanation on what magic is happening under the hood that allows the Array to work the way it does.
The obvious follow up question is whether there is any way to intercept the bracket notation access to define your own behavior, but I think I already know the answer to that.
You'd probably have to look at the implementation code to know precisely what's going on, but the basic idea is that arrays are actually layered atop objects.
This is backwards:
With an object, the [] is a shortcut for a property accessor.
The bracket notation is more fundamental. Thus, obj['foo'] and obj.foo work the same, but there is no equivalent for obj['foo & bar'], which is perfectly legitimate, and will respond with a value if obj has a key named "foo & bar".
Is an array then just an object with a long list of integer based properties?
Not quite, but you're not far off. Arrays are objects with the Array prototype, and with a little bit of additional magic to set the length property when new keys are added, or remove keys when that length is set.
And no, you cannot override the [] operator for your own purposes.
Is an array then just an object with a long list of integer based properties?
Yes, in it's simplest form, an Array is an Object with a list of integer base properties that is based on the Array prototype (which gives access to all the array methods like map, forEach, etc.)
As for intercepting the bracket notation, no, I have not seen anything that would allow that besides creating your own Object that has the methods you need (and then only access that object via the appropriate methods).
More info from MDN:
Arrays are list-like objects whose prototype has methods to perform traversal and mutation operations. Neither the length of a JavaScript array nor the types of its elements are fixed. Since an array's length can change at any time, and data can be stored at non-contiguous locations in the array, JavaScript arrays are not guaranteed to be dense; this depends on how the programmer chooses to use them. In general, these are convenient characteristics; but if these features are not desirable for your particular use, you might consider using typed arrays.
Arrays cannot use strings as element indexes (as in an associative array) but must use integers. Setting or accessing via non-integers using bracket notation (or dot notation) will not set or retrieve an element from the array list itself, but will set or access a variable associated with that array's object property collection. The array's object properties and list of array elements are separate, and the array's traversal and mutation operations cannot be applied to these named properties.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#Array_object

How are JavaScript arrays implemented?

Namely, how does the following code:
var sup = new Array(5);
sup[0] = 'z3ero';
sup[1] = 'o3ne';
sup[4] = 'f3our';
document.write(sup.length + "<br />");
output '5' for the length, when all you've done is set various elements?
My 'problem' with this code is that I don't understand how length changes without calling a getLength() or a setLength() method. When I do any of the following:
a.length
a['length']
a.length = 4
a['length'] = 5
on a non-array object, it behaves like a dict / associative array. When I do this on the array object, it has special meaning. What mechanism in JavaScript allows this to happen? Does JavaScript have some type of property system which translates
a.length
a['length']
into "get" methods and
a.length = 4
a['length'] = 5
into "set" methods?
Everything in JavaScript is an object. In the case of an Array, the length property returns the size of the internal storage area for indexed items of the array. Some of the confusion may come into play in that the [] operator works for both numeric and string arguments. For an array, if you use it with a numeric index, it returns/sets the expected indexed item. If you use it with a string, it returns/sets the named property on the array object - unless the string corresponds to a numeric value, then it returns the indexed item. This is because in JavaScript array indexes are coerced to strings by an implicit toString() call. Frankly, this is just one more of those things that makes you scratch your head and say "JavaScript, this, this is why they laugh at you."
The actual underlying representation may differ between browsers (or it may not). I wouldn't rely on anything other than the interface that is supplied when working with it.
You can find out more about JavaScript arrays at MDN.
Characteristics of a JavaScript array
Dynamic - Arrays in JavaScript can grow dynamically .push
Can be sparse - for example, array[50000] = 2;
Can be dense - for example, array = [1, 2, 3, 4, 5]
In JavaScript, it is hard for the runtime to know whether the array is going to be dense or sparse. So all it can do is take a guess. All implementations use a heuristic to determine if the array is dense or sparse.
For example, code in point 2 above, can indicate to the JavaScript runtime that this is likely a sparse array implementation. If the array is initialised with an initial count, this could indicate that this is likely a dense array.
When the runtime detects that the array is sparse, it is implemented in a similar way to an object. So instead of maintaining a contiguous array, a key/value map is built.
For more references, see How are JavaScript arrays implemented internally?
This really depends on what you intend to do with it.
[].length is "magical".
It doesn't actually return the number of items in the array. It returns the largest instated index in the array.
var testArr = []; testArr[5000] = "something"; testArr.length; // 5001
But the method behind the setter is hidden in the engine itself.
Some engines in some browsers will give you access to their implementations of those magic-methods.
Others will keep everything completely locked down.
So don't rely on defineGetter and defineSetter methods, or even, really, __proto__ methods, unless you know which browsers you know you're targeting, and which you aren't.
This will change in the future, where opt-in applications written in ECMAScript Next/6 will have access to more.
ECMAScript 5-compliant browsers are already starting to offer get and set magic methods in objects and there's more to come... ...but it's probably a while away before you can dump support for oldIE and a tonne of smartphones, et cetera...
It is important to know that when you do sup['look'] = 4; you are not using an associative array, but rather modify properties on the object sup.
It is equivalent to sup.look = 4; since you can dynamically add properties on JavaScript objects at any time. sup['length'] would for an instance output 5 in your first example.
To add to tvanfosson's answer: In ECMA-262 (the 3.0 specification, I believe), arrays are simply defined as having this behavior for setting properties (See 15.4.5.1). There's no general mechanism underlying it (at least as of now) - this is just how it's defined, and how JavaScript interpreters must behave.
As other people have mentioned, a property in JavaScript can basically act as both as getter and a setter of your array (or string or other inputs).
As a matter of fact, you might try this yourself:
const test = [1, 2, 3, 4, 5]
test.length = 3
console.log(test) // [1, 2, 3]
test.length = 5
console.log(test) // Guess what happens here!
As far as I know, arrays in JavaScript do not work exactly like associative arrays and you have elements which are put in memory as contiguously as possible (given that you can have arrays of mixed objects), depending on the JavaScript engine you are considering.
As a side note, I am a bit baffled that the most voted answer keeps spreading the over-simplified myth (or half-truth) of "everything being an object in JavaScript"; that is not exactly true, otherwise you will never study primitives, for example.
Try to do this:
const pippi = "pippi"
pippi.cat = "cat"
console.log(pippi.cat) // Will it work? Throw an error? Guess why again
Spoiler: the string is wrapped in a throwaway object for that specific operation on the second line, and then in the following one you are just going to access a property of the primitive which is not there (provided you did not play with String.prototype or the like), so you get undefined.
Array object inherits caller, constructor, length, and name properties from Function.prototype.
A JavaScript array is an object just like any other object, but JavaScript gives it special syntax.
arr[5] = "yo"
The above is syntactic sugar for
arr.insert(5,"yo")
which is how you would add stuff to a regular object. It's what is inside the insert method that changes the value of arr.length
See my implementation of a customArray type here: http://jsfiddle.net/vfm3vkxy/4/

Categories