3

When you're writing ES6 classes you can use destructuring in the constructor to make it easier to use default options:

class Person {
    constructor({
        name = "Johnny Cash",
        origin = "American",
        profession = "singer"
    } = {}) {
        this.name = name;
        this.origin = origin;
        this.profession = profession;
    }

    toString() {
        return `${this.name} is an ${this.origin} ${this.profession}`;
    }
}

This allows you do to things like this:

const person = new Person();
console.log(person.toString());
// Returns 'Johnny Cash is an American singer'

const nina = new Person({
    name : "Nina Simone"
})
console.log(nina.toString());
// Returns 'Nina Simone is an American singer'

However, you need to repeat the arguments in the constructor to assign them to the class instance. I already found out you can do this to make it less verbose:

Object.assign(this, { name, origin, profession });

But you still need to repeat those three variables. Is there any way to make the assignment even shorter without repeating the variables?

Husky
  • 5,757
  • 2
  • 46
  • 41

3 Answers3

3

You can just have your constructor take one "config" parameter, and then say Object.assign(this, config). To have defaults, you can just set up a default object, or assign them outside the constructor as class properties.

EDIT To T.J. Crowder's point, technically anything passed in while using Object.assign() will be added to your object, which could be harmful in a number of ways. I have added a way to strip the config object to only contain a set of properties that you define. T.J.'s solution for this will work as well, I just did it differently so you have options.

function stripObjToModel(model, obj) {
  // this method will modify obj
  Object.keys(obj).forEach(function (key) {
    if (!model.hasOwnProperty(key)) {
      delete obj[key];
    }
  });
  return obj;
}
class Person {
    constructor(config = {}) {
        var defaults = {
            name: "Johnny Cash",
            origin: "American",
            profession:  "singer"
        };
        Object.assign(this, defaults, stripObjToModel(defaults, config));
    }

    toString() {
        return `${this.name} is an ${this.origin} ${this.profession}`;
    }
}

const person = new Person();
console.log(person.toString());
// Returns 'Johnny Cash is an American singer'

