JS Tip of the Day: Copying Accessor Properties

Copying Accessor Properties
Version: ES2015 (Object.assign), ES2017 (Object.getOwnPropertyDescriptors)
Level: Intermediate

In using Object.assign() to copy properties into an object, you’re assigning new properties much in the way that you would if you were to use the assignment operator (=). Each new property is added as a simple data property, even if the original property value is coming from an accessor property. In other words, Object.assign() does not copy property implementations, only property values.

let source = {
    data: 1,
    get accessor () {
        return 2
    }
};
let target = {};
Object.assign(target, source);
console.log(target.data); // 1
console.log(target.accessor); // 2

console.log(Object.getOwnPropertyDescriptor(source, 'accessor').get);
// function

console.log(Object.getOwnPropertyDescriptor(target, 'accessor').get);
// undefined (not accessor, only data property)

If you want to copy properties from an object and keep them accessors if they are accessors, Object.assign() won’t cut it. Instead what you’ll want to use Object.defineProperty(), or more likely, in the case of copying multiple properties at once, Object.defineProperties(). This combined with Object.getOwnPropertyDescriptor() or Object.getOwnPropertyDescriptors() will let you copy accessor properties as accessor properties. Additionally, it will maintain other property characteristics such as writable, enumerable, and configurable.

let source = {
    data: 1,
    get accessor () {
        return 2
    }
};
let target = {};
Object.defineProperties(
    target,
    Object.getOwnPropertyDescriptors(source)
);
console.log(target.data); // 1
console.log(target.accessor); // 2

console.log(Object.getOwnPropertyDescriptor(source, 'accessor').get);
// function

console.log(Object.getOwnPropertyDescriptor(target, 'accessor').get);
// function (accessor!)

Bear in mind that when accessor functions are copied this way, both objects will be using the same accessor functions. This can cause problems if the function is using some hidden data such as a closure variable to represent the property’s value.

let vault;
{
    let secretPassword = '12345'; // only accessible to password
    vault = {
        get password () {
            return secretPassword;
        },
        set password (value) {
            secretPassword = value;
        }
    }
};
let lockbox = {};
Object.defineProperties(
    lockbox,
    Object.getOwnPropertyDescriptors(vault)
);
console.log(vault.password); // 12345
lockbox.password = 'abcde';
console.log(lockbox.password); // abcde
console.log(vault.password); // abcde (should not have changed!)

However, if the accessor is using another, non-hidden data property for its data, everything will work ok since the function will pull from the instance for its data rather than something that would be shared between both instances.

let vault = {
    _password: '12345', // not so secret now
    get password () {
        return this._password;
    },
    set password (value) {
        this._password = value;
    }
};
let lockbox = {};
Object.defineProperties(
    lockbox,
    Object.getOwnPropertyDescriptors(vault)
);
console.log(vault.password); // 12345
lockbox.password = 'abcde';
console.log(lockbox.password); // abcde
console.log(vault.password); // 12345 (Ok, unchanged)

More info:


More tips: JavaScript Tips of the Day