ES6 Variable Declarations and Assignments: Value Transfer, Shallow Copy and Deep Copy Details

Keywords: Javascript Attribute REST JSON

ES6 Variable Declarations and Assignments: Value Transfer, Shallow Copy and Deep Copy Details Induced in the author's Modern JavaScript Development: Grammar Basis and Practical Skills Series of articles. This paper first introduces three commonly used variable declaration methods in ES6, then discusses the characteristics of JavaScript by value delivery, and finally introduces the techniques of compound type copy. Interested readers can read the next chapter. Scope and Promotion of ES6 Variables: Detailed Life Cycle of Variables.

Variable declaration and assignment

ES6 introduces two new variable declaration keywords, let and const, as well as block scope. Firstly, this paper introduces three kinds of variable declaration methods commonly used in ES6, then discusses the characteristics of JavaScript by value delivery and various assignment methods, and finally introduces the skills of compound type copy.

Variable declaration

In JavaScript, basic variable declarations can be in var mode; JavaScript allows for the omission of VaR and assignment of undeclared variables directly. That is to say, var a = 1 and a = 1 have the same effect. But since it's easy to create global variables (especially within functions) without knowing it, it's always recommended to declare variables using the VaR command. In ES6, the way of variable declaration is extended, let and const are introduced. The difference between VaR and let keywords is that the variable scope declared by VaR is the nearest function block, while the variable scope declared by let is the nearest closed block, which is often smaller than the function block. On the other hand, although variables created with let keywords are also promoted to the scope header, they cannot be used before the actual declaration; if used forcibly, a ReferenceError exception will be thrown.

var

var is one of the basic variable declaration methods in JavaScript. Its basic grammar is:

var x; // Declaration and initialization
x = "Hello World"; // Assignment

// Or all in one
var y = "Hello World";

Before ECMAScript 6, we had no other variable declaration in JavaScript. Variables declared by var acted on the function scope. If there was no corresponding closed function scope, the variable would be treated as the default global variable.

function sayHello(){
  var hello = "Hello World";
  return hello;
}
console.log(hello);

ReferenceError: hello is not defined, because hello variables can only act on sayHello functions, but they can be called normally if they are used in the following way: declare global variables first and then use them:

var hello = "Hello World";
function sayHello(){
  return hello;
}
console.log(hello);

let

In ECMAScript 6, we can use let keyword to declare variables:

let x; // Declaration and initialization
x = "Hello World"; // Assignment

// Or all in one
let y = "Hello World";

The variable declared by let keyword belongs to the block scope, that is, the function contained in {}. The advantage of using let keyword is that it can reduce the probability of accidental errors, because it guarantees that each variable can only be accessed in the smallest scope.

var name = "Peter";
if(name === "Peter"){
  let hello = "Hello Peter";
} else {
  let hello = "Hi";
}
console.log(hello);

The above code also throws a ReferenceError: hello is not defined exception, because hello can only be accessed in a closed block scope, we can make the following modifications:

var name = "Peter";
if(name === "Peter"){
  let hello = "Hello Peter";
  console.log(hello);
} else {
  let hello = "Hi";
  console.log(hello);
}

We can use this block-level scoping feature to avoid problems caused by variable retention in closures, such as the following two asynchronous codes, using var with the same variable in each loop, and using let declaration with I with different bindings in each loop, i.e., closures capture different I instances in each loop:

for(let i = 0;i < 2; i++){
        setTimeout(()=>{console.log(`i:${i}`)},0);
}

for(var j = 0;j < 2; j++){
        setTimeout(()=>{console.log(`j:${j}`)},0);
}

let k = 0;
for(k = 0;k < 2; k++){
        setTimeout(()=>{console.log(`k:${k}`)},0);
}

// output
i:0
i:1
j:2
j:2
k:2
k:2

const

Const keywords are commonly used for constant declarations. Constants declared with const keywords need to be initialized at the time of declaration and can not be modified again. Constants declared with const keywords are restricted to block-level scope for access.