const nina = new Person({
    name : "Nina Simone",
    foo: "bar",
    toString: function () {return "Hello World!";}
})
console.log(nina.toString());
// Returns 'Nina Simone is an American singer'
mhodges
  • 10,938
  • 2
  • 28
  • 46
  • 1
    @T.J.Crowder Yeah, that was my mistake - it has been fixed – mhodges Jan 31 '18 at 16:37
  • 1
    What bothers me about an `Object.assign` solution like this is that **all** properties from the options object are copied onto `this`, whether they're `name`, `origin`, `profession`, or something else (could even be `toString`). – T.J. Crowder Jan 31 '18 at 16:41
  • @T.J.Crowder Good point - edited. – mhodges Jan 31 '18 at 16:52
  • @mhodges I didn't realise you could use `Object.assign` with more than two arguments, that's very smart. Thanks! But i guess what @T.J.Crowder mentions is indeed a bit of a problem, and adding an extra function just to strip unwanted values seems a bit of a kludge. I guess my original solution is more explicit in that respect. – Husky Jan 31 '18 at 16:56
  • @Husky Yeah, you can use it with `n` number of arguments - they are applied from **right to left** - meaning that the right-most objects will have their values reflected in the resulting object. – mhodges Jan 31 '18 at 16:57
  • @mhodges: Not to nit-pick, but they're **applied** *left* to *right* -- which is why if there are overlapping properties, the rightmost one wins (it's last to be applied). – T.J. Crowder Jan 31 '18 at 16:59
  • @T.J.Crowder I guess it depends how you think about it. I think about it as `n` is merged with `n-1`, `n-1` is merged with `n-2`, etc. all the way until your first argument. You merge the right into the left, not the other way around. The resulting object reference will always be that of the left-most argument to Object.assign – mhodges Jan 31 '18 at 17:00
  • @mhodges: Yeah. The mechanics of it really don't matter much outside of the timing of getters. :-) – T.J. Crowder Jan 31 '18 at 17:01
  • @T.J.Crowder From MSDN: Syntax - `Object.assign(target, ...sources)` "Properties in the target object will be overwritten by properties in the sources if they have the same key. Later sources' properties will similarly overwrite earlier ones. " – mhodges Jan 31 '18 at 17:04
  • @mhodges: Yes, I didn't say anything to contradict that. There's a difference between "applied right-to-left" and "later sources' properties ... overwrite earlier ones." *(Not that MSDN would be my go-to reference for this... ;-) )* – T.J. Crowder Jan 31 '18 at 17:05
  • 1
    @T.J.Crowder Yeah, maybe I misspoke - I think a better word instead of "applied" would be "given priority" – mhodges Jan 31 '18 at 17:06
  • @mhodges @T.J.Crowder now that i've given it some more thought the original alternative of using `Object.assign(this, { name, origin, profession });` is actually quite nice because it prevents accidental copying of values (like the `toString` method). – Husky Jan 31 '18 at 17:08
  • 1
    @Husky Yeah, I think that's the general consensus - your original code is fine. It's a tad verbose, but I don't think there's a good alternative. – mhodges Jan 31 '18 at 17:09
  • 2
    @T.J.Crowder Looking at [the spec](https://www.ecma-international.org/ecma-262/6.0/#sec-object.assign), you're right. Mechanically, it's done from left to right. – mhodges Jan 31 '18 at 17:10
1

Not that I'm advocating it, but you can technically do

constructor(options = {}) {
  ({
    name: this.name = "Johnny Cash",
    origin: this.origin = "American",
    profession: this.profession = "singer",
  } = options);
}

Personally I think your example code with a bit of repetition looks totally fine.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • I get an `Unexpected token this` when i try running that... – Husky Jan 31 '18 at 16:53
  • 1
    LOL - still repeats the names, though. :-) And you couldn't in a subclass, `this` isn't available in the parameters scope of a subclass constructor. – T.J. Crowder Jan 31 '18 at 16:54
  • @Husky: Try it now (Logan had `:` where he meant `=`, amusingly the opposite of the mistake I made in my answer that he corrected...). – T.J. Crowder Jan 31 '18 at 16:54
  • Woops, I always forget you can't do that in a binding pattern, only an assignment pattern. Fixed. – loganfsmyth Jan 31 '18 at 16:56
  • @loganfsmyth: You can, just not in a subclass constructor. :-) – T.J. Crowder Jan 31 '18 at 16:56
  • You got me on the subclassing point, but moving it out fixes that. As for repeating the names, I honestly think it's bad practice not to at least repeat them once, since the fact that function params match class property names is entirely arbitrary. Repeating them more than these two times is certainly getting ugly though. – loganfsmyth Jan 31 '18 at 16:58
  • 1
    I guess this solution is also a bit moot because it's repeating the variable names again. – Husky Jan 31 '18 at 16:59
  • 1
    Logan - Yeah. Like you, I think @Husky's original code is the best way to go. Simple, clear, and not *too* much repetition. – T.J. Crowder Jan 31 '18 at 17:00
  • @Husky Depends I guess. It repeats the option name once instead of twice, which seems nice to me. Like I said I think explicitly mentioning both the parameter name and the property name separately is a good thing. For instance what if you want to make the class properties use underscore prefixes to flag them as non-public? They are conceptually different names, it just happens that they are the same in this case. – loganfsmyth Jan 31 '18 at 17:12
  • @loganfsmyth you're right, my original solution isn't as verbose as i initially thought, and more explicit. I think this solution (like you said) is a bit too crazy ;) – Husky Jan 31 '18 at 17:14
0

Sadly, I don't think there's any built-in way to accept an options object with individual defaults and grab only those properties from the object you receive (and not other alien properties that may also be specified) without repeating yourself. This is unfortunate. (It's also not the first time I've wanted the destructuring and a reference to the object being destructured.)

So that means we have to go out on our own, but it's simple enough:

function applyDefaults(target, options, defaults) {
    for (const key in defaults) { // Or `const key of Object.keys(defaults)` for just own props
        target[key] = (typeof options[key] === undefined ? defaults : options)[key];
    }
}

then:

constructor(options = {}) {
    applyDefaults(this, options, {
        name: "Johnny Cash",
        origin: "American",
        profession: "singer"
    });
}

(If recreating the object on every constructor call worries you, you could move that out of the class, but I wouldn't worry about it.)

But then the individual property defaults aren't described in the method signature, which for me would be strong enough reason to go ahead and repeat the names as you have it now.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Yes, this is a nice solution as well, and probably how i would have handled it if i couldn't write ES6. But maybe repeating the properties is not so bad as i initially thought: at least it is explicit. – Husky Jan 31 '18 at 17:12
  • 1
    @Husky: Yeah, I'd stick with what you have. But if you want an alternative, this is short and to the point, and only lists the names once. ;-) – T.J. Crowder Jan 31 '18 at 17:16