A shorter class initialisation in ECMAScript 6

2019-01-18 12:31发布

问题:

Every time I create some class, I need to do the same boring procedure:

class Something {
  constructor(param1, param2, param3, ...) {
    this.param1 = param1;
    this.param2 = param2;
    this.param3 = param3;
    ...
  }
}

Is there any way to make it more elegant and shorter? I use Babel, so some ES7 experimental features are allowed. Maybe decorators can help?

回答1:

You can use Object.assign:

class Something {
  constructor(param1, param2, param3) {
    Object.assign(this, {param1, param2, param3});
  }
}

It's an ES2015 (aka ES6) feature that assigns the own enumerable properties of one or more source objects to a target object.

Granted, you have to write the arg names twice, but at least it's a lot shorter, and if you establish this as your idiom, it handles it well when you have arguments you do want on the instance and others you don't, e.g.:

class Something {
  constructor(param1, param2, param3) {
    Object.assign(this, {param1, param3});
    // ...do something with param2, since we're not keeping it as a property...
  }
}

Example: (live copy on Babel's REPL):

class Something {
  constructor(param1, param2, param3) {
    Object.assign(this, {param1, param2, param3});
  }
}
let s = new Something('a', 'b', 'c');
console.log(s.param1);
console.log(s.param2);
console.log(s.param3);

Output:

a
b
c


回答2:

Unfortunately, all you can do are simple things like Object.assign, but if you're trying to remove the redundancy of typing all the params twice (once in constructor signature and once in assignment) there isn't much you can do.

That said, you could do a hack like this. Though I'm not sure the effort and modicum of obfuscation that comes with it is worth it.

var dependencies = ['param1', 'param2', 'param3'];

class Something {
  constructor(...params) {
    params.forEach((param, index) => this[dependencies[index]] = param);
  }
}

var something = new Something('foo', 'bar', 'baz');
// something.param1 === 'foo'

This way you're using a single array of argument names, then using that same array as a reference when creating the properties on your instance of Something. This pattern would work well in an Angular application where you're trying to preserve dependency names through minification by setting the $inject property.

Something.$inject = dependencies;

PS - Welcome to the redundant hell of classical languages that I thought I got away from when I became a JS developer :P

Honestly, you should probably just use a classic object literal unless you really need the formality of an actual class.

Edit: I suppose you could accept an object literal in your constructor if you want the ease of a literal and the formality of an actual class.

class Something {
  constructor(params) {
    Object.keys(params).forEach((name) => this[name] = params[name]);
  }
}

var something = new Something({
  param1: 'foo',
  param2: 'bar',
  param3: 'baz'
});

But now you've just turned a class into a dynamic class that can be instantiated with any properties, kinda like an object literal :P

Usually I want a class because I want to formalize the object and present a consistent and strictly testable API.



回答3:

We could create a static method within each class that takes the arguments object and an array of names and returns an object that can be assigned to the new instance using Object.assign.

Check it out using the Babel REPL.

class Something {
  static buildArgs (ctx, args, paramNames) {
    let obj = {}
    Array.from(args).forEach(function (arg, i) {
      let name = paramNames[i] || i
      obj[name] = args[i]
    })
    Object.assign(ctx, obj)
  }

  constructor () {
    Something.buildArgs(this, arguments, [
      'param1',
      'param2'
    ]);
    console.log(this)
  }
}

new Something('one', 'two')

Admittedly the addition of a method buildArgs means that this solution is not shorter, however the body of the constructor is and we also have these advantages:

  1. You are only writing the parameter names once.
  2. You are protected against minification.

The code above accommodates extra arguments (i >= paramNames.length) however we could modify it if this behaviour is undesirable such that these are still parsed, but not assigned to the instance:

class Something {
  static buildArgs (ctx, args, paramNames) {
    let obj = {instance: {}, extra: {}}
    Array.from(args).forEach(function (arg, i) {
      let name = paramNames[i] || i
      if (name) {
          obj.instance[name] = args[i]
      } else {
          obj.extra[i] = args[i]
      }
    })
    Object.assign(ctx, obj)
  }

  constructor () {
    let args = Something.buildArgs(this, arguments, ['param1', 'param2']);
    // Do stuff with `args.extra`
  }
}

Or ignored altogether:

  static buildArgs (args, paramNames) {
    let obj = {}
    Array.from(args).forEach(function (arg, i) {
      let name = paramNames[i]
      if (name) obj[name] = args[i]
    })
    return obj
  }