function f() {
  {
    let x;
    {
      // okay, block scoped name
      const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
    let x = "inner";
  }
}

There are some differences in the expression of const keyword in JavaScript in C. For example, the following usage is correct in JavaScript, and exception is thrown in C:

# JavaScript
const numbers = [1, 2, 3, 4, 6]
numbers[4] = 5
console.log(numbers[4]) // print 5 

# C
const int numbers[] = {1, 2, 3, 4, 6};
numbers[4] = 5; // error: read-only variable is not assignable
printf("%d\n", numbers[4]); 

From the above comparison, we can also see that const restriction in JavaScript is not value immutability; instead, it creates immutable bindings, that is, read-only references to a value, and prohibits reassignment of that reference, that is, the following code triggers errors:

const numbers = [1, 2, 3, 4, 6]
numbers = [7, 8, 9, 10, 11] // error: assignment to constant variable
console.log(numbers[4])

We can understand this mechanism by referring to the following pictures, each variable identifier is associated with the physical address of the actual value of a variable; the so-called read-only variable is that the variable identifier can not be reassigned, and the value that the variable points to is variable.

There are so-called primitive and composite types in JavaScript. The primitive type declared with const is invariant in value:

# Example 1
const a = 10
a = a + 1 // error: assignment to constant variable
# Example 2
const isTrue = true
isTrue = false // error: assignment to constant variable
# Example 3
const sLower = 'hello world'
const sUpper = sLower.toUpperCase() // create a new string
console.log(sLower) // print hello world
console.log(sUpper) // print HELLO WORLD

If we want to make an object immutable, we need to use Object.freeze(); however, this method only works for the Object of the key pair, but not for the Date, Map and Set types:

# Example 4
const me = Object.freeze({name: "Jacopo"})
me.age = 28
console.log(me.age) // print undefined
# Example 5
const arr = Object.freeze([-1, 1, 2, 3])
arr[0] = 0
console.log(arr[0]) // print -1
# Example 6
const me = Object.freeze({
  name: 'Jacopo', 
  pet: {
    type: 'dog',
    name: 'Spock'
  }
})
me.pet.name = 'Rocky'
me.pet.breed = 'German Shepherd'
console.log(me.pet.name) // print Rocky
console.log(me.pet.breed) // print German Shepherd

Even Object.freeze() can only prevent top-level attributes from being modified, but cannot limit the modification of nested attributes, which we will continue to discuss in the shallow and deep copy sections below.

Assignment of variables

Transmission by value

JavaScript always passes-by-value, but when we pass a reference to an object, the value here refers to the reference to the object. The parameter of a function in value transfer is a copy of the argument passed when it is called. Modifying the value of the parameter does not affect the argument. When pass-by-reference is passed, the formal parameter of the function receives the implicit reference of the argument instead of the copy. This means that if the value of the function parameter is modified, the argument will also be modified. At the same time, they point to the same value. Let's first look at the difference between value-by-value passing and reference passing in C:

void Modify(int p, int * q)
{
    p = 27; // Pass by value - P is a copy of argument a, only p is modified
    *q = 27; // q is a reference to b, q and B are modified
}
int main()
{
    int a = 1;
    int b = 1;
    Modify(a, &b);   // a is passed by value and b by reference.
                     // a unchanged, b changed
    return(0);
}

In JavaScript, the comparison example is as follows:

function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);    
console.log(obj2.item);

// Output result
10
changed
unchanged

JavaScript passing by value is shown by modifying the value of c internally but without affecting the external obj2 variable. If we have a deeper understanding of this problem, JavaScript passes-by-sharing for objects. It was first proposed by Barbara Liskov in GLU language in 1974; this evaluation strategy was used in Python, Java, Ruby, JS and other languages. The key point of this strategy is that when calling function parameters, the function accepts a copy of the object argument reference (neither the object copy passed by value nor the implicit reference passed by reference). It differs from passing by reference in that the assignment of function parameters in shared transfer does not affect the value of arguments. The direct representation of shared delivery is obj1 in the above code. When we modify the attribute value of the object pointed to by b in the function, we also get the changed value when we use obj1 to access the same variable.

continuous assignment

JavaScript supports continuous assignment of variables, such as:

var a=b=1;

However, in continuous assignment, reference retention occurs. Consider the following scenarios:


