0

Let's say we have an object obj with its foo property being non-writable


const obj = {foo: {}}
Object.defineProperty(obj, 'foo', {value: {bar: 'baz'}, writable: false, enumerable: true})

And I want to deep copy this object with its original property descriptors preserved. So for the copied object, its foo property should still be non-writable.


const propertyDescriptors = Object.getOwnPropertyDescriptors(obj)
const cloned = Object.create(
  Object.getPrototypeOf(obj),
  propertyDescriptors
)

But the problem occurs when I want to deep copy the foo property's value. Note that I wrote a recursive algorithm to deep copy but I just used the spread operator here for brevity.

cloned.foo = {...obj.foo}

Now there is an error because cloned.foo has writable: false because we preserved the property descriptor from the original obj

I am thinking if there is a way to get around this so that I can deep copy the value of the property and also preserve its original property descriptors?

Maxim Mazurok
  • 3,856
  • 2
  • 22
  • 37
Joji
  • 4,703
  • 7
  • 41
  • 86
  • You have to add the descriptor *after* you copy all the properties. – Barmar May 27 '22 at 19:21
  • @Barmar but that's _not_ how a deep clone function is used. The target object is defined before it got deep copied. – Joji May 27 '22 at 19:49
  • Usually a deep copy function returns the copy, it doesn't copy into an existing object. – Barmar May 27 '22 at 19:57
  • @Barmar of course it would not copy into an existing object. It returns the copy. Where exactly did I make you think that I mean to change the original object? – Joji May 27 '22 at 19:59
  • You said "the target object is defined before it got deep copied". That means you're creating the object then copying into it. The object should be created by the cloning function, and it should delay making a property read-only until after it has filled it in. – Barmar May 27 '22 at 20:10
  • @Barmar the target object is the original object, not the resulting copied object. The target object that waits to be copied already exist somewhere in memory and at some point you pass that into the deep copy function which gives you a copied object, with all the property descripots preserved. The deep copy function wouldn't know if the original object has a read-only prop or not. – Joji May 27 '22 at 20:13
  • You need to copy the object without copying the property descriptors. Then add the descriptors afterward. – Barmar May 27 '22 at 20:17

3 Answers3

2

Updated @2022-05-31:

The below code go through every property deepCopyWithPropertyDescriptors and copy the value and descriptors with dynamic programming. Be aware of the max depth of object to avoid stack overflow.

Run the code to see the result before and after clone compared side by side.

function isObject(value) {
    var type = typeof value
    return value != null && (type == 'object' || type == 'function')
}

function deepCopyWithPropertyDescriptors(o) {
    const resultObj = {}

    const desc = Object.getOwnPropertyDescriptors(o)
    delete desc.value

    for(const key in o) {
        const value = o[key]
        if(isObject(value)) {
            resultObj[key] = deepCopyWithPropertyDescriptors(value)
        } else {
            resultObj[key] = value
        }
    }

    Object.defineProperties(resultObj, desc)

    return resultObj
}

// Examples 1
const obj = {foo: {}}
Object.defineProperty(obj, 'foo', {value: {bar: 'baz'}, writable: false, enumerable: true})
const cloned = deepCopyWithPropertyDescriptors(obj)

console.log("obj", Object.getOwnPropertyDescriptors(obj), Object.getOwnPropertyDescriptors(cloned))

// Examples 2
const obj2 = {foo: {}}
const obj2xfoo = {bar: 'xx'}
Object.defineProperty(obj2xfoo, 'bar', {value: 'baz', writable: true, enumerable: true, configurable: false})
Object.defineProperty(obj2, 'foo', {value: obj2xfoo, writable: false, enumerable: true, configurable: false})

const cloned2 = deepCopyWithPropertyDescriptors(obj2)

console.log("obj2.foo:", Object.getOwnPropertyDescriptors(obj2.foo), Object.getOwnPropertyDescriptors(cloned2.foo))
console.log("obj2:", Object.getOwnPropertyDescriptors(obj2), Object.getOwnPropertyDescriptors(cloned2))

The following won't copy property descriptors:

structuredClone

You can use the latest deep clone browser api structuredClone, as you do not care the compatibilities of these browsers: Internet Explorer, Opera Android, Samsung Internet.

const obj = {foo: {}}
Object.defineProperty(obj, 'foo', {value: {bar: 'baz'}, writable: false, enumerable: true})

const propertyDescriptors = Object.getOwnPropertyDescriptors(obj)

const cloned0 = structuredClone(obj)

console.log(cloned0)

JSON.parse and JSON.stringify

You can use the JSON.parse and JSON.stringify functions to bypass the writable read only check.

const obj = {foo: {}}
Object.defineProperty(obj, 'foo', {value: {bar: 'baz'}, writable: false, enumerable: true})

const propertyDescriptors = Object.getOwnPropertyDescriptors(obj)

