The Principle and Implementation of Shallow Copy in the javascript Series--Object.assign

Keywords: Javascript Attribute

1. Preface

In the previous article, you learned assignment, shallow copy, and deep copy.The knowledge and differences of the three are introduced.

Port: https://www.mwcxs.top/page/59...

This paper describes how to implement a shallow copy of Object.assign(), and then let's try to implement a shallow copy.

2. Shallow copy of Object.assign()

What is a shallow copy?A shallow copy is the creation of a new object with an exact copy of the original object's attribute values.

What is a shallow copy of Object.assign()?Copies the values of all enumerable attributes from one or more data source objects to the target object, and returns the target object.

Rule of grammar:

Object.assign(target,...sources)

Where the target object is the target object, the source object is the source object, which can be multiple, and the modification returns the target object.

1. If the attributes in the target object have the same attribute keys, the attributes will be overwritten by the attributes in the source object.

2. The generic affinity of the source object will be similar to overwriting the previous attributes.

Two points are emphasized:

1. Enumerable attributes (own attributes)

2. string or symbol s can be directly assigned

2.1 Chestnuts 1

A shallow copy is a copy of the basic type value of the first layer and the reference type address of the first layer.

// saucxs
// Step One
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "saucxs",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c);
// true

// Step 2
b.name = "change";
b.book.price = "55";
console.log(b);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// }

// Step 3
console.log(a);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "55"}
// }

Analysis:

1. In the first step, the value of the source object b is copied to the target object a by using Object.assign. Here the return value is defined as object c. It can be seen that b replaces the value of a with the same key, that is, if the attributes in the target object a have the same key, the affinity will be overwritten by the attributes in the source object b.The returned object c is the target object a.

2. In the second step, modify the basic type value (name) and reference type value (book) of source object b.

3. In the third step, the basic type value of the target object a does not change after the shallow copy, but the reference type value changes because Object.assign() copies the attribute value.The attribute value of the added source object is a reference to the object, copying only that reference address.

2.2 Chestnuts 2

Attributes of string and symbol s are copied, and source objects with null or undefined values are not skipped.