var a = {n:1};  
a.x = a = {n:2};  
alert(a.x); // --> undefined  

In order to explain the above problems, we introduce a new variable:


var a = {n:1};  
var b = a; // Hold a for review  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> [object Object]  

In fact, in continuous assignment, a value is a memory address directly assigned to a variable:


              a.x  =  a  = {n:2}
              │      │
      {n:1}<──┘      └─>{n:2}

Deconstruction: Deconstruction Assignment

Deconstruction assignment allows you to assign attributes of arrays and objects to various variables using a grammar similar to array or object literals. This assignment grammar is extremely concise and clearer than traditional attribute access methods. The traditional way to access the first three elements of an array is:

    var first = someArray[0];
    var second = someArray[1];
    var third = someArray[2];

By deconstructing the characteristics of assignment, it can be changed into:

    var [first, second, third] = someArray;
// === Arrays

var [a, b] = [1, 2];
console.log(a, b);
//=> 1 2


// Use from functions, only select from pattern
var foo = () => {
  return [1, 2, 3];
};

var [a, b] = foo();
console.log(a, b);
// => 1 2


// Omit certain values
var [a, , b] = [1, 2, 3];
console.log(a, b);
// => 1 3


// Combine with spread/rest operator (accumulates the rest of the values)
var [a, ...b] = [1, 2, 3];
console.log(a, b);
// => 1 [ 2, 3 ]


// Fail-safe.
var [, , , a, b] = [1, 2, 3];
console.log(a, b);
// => undefined undefined


// Swap variables easily without temp
var a = 1, b = 2;
[b, a] = [a, b];
console.log(a, b);
// => 2 1


// Advance deep arrays
var [a, [b, [c, d]]] = [1, [2, [[[3, 4], 5], 6]]];
console.log("a:", a, "b:", b, "c:", c, "d:", d);
// => a: 1 b: 2 c: [ [ 3, 4 ], 5 ] d: 6


// === Objects

var {user: x} = {user: 5};
console.log(x);
// => 5


// Fail-safe
var {user: x} = {user2: 5};
console.log(x);
// => undefined


// More values
var {prop: x, prop2: y} = {prop: 5, prop2: 10};
console.log(x, y);
// => 5 10

// Short-hand syntax
var { prop, prop2} = {prop: 5, prop2: 10};
console.log(prop, prop2);
// => 5 10

// Equal to:
var { prop: prop, prop2: prop2} = {prop: 5, prop2: 10};
console.log(prop, prop2);
// => 5 10

// Oops: This doesn't work:
var a, b;
{ a, b } = {a: 1, b: 2};

// But this does work
var a, b;
({ a, b } = {a: 1, b: 2});
console.log(a, b);
// => 1 2

// This due to the grammar in JS. 
// Starting with { implies a block scope, not an object literal. 
// () converts to an expression.

// From Harmony Wiki:
// Note that object literals cannot appear in
// statement positions, so a plain object
// destructuring assignment statement
//  { x } = y must be parenthesized either
// as ({ x } = y) or ({ x }) = y.

// Combine objects and arrays
var {prop: x, prop2: [, y]} = {prop: 5, prop2: [10, 100]};
console.log(x, y);
// => 5 100