const cloned1 = JSON.parse(JSON.stringify(obj))

console.log(cloned1)

Reference

Edwin
  • 395
  • 2
  • 12
  • Dates, Maps, Sets, RegExp, and more get lost using JSON. – kelsny May 30 '22 at 05:28
  • @hittingonme That's true. An idea is that we can use `replacer` in `JSON.stringify` and `reviver` in `JSON.parse` to handle that, but that would be another question. – Edwin May 30 '22 at 05:44
  • hey I don't think you understand my question. the question is not about how to do deep clone. btw JSON.parse and JSON.stringify is a straight-up wrong answer even for deep clone. All the JSON-incompatible values get lost. My question is to preserve the property descriptors after deep clone. Your `clone1` doesn't have the same property descriptors as `obj` has – Joji May 30 '22 at 17:15
  • I'm sorry that I was missing the point. Updated with a function `deepCopyWithPropertyDescriptors` handles the descriptors. Please have a look. – Edwin May 31 '22 at 08:00
0

You can use Object.defineProperty() a couple of times will give the expected result.

With first defineProperty() call you can create property with value undefined and property descriptor configurable: true. The next call sets the value to the added property and then updates the property descriptor as per the original object into a cloned object. Calling defineProperty() with the same key, allows modifying value and descriptor if configurable is true.

'use strict';

const obj = {foo: {}}
const foo = {bar: 'baz'};

Object.defineProperty(obj, 'foo', {value: {bar: 'baz'}, writable: false, enumerable: true})


const clonedObj = cloneObject(obj); 

//comparing with original object property
console.log(obj.foo === clonedObj.foo);
//comparing with different object with same properties
console.log(foo === clonedObj.foo);


console.log('\nOriginal Object');
objWithDiscriptors(obj);
console.log('\nClonned Object');
objWithDiscriptors(clonedObj);


function cloneObject(sourceObj){
    const __clone = {};
    Object.keys(sourceObj).map(k => {
        const propertyDescriptor = Object.getOwnPropertyDescriptor(sourceObj, k);

        Object.defineProperty(__clone, k, {configurable: true});

        if(typeof sourceObj[k] === 'object' && sourceObj[k] !== null && sourceObj[k] !== undefined && !Array.isArray(sourceObj[k])){
            Object.defineProperty(__clone, k, {value: cloneObject(sourceObj[k])});
        }else{
            Object.defineProperty(__clone, k, {value: sourceObj[k]});
        }

        Object.defineProperty(__clone, k, {
            writable: propertyDescriptor.writable,
            enumerable: propertyDescriptor.enumerable,
            configurable: propertyDescriptor.configurable
        });
    })
    return __clone;
}

function objWithDiscriptors(obj){
    Object.keys(obj).map(k => {
        console.group(k);
        const descriptor = Object.getOwnPropertyDescriptor(obj, k);
        console.log(`configurable: ${descriptor.configurable}`);
        console.log(`enumerable: ${descriptor.enumerable}`);
        console.log(`writable: ${descriptor.writable}`);
        if(typeof obj[k] == 'object' && obj[k] !== null && obj[k] !== undefined){
            objWithDiscriptors(obj[k]);
        }
        console.groupEnd()
    })
}

Additional check and logic can be added for array of objects.

MORÈ
  • 2,480
  • 3
  • 16
  • 23
0

If I understand your requirement correctly, You want to deep copy the source object into a target object by preserving source object properties (writable, enumerable). If Yes, You can use Object.assign() which copies all enumerable own properties from source object to a target object and returns the modified target object.

const obj = {};

Object.defineProperty(obj, 'foo', {
    value: {bar: 'baz'},
  writable: false,
  enumerable: true
});

const objDescriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

const structuredObj = Object.assign(obj);

const structuredObjDescriptor = Object.getOwnPropertyDescriptor(structuredObj, 'foo');

console.log(objDescriptor);

console.log(structuredObjDescriptor);

One limitation : Above solution is performing shallow copy not a deep copy.

Just curious, As writable property is false and user can not modify the foo property value, Then why we need to perform deep copy of the source object ?

Update :

To get both enumerable as well as non-enumerable properties from the cloned object. You can use Object.getOwnPropertyNames() method and then iterate over the object keys.

Demo :

const obj = {};

Object.defineProperty(obj, 'foo', {
    value: {bar: 'baz'},
  writable: false,
  enumerable: false
});

const structuredObj = Object.assign(obj);

console.log(JSON.stringify(structuredObj)); // An empty object as property is non enumerable.

// This iteration will return both enumerable as well as non-enumerable properties. 
Object.getOwnPropertyNames(structuredObj).forEach(function(property) {
    console.log(property, structuredObj[property]);
});
Debug Diva
  • 26,058
  • 13
  • 70
  • 123