// saucxs
// Step One
let a = {
    name: "saucxs",
    age: 18
}
let b = {
    b1: Symbol("saucxs"),
    b2: null,
    b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     b1: Symbol(saucxs),
//     b2: null,
//     b3: undefined
// }
console.log(a === c);
// true

3. Implement Object.assign simulation

Approximate ideas for implementing Object.assign simulation:

1. Determine whether the native Object supports assign. If it does not exist, an assign function is created and bound to the Object using Object.defineProperty.

2. Determine if the parameter is correct (the target parameter cannot be empty, you can set {} to pass in directly, but it must have a value).

3. Convert an object using Object(), save it as to, and return the object to.

4. Use the for-in loop to iterate through all the enumerable self-attributes and copy them to the new target object (use hasOwnProperty to get the own attributes, that is, attributes on the non-prototype chain)

Referring to native, the implementation code is as follows, using assign2 instead of assign.The symbols attribute is not supported by the simulation here because there is no symbol in es5.

// saucxs
if (typeof Object.assign2 != 'function') {
  // Note 1
  Object.defineProperty(Object, "assign2", {
    value: function (target) {
      'use strict';
      if (target == null) { // Note 2
        throw new TypeError('Cannot convert undefined or null to object');
      }

      // Note 3
      var to = Object(target);
        
      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) {  // Note 2
          // Note 4
          for (var nextKey in nextSource) {
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

Test it:

// saucxs
// test case
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "saucxs",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let c = Object.assign2(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// } 
console.log(a === c);
// true

3.1 Note 1: Enumerability

Properties mounted on Objects are not enumerable natively, but can be enumerated directly after the property a is mounted on Objects, so you must use Object.defineProperty and set enumerable: false and writable: true, configurable: true.

// saucxs
for(var i in Object) {
    console.log(Object[i]);
}
// No Output

Object.keys( Object );
// []

As explained above, attributes on native Object s are not enumerable.

There are two ways to see whether Object.assign is enumerable, either using Object.getOwnPropertyDescriptor or Object.propertyIsEnumberable, where propertyIsEnumerable(.) checks whether a given property name exists directly in the object (not in the prototype chain) and satisfies enumerable:true.The specific usage is as follows:

// saucxs
Object.getOwnPropertyDescriptor(Object, "assign");
// {
//     value: ƒ, 
//  writable: true, //writable
//  enumerable: false, //not enumerable, note that this is false
//  configurable: true //configurable
// }
// saucxs
Object.propertyIsEnumerable("assign");
// false

Indicates that Object.assign is not enumerable.

Attribute a can be enumerated after it is mounted directly on the Object.Let's look at the code:

// saucxs
Object.a = function () {
    console.log("log a");
}

Object.getOwnPropertyDescriptor(Object, "a");
// {
//     value: ƒ, 
//  writable: true, 
//  enumerable: true, //Note that this is true
//  configurable: true
// }

Object.propertyIsEnumerable("a");
// true

So to implement Object.assign, you must use Object.defineProperty and set writable: true, enumerable: false, configurable: true, of course, false by default.

// saucxs
Object.defineProperty(Object, "b", {
    value: function() {
        console.log("log b");
    }
});

Object.getOwnPropertyDescriptor(Object, "b");
// {
//     value: ƒ, 
//  writable: false, //Note that this is false
//  enumerable: false, //Note that this is false
//  configurable: false //Note that this is false
// }

Implementing simulation involves code

// saucxs
// Determine whether function assign2 exists in native Object
if (typeof Object.assign2 != 'function') {
  // Defining a new attribute assign2 using an attribute descriptor
  Object.defineProperty(Object, "assign2", {
    value: function (target) { 
      ...
    },
    // The default value is false, that is, enumerable: false
    writable: true,
    configurable: true
  });
}

3.2 Note 2: Determine if the parameters are correct

Some articles determine whether the parameter is correct or not.

// saucxs
if (target === undefined || target === null) {
    throw new TypeError('Cannot convert undefined or null to object');
}

This is certainly OK, but it is not necessary because undefined and null are equal (elevation 3 P52), that is, undefined == null returns true, just do the following.

// saucxs
if (target == null) { // TypeError if undefined or null
    throw new TypeError('Cannot convert undefined or null to object');
}

3.3 Note 3: The original type is packaged as an object

// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 

// The original type is wrapped, null and undefined are ignored.
// Note that only wrapper objects of strings may have their own enumerable properties.
console.log(obj); 
// { "0": "a", "1": "b", "2": "c" }

The source objects v2, v3, and v4 in the code above are actually ignored because they do not have enumerable properties themselves.

// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;

// Object.keys(.)) returns an array containing all enumerable properties
// Only the attributes directly contained by the object will be found, not the [[Prototype]] chain
Object.keys( v1 ); // [ '0', '1', '2' ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 ); // TypeError: Cannot convert undefined or null to object

The code above shows that Object.keys(..)) returns an array containing all enumerable attributes, only the attributes directly contained by the object, not the [[prototype]] chain.

// Object.getOwnPropertyNames(.)) returns an array containing all properties, whether or not they are enumerable
// Only the attributes directly contained by the object will be found, not the [[Prototype]] chain
Object.getOwnPropertyNames( v1 ); // [ '0', '1', '2', 'length' ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 ); 
// TypeError: Cannot convert undefined or null to object

The code above shows that Object.getOwnPropertyNames(.)) returns an array to protect all properties of the welding, whether they can be enumerated or not, only the attributes directly contained by the object, not the [[prototype]] chain.

But this can be done:

// saucxs
var a = "abc";
var b = {
    v1: "def",
    v2: true,
    v3: 10,
    v4: Symbol("foo"),
    v5: null,
    v6: undefined
}

var obj = Object.assign(a, b); 
console.log(obj);
// { 
//   [String: 'abc']
//   v1: 'def',
//   v2: true,
//   v3: 10,
//   v4: Symbol(foo),
//   v5: null,
//   v6: undefined 
// }

Why?Because undefined, true, and so on are not suitable objects, but are property values of object b, object B is enumerable.

// saucxs
// Follow the code above
Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]

In fact, you can also see here that the target object is the original type and will be wrapped as an object. Corresponding to the code above, the target object a will be wrapped as [String:'abc']. What should we do when simulating the implementation?It's easy, just use Object(..).

// saucxs
var a = "abc";
console.log( Object(a) );
// {0: 'a', 1: 'b', 2: 'c'}

Let's see if the following code can be executed:

// saucxs
var a = "abc";
var b = "def";
Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'

The error is also reported because when Object('abc'), its property descriptor writable is not writeable, that is, writeable: false.

// saucxs
var myObject = Object( "abc" );

Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]

Object.getOwnPropertyDescriptor(myObject, "0");
// { 
//   value: 'a',
//   writable: false, //Notice here
//   enumerable: true,
//   configurable: false 
// }

3.4 Note 4: Existence

How to determine if an attribute exists in an object without accessing its value, see the following code:

// saucxs
var anotherObject = {
    a: 1
};

// Create an object associated with anotherObject
var myObject = Object.create( anotherObject );
myObject.b = 2;

("a" in myObject); // true
("b" in myObject); // true

myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true

Using the in and hasOwnProperty methods, the differences are as follows:

1. The in operator checks whether attributes are in the object and its [[propertype]] prototype chain;

2. hasOwnProperty(..)) only checks if it is in the myObject object, not in the [[prototype]] prototype chain.

The Object.assign method is certainly not copying attributes on the prototype chain, so hasOwnProperty(..) is used to determine what to do when simulating an implementation, but it is problematic to use myObject.hasOwnProperty(..) directly because objects may not be connected to Object.prototype (created by Object.create(null), in which case myObject.hasOwnProperty(.) is used.Will fail.

// saucxs
var myObject = Object.create( null );
myObject.b = 2;

("b" in myObject); 
// true

myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function

The solution is to use call as follows:

// saucxs
var myObject = Object.create( null );
myObject.b = 2;

Object.prototype.hasOwnProperty.call(myObject, "b");
// true
//So in this simulation implementation, the relevant code is as follows.

// saucxs
// Use for..in to traverse the object nextSource to get the property value
// Attributes on its prototype chain are also checked here
for (var nextKey in nextSource) {
    // Use hasOwnProperty to determine whether the property nextKey exists in the object nextSource
    // Filter attributes on its prototype chain
    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
        // Assign to object and return to object to after traversal
        to[nextKey] = nextSource[nextKey];
    }
}

4. Reference

1,Object.assign() of MDN

2,Understanding Object.assign()

Posted by locell on Sat, 18 May 2019 14:23:46 -0700