// Deep objects
var {
  prop: x,
  prop2: {
    prop2: {
      nested: [ , , b]
    }
  }
} = { prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"]}}};
console.log(x, b);
// => Hello c


// === Combining all to make fun happen

// All well and good, can we do more? Yes!
// Using as method parameters
var foo = function ({prop: x}) {
  console.log(x);
};

foo({invalid: 1});
foo({prop: 1});
// => undefined
// => 1


// Can also use with the advanced example
var foo = function ({
  prop: x,
  prop2: {
    prop2: {
      nested: b
    }
  }
}) {
  console.log(x, ...b);
};
foo({ prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"]}}});
// => Hello a b c


// In combination with other ES2015 features.

// Computed property names
const name = 'fieldName';
const computedObject = { [name]: name }; // (where object is { 'fieldName': 'fieldName' })
const { [name]: nameValue } = computedObject;
console.log(nameValue)
// => fieldName



// Rest and defaults
var ajax = function ({ url = "localhost", port: p = 80}, ...data) {
  console.log("Url:", url, "Port:", p, "Rest:", data);
};

ajax({ url: "someHost" }, "additional", "data", "hello");
// => Url: someHost Port: 80 Rest: [ 'additional', 'data', 'hello' ]

ajax({ }, "additional", "data", "hello");
// => Url: localhost Port: 80 Rest: [ 'additional', 'data', 'hello' ]


// Ooops: Doesn't work (in traceur)
var ajax = ({ url = "localhost", port: p = 80}, ...data) => {
  console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({ }, "additional", "data", "hello");
// probably due to traceur compiler

But this does:
var ajax = ({ url: url = "localhost", port: p = 80}, ...data) => {
  console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({ }, "additional", "data", "hello");


// Like _.pluck
var users = [
  { user: "Name1" },
  { user: "Name2" },
  { user: "Name2" },
  { user: "Name3" }
];
var names = users.map( ({ user }) => user );
console.log(names);
// => [ 'Name1', 'Name2', 'Name2', 'Name3' ]


// Advanced usage with Array Comprehension and default values
var users = [
  { user: "Name1" },
  { user: "Name2", age: 2 },
  { user: "Name2" },
  { user: "Name3", age: 4 }
];

[for ({ user, age = "DEFAULT AGE" } of users) console.log(user, age)];
// => Name1 DEFAULT AGE
// => Name2 2
// => Name2 DEFAULT AGE
// => Name3 4

Arrays and Iterators

The above is a simple example of array deconstruction assignment. The general form of its grammar is:

    [ variable1, variable2, ..., variableN ] = array;

This assigns values to variables from variable1 to variableN for the corresponding element items in the array. If you want to declare variables at the same time of assignment, you can add var, let or const keywords before the assignment statement, for example:

    var [ variable1, variable2, ..., variableN ] = array;
    let [ variable1, variable2, ..., variableN ] = array;
    const [ variable1, variable2, ..., variableN ] = array;

In fact, it's inappropriate to describe variables because you can deconstruct nested arrays at any depth:

    var [foo, [[bar], baz]] = [1, [[2], 3]];
    console.log(foo);
    // 1
    console.log(bar);
    // 2
    console.log(baz);
    // 3

In addition, you can leave the corresponding bits blank to skip some elements in the deconstructed array:

    var [,,third] = ["foo", "bar", "baz"];
    console.log(third);
    // "baz"

And you can also go through it.“ Indefinite parameter ” The pattern captures all the trailing elements in the array:

    var [head, ...tail] = [1, 2, 3, 4];
    console.log(tail);
    // [2, 3, 4]

When accessing empty arrays or cross-border access arrays, their deconstruction is consistent with their indexing behavior, and the final result is undefined.

    console.log([][0]);
    // undefined
    var [missing] = [];
    console.log(missing);
    // undefined

Note that the pattern of array deconstruction assignment is also applicable to any iterator:

    function* fibs() {
      var a = 0;
      var b = 1;
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }
    var [first, second, third, fourth, fifth, sixth] = fibs();
    console.log(sixth);
    // 5

object

By deconstructing an object, you can bind each of its attributes to a different variable, first specifying the bound attribute, and then following a variable to be deconstructed.

    var robotA = { name: "Bender" };
    var robotB = { name: "Flexo" };
    var { name: nameA } = robotA;
    var { name: nameB } = robotB;
    console.log(nameA);
    // "Bender"
    console.log(nameB);
    // "Flexo"

When the attribute name is consistent with the variable name, a practical syntactic abbreviation can be used:

    var { foo, bar } = { foo: "lorem", bar: "ipsum" };
    console.log(foo);
    // "lorem"
    console.log(bar);
    // "ipsum"

As with array decomposition, you can nest and further combine object decomposition at will:

    var complicatedObj = {
      arrayProp: [
        "Zapp",
        { second: "Brannigan" }
      ]
    };
    var { arrayProp: [first, { second }] } = complicatedObj;
    console.log(first);
    // "Zapp"
    console.log(second);
    // "Brannigan"

When you deconstruct an undefined attribute, the value is undefined:

    var { missing } = {};
    console.log(missing);
    // undefined

Note that when you deconstruct an object and assign variables, if you have declared or do not intend to declare these variables (that is, there are no let, const or var keywords before the assignment statement), you should pay attention to such a potential grammatical error:

    { blowUp } = { blowUp: 10 };
    // Syntax error grammar error

Why did it go wrong? This is because JavaScript syntax informs the parsing engine to parse any statement starting with {into a block statement (for example, {console} is a legitimate block statement). The solution is to wrap the entire expression in a pair of parentheses:

    ({ safe } = {});
    // No errors have no grammatical errors

Default value

When the property you want to deconstruct is undefined, you can provide a default value:

    var [missing = true] = [];
    console.log(missing);
    // true
    var { message: msg = "Something went wrong" } = {};
    console.log(msg);
    // "Something went wrong"
    var { x = 3 } = {};
    console.log(x);
    // 3

Since object deconstruction is allowed in Deconstruction and default values are also supported, deconstruction can be applied to function parameters and default values of parameters.

function removeBreakpoint({ url, line, column }) {
      // ...
    }

The deconstruction feature comes in handy when we construct an object that provides configuration and require the object's properties to carry default values. For example, jQuery's ajax function uses a configuration object as its second parameter. We can override the function definition in this way:

jQuery.ajax = function (url, {
      async = true,
      beforeSend = noop,
      cache = true,
      complete = noop,
      crossDomain = false,
      global = true,
      // ... More configuration
    }) {
      // ... do stuff
    };

Similarly, deconstruction can be applied to multiple return values of functions, similar to tuples in other languages.

function returnMultipleValues() {
      return [1, 2];
    }
var [foo, bar] = returnMultipleValues();

Three Dots

Rest Operator

In JavaScript function calls, we often use built-in arguments objects to get the parameters of function calls, but there are many inconveniences in this way. For example, the arguments object is an Array-Like object, which cannot directly use the array's. map() or. forEach() functions; and because arguments are bound to the scope of the current function, if we want to use the arguments object of the outer function in the nested function, we need to create intermediate variables.

function outerFunction() {  
   // store arguments into a separated variable
   var argsOuter = arguments;
   function innerFunction() {
      // args is an array-like object
      var even = Array.prototype.map.call(argsOuter, function(item) {
         // do something with argsOuter               
      });
   }
}

ES6 provides Rest Operator to get the call parameters of the function in array form. Rest Operator can also be used to get the remaining variables in array form in Deconstruction assignment:

function countArguments(...args) {  
   return args.length;
}
// get the number of arguments
countArguments('welcome', 'to', 'Earth'); // => 3  
// destructure an array
let otherSeasons, autumn;  
[autumn, ...otherSeasons] = cold;
otherSeasons      // => ['winter']  

Typical Rest Operator application scenarios such as filtering for specified types of indefinite arrays:

function filter(type, ...items) {  
  return items.filter(item => typeof item === type);
}
filter('boolean', true, 0, false);        // => [true, false]  
filter('number', false, 4, 'Welcome', 7); // => [4, 7]  

Although there is no arguments object defined in Arrow Function, we can still use Rest Operator to get the call parameters of Arrow Function:

(function() {
  let outerArguments = arguments;
  const concat = (...items) => {
    console.log(arguments === outerArguments); // => true
    return items.reduce((result, item) => result + item, '');
  };
  concat(1, 5, 'nine'); // => '15nine'
})();

Spread Operator

Spread Operator is just the opposite of Rest Operator. It is often used for array construction and deconstruction assignment. It can also be used to convert an array into a list of parameters of a function. The basic usage of Spread Operator is as follows:

let cold = ['autumn', 'winter'];  
let warm = ['spring', 'summer'];  
// construct an array
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// function arguments from an array
cold.push(...warm);  
cold              // => ['autumn', 'winter', 'spring', 'summer']  

We can also use Spread Operator to simplify function calls:

class King {  
   constructor(name, country) {
     this.name = name;
     this.country = country;     
   }
   getDescription() {
     return `${this.name} leads ${this.country}`;
   }
}
var details = ['Alexander the Great', 'Greece'];  
var Alexander = new King(...details);  
Alexander.getDescription(); // => 'Alexander the Great leads Greece'  

Another advantage is that it can be used to replace Object.assign to easily create new objects from old objects and to modify some values; for example:

var obj = {a:1,b:2}
var obj_new_1 = Object.assign({},obj,{a:3});
var obj_new_2 = {
  ...obj,
  a:3
}

Finally, we need to discuss Spread Operator and Iteration Protocols, in fact Spread Operator also uses Iteration Protocols for element traversal and result collection; therefore, we can also control the performance of Spread Operator by customizing Iterator. The Iterable protocol specifies that the object must contain the Symbol.iterator method, which returns an Iterator object:

interface Iterable {  
  [Symbol.iterator]() {
    //...
    return Iterator;
  }
}

The Iterator object is subordinate to the Iterator Protocol and needs to provide the next member method, which returns an object containing the properties of done and value:

interface Iterator {  
  next() {
     //...
     return {
        value: <value>,
        done: <boolean>
     };
  };
}

Typical Iterable objects are strings:

var str = 'hi';  
var iterator = str[Symbol.iterator]();  
iterator.toString(); // => '[object String Iterator]'  
iterator.next();     // => { value: 'h', done: false }  
iterator.next();     // => { value: 'i', done: false }  
iterator.next();     // => { value: undefined, done: true }  
[...str];            // => ['h', 'i']

We can control the effect of an array-like object on an iterator by customizing its Symbol.iterator property:

function iterator() {  
  var index = 0;
  return {
    next: () => ({ // Conform to Iterator protocol
      done : index >= this.length,
      value: this[index++]
    })
  };
}
var arrayLike = {  
  0: 'Cat',
  1: 'Bird',
  length: 2
};
// Conform to Iterable Protocol
arrayLike[Symbol.iterator] = iterator;  
var array = [...arrayLike];  
console.log(array); // => ['Cat', 'Bird']  

ArayLike [Symbol. iterator] creates an attribute for the object that is an iterator, so that the object conforms to the Iterable protocol; iterator() returns an object that contains the next member method, so that the object eventually has a behavior similar to that of an array.

Copy Composite Data Types: Copies of Composite Types

Shallow Copy: Shallow Copy

Top-level attribute traversal

Shallow copy refers to the independent copy of the first key pair when the object is copied. A simple implementation is as follows:

// Shallow Copy Realization
function shadowCopy(target, source){ 
    if( !source || typeof source !== 'object'){
        return;
    }
    // This method is a little trick. target must be defined beforehand, otherwise it can't change the parameters.
       // Specific reasons can be explained by whether JS is passed by value or by reference in Resources.
    if( !target || typeof target !== 'object'){
        return;
    }  
    // It's better to distinguish between object and array replication here.
    for(var key in source){
        if(source.hasOwnProperty(key)){
            target[key] = source[key];
        }
    }
}

//Test example
var arr = [1,2,3];
var arr2 = [];
shadowCopy(arr2, arr);
console.log(arr2);
//[1,2,3]

var today = {
    weather: 'Sunny',
    date: {
        week: 'Wed'
    } 
}

var tomorrow = {};
shadowCopy(tomorrow, today);
console.log(tomorrow);
// Object {weather: "Sunny", date: Object}

Object.assign

Object.assign() method can copy enumerable attributes owned by any number of source objects to the target object, and then return to the target object. The Object.assign method only copies the source object's own enumerable attributes to the target object. Note that for accessor attributes, this method executes the getter function of that accessor attribute, and then copies the resulting value to the target object. If you want to copy the accessor attribute itself, use Object.getOwnPropertyDescriptor() and Object.defineProperties() Method.

Be careful, Character string Type and symbol Type attributes are copied.

Note that exceptions may occur during the copying of attributes, such as a read-only attribute of the target object and an attribute of the source object with the same name, when the method throws a TypeError Exceptions, interruptions in the copy process, attributes that have been copied successfully will not be affected, and attributes that have not been copied will no longer be copied.

Note that Object.assign skips those values null or undefined The source object of the.

Object.assign(target, ...sources)
  • Example: Shallow copy of an object
var obj = { a: 1 };
var copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
  • Example: Merge several objects
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };

var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // {a: 1, b: 2, c: 3} Note that the target object itself will change.
  • Example: Copy properties of type symbol s
var o1 = { a: 1 };
var o2 = { [Symbol("foo")]: 2 };

var obj = Object.assign({}, o1, o2);
console.log(obj); // { a: 1, [Symbol("foo")]: 2 }
  • Example: Inheritance attributes and non-enumerable attributes cannot be copied
var obj = Object.create({foo: 1}, { // foo is an inheritance property.
    bar: {
        value: 2  // bar is a non-enumerable property.
    },
    baz: {
        value: 3,
        enumerable: true  // baz is an enumerable property of its own.
    }
});

var copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
  • Example: The original value is implicitly converted to its wrapper object
var v1 = "123";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo")

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// Source objects, if they are original values, are automatically converted into their wrapper objects.
// The null and undefined original values will be completely ignored.
// Note that only wrapped objects of strings have their own enumerable properties.
console.log(obj); // { "0": "1", "1": "2", "2": "3" }
  • Example: Exceptions occur during copying attributes
var target = Object.defineProperty({}, "foo", {
    value: 1,
    writeable: false
}); // The foo property of target is a read-only property.

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// Note that this exception occurs when the second attribute of the second source object is copied.

console.log(target.bar);  // 2. The first source object copy is successful.
console.log(target.foo2); // 3. It shows that the first attribute of the second source object has been copied successfully.
console.log(target.foo);  // 1. The read-only property cannot be overwritten, so the second property copy of the second source object fails.
console.log(target.foo3); // Unfined, the assign method exits after the exception, and the third attribute will not be copied.
console.log(target.baz);  // Unfined, the third source object will not be copied.

Use []. concat to copy arrays

Similar to object replication, we recommend [. concat for deep replication of arrays:

var list = [1, 2, 3];
var changedList = [].concat(list);
changedList[1] = 2;
list === changedList; // false

Similarly, the concat method can only guarantee one layer of deep replication:

> list = [[1,2,3]]
[ [ 1, 2, 3 ] ]
> new_list = [].concat(list)
[ [ 1, 2, 3 ] ]
> new_list[0][0] = 4
4
> list
[ [ 4, 2, 3 ] ]

Defects of shallow copy

However, it should be noted that assign is a shallow copy, or a first-level deep copy. Two examples are given to illustrate that:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    }
};

const opt = Object.assign({}, defaultOpt, {
    title: {
        subtext: 'Yes, your world.'
    }
});

console.log(opt);

// Expected results
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
// Actual result
{
    title: {
        subtext: 'Yes, your world.'
    }
}

In the example above, for the first-level sub-elements of an object, only references are replaced, but content is not added dynamically. So assign doesn't solve the problem of object reference confusion. Refer to the following example:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    } 
};

const opt1 = Object.assign({}, defaultOpt);
const opt2 = Object.assign({}, defaultOpt);
opt2.title.subtext = 'Yes, your world.';

console.log('opt1:');
console.log(opt1);
console.log('opt2:');
console.log(opt2);

// Result
opt1:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
opt2:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}

DeepCopy: Deep Copy

Recursive property traversal

Generally speaking, when considering deep replication of composite types in JavaScript, it often refers to the processing of Date, Object and Array. The most common way we can think of is to create an empty new object first, and then recursively traverse the old object until we find the underlying type of child nodes before assigning the corresponding location to the new object. The problem with this approach, however, is that there is a magical prototype mechanism in JavaScript, and the prototype will appear during traversal, and then the prototype should not be assigned to new objects. So in the traversal process, we should consider using hasOenProperty method to filter out those attributes inherited from the prototype chain:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

The call is as follows:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cylicGraph["right"] = cylicGraph;

Deep copy using JSON

JSON.parse(JSON.stringify(obj));

For general needs can be met, but it has shortcomings. In the following example, you can see that JSON replication ignores undefined dropouts and function expressions.

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
//Object {a: 1, b: 2}

Extended reading

Posted by pfdesigns on Tue, 04 Jun 2019 13:26:05 -0700