For the record, this post is not my own. It was written by Github Watcher.

If there is infringement, please contact to delete

Here’s the article: Take a look at deep copy in JavaScript.

Call by Reference

Calling by reference can be a bit convoluted, you can just say “call by reference”.

JavaScript passes everything by reference. If you don’t know what this means, look at this example:

function mutate(obj) {
  obj.a = true;
}

const obj = {a: false};
mutate(obj)
console.log(obj.a); // true
Copy the code

The function mutate changes the object passed as a parameter. In a “call by value” environment, the function gets the passed value, which is a copy, and the function uses it to work together. Any changes that a function makes to an object are not visible outside of that function. But in a “call by reference” environment like JavaScript, the function gets a — you guessed it — reference and changes the actual object itself. So the final console.log will print true.

But at some point, you might want to keep your original object and create a copy of the corresponding function.

For those who are not familiar with this, you can look at the following resources

  1. You Don’t Know Js — types&grammer — cp2 values
  2. The difference between primitive and reference types

Shallow copy: object.assign ()

One way to copy an Object is to use object. assign(target, sources…). . It takes any number of source objects, enumerates all of their own properties and assigns those properties to target. If we use a new empty object as the target, we are basically copying.

const obj = / *... * /;
const copy = Object.assign({}, obj);
Copy the code

However, this is a shallow copy. If our objects contain an object, they will keep a shared reference, which is not what we want:

function mutateDeepObject(obj) {
  obj.a.thing = true;
}

const obj = {a: {thing: false}};
const copy = Object.assign({}, obj);
mutateDeepObject(copy)
console.log(obj.a.thing); // true
Copy the code

Another potential is that object.assign () converts the getter to a pure property.

What does it have to do with getters? To try:

var myObject = {
	get say() {
		return 'Hi, xiaohesong'; }};console.log('before assign'.Object.getOwnPropertyDescriptor(myObject, 'say'))
var obj = Object.assign({}, myObject)
console.log('after assign'.Object.getOwnPropertyDescriptor(obj, 'say'));
Copy the code

Try it. Does it turn into a pure property? Why don’t you try setter again?

And now? As it turns out, there are two ways to create deep copies of objects.

Note: Someone asked about object extension operators. Object extension also creates a shallow copy.

For extended operators, take a look at some of the features described earlier.

JSON.parse

One of the oldest ways to create an object is to convert it to a string representation and then parse it back into an object. This feels heavy, but it works:

const obj = / *... * /;
const copy = JSON.parse(JSON.stringify(obj));
Copy the code

The drawback here is that you create a temporary and possibly large string just to go back to the parser. Another disadvantage is that this approach does not handle looping objects. No matter what you think, it could easily happen. For example, when building a tree data structure, a node refers to its parent, which in turn refers to its children.

const x = {};
const y = {x};
x.y = y; // Cycle: x.y.x.y.x.y.x.y.x...
const copy = JSON.parse(JSON.stringify(x)); // throws!
Copy the code

In addition, things like Maps, Sets, RegExps, Dates, ArrayBuffers, and other built-in types are lost in serialization.

Json. stringify: You Don’t Know Js: Types & Grammar cp4: Coercion JSON Stringification

Structured Clone

Structured cloning is an existing algorithm for transferring values from one domain to another. For example, this method is used when you call postMessage to send a message to another window or WebWorker. The benefit of structured cloning is that it can handle looping objects and supports a large number of built-in types. The problem is that, at the time of writing this article, the algorithm is not exposed directly, but as part of other apis. So we have to look at those apis first, don’t we…

MessageChannel

As I said, whenever postMessage is called, a structured cloning algorithm is used (in the case of objects). We can create a MessageChannel and send a message. On the receiving side, the message contains a structural clone of the original data object.

function structuralClone(obj) {
  return new Promise(resolve= > {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev= > resolve(ev.data);
    port1.postMessage(obj);
  });
}

const obj = / *... * /;
const clone = await structuralClone(obj);
Copy the code

The disadvantage of this approach is that it is asynchronous. This is not a big deal, but sometimes you need to deeply copy objects in a synchronous fashion.

History API

If you’ve ever used history.pushState() to build a SPA, you know that you can provide a state object to hold the URL. It turns out that the state object is synchronized on the cloning of the structure. We had to be careful not to mess with any program logic that might use state objects, so we needed to restore the original state after cloning. To prevent triggering any events, use history.replacestate () instead of history.pushState().

function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

const obj = / *... * /;
const clone = structuralClone(obj);
Copy the code

Again, going into the browser’s engine just to copy an object feels a bit heavy, but you have to do what you have to do. In addition, Safari limits the number of calls to replaceState to 100 in 30 seconds.

Notification API

After a tweet on Twitter, Jeremy Banks showed me a third way to use structured cloning:

function structuralClone(obj) {
  return new Notification(' ', {data: obj, silent: true}).data;
}

const obj = / *... * /;
const clone = structuralClone(obj);
Copy the code

Short and concise. I like him. However, it basically requires the permissions mechanism to be enabled in the browser, so I suspect it’s very slow. For some reason, Safari keeps returning undefined for data objects.

Performance characteristics

I want to measure which of these methods has the best performance. In my first (naive) attempt, I took a small JSON object and copied it a thousand times through these different methods. Luckily, Mathias Bynens told me V8 has a cache for adding attributes to objects. So it’s pretty much irrelevant. To make sure I don’t hit the cache, I write a function that uses random names to generate objects of given depth and width and reruns the test.

For V8 there is a cache, also look at the outline and inline cache in the previous article below

The illustration!

Here’s how the different technologies in Chrome, Firefox and Edge perform. The lower the better.

conclusion

So what do we learn from this article?

  • If you don’t need to loop objects and you don’t need to keep built-in types, you canJSON.parse(JSON.stringify())To get the fastest clone of any browser, I found it very surprising.
  • If you want a properly structured clone,MessageChannelIs your only reliable cross-browser option.

Wouldn’t it be better if we made structuredClone() a function on the corresponding platform? I certainly think so, and revisited an existing issue of the HTML specification to reconsider this approach.

The original:

  1. Deep-copying in JavaScript
  2. Originally in this article: JavaScript deep copy