30

CoffeeScript automatically sets the arguments as instance properties in the constructor if you prefix the arguments with @.

Is there any trick to accomplish the same in ES6?

William Lepinski
  • 888
  • 1
  • 7
  • 14
  • 6
    Nope, but you can do `Object.assign(this, {arg, u, ments});`. – Felix Kling Dec 17 '14 at 15:52
  • 1
    That could be shortcut, but i'll had to type the arguments names either way. I think we'll need to wait until Annotations get alive to do some sort of AOP in Javascript. Anyway, thanks for the answer. – William Lepinski Dec 17 '14 at 18:27
  • You could pass the arguments as an object to start with. Then it's just a matter of saying `constructor(options) { Object.assign(this, options); }`. –  Dec 20 '14 at 05:17
  • @torazaburo Unfortunately not an option if people are using AngularJS! Good idea though, I've used that myself a couple times – Jay Jun 04 '16 at 18:55
  • @WilliamLepinski Have you figured out how to do this in plain es6+ without typescript. It seems i have the same question - how to prevent dependencies duplication in angularjs constructors. Was going to write a decorator that use constructor arguments, but there's no arguments in constructor and most likely it won't contain named arguments. – Rantiev Sep 16 '17 at 23:48

4 Answers4

22

Felix Kling's comment outlines the closest you'll get to a tidy solution for this. It uses two ES6 features—Object.assign and the object literal property value shorthand.

Here's an example with tree and pot as the instance properties:

class ChristmasTree {
    constructor(tree, pot, tinsel, topper) {
        Object.assign(this, { tree, pot });
        this.decorate(tinsel, topper);
    }

    decorate(tinsel, topper) {
        // Make it fabulous!
    }
}

Of course, this isn't really what you wanted; you still need to repeat the argument names, for one thing. I had a go at writing a helper method which might be a bit closer…

Object.autoAssign = function(fn, args) {

    // Match language expressions.
    const COMMENT  = /\/\/.*$|\/\*[\s\S]*?\*\//mg;
    const ARGUMENT = /([^\s,]+)/g;

    // Extract constructor arguments.
    const dfn     = fn.constructor.toString().replace(COMMENT, '');
    const argList = dfn.slice(dfn.indexOf('(') + 1, dfn.indexOf(')'));
    const names   = argList.match(ARGUMENT) || [];

    const toAssign = names.reduce((assigned, name, i) => {
        let val = args[i];

        // Rest arguments.
        if (name.indexOf('...') === 0) {
            name = name.slice(3);
            val  = Array.from(args).slice(i);
        }

        if (name.indexOf('_') === 0) { assigned[name.slice(1)] = val; }

        return assigned;
    }, {});

    if (Object.keys(toAssign).length > 0) { Object.assign(fn, toAssign); }
};

This auto-assigns any parameters whose names are prefixed with an underscore to instance properties:

constructor(_tree, _pot, tinsel, topper) {
    // Equivalent to: Object.assign({ tree: _tree, pot: _pot });
    Object.autoAssign(this, arguments);
    // ...
}

It supports rest parameters, but I omitted support for default parameters. Their versatility, coupled with JS' anaemic regular expressions, makes it hard to support more than a small subset of them.

Personally, I wouldn't do this. If there were a native way to reflect on the formal arguments of a function, this would be really easy. As it is, it's a mess, and doesn't strike me as a significant improvement over Object.assign.

Jordan Gray
  • 16,306
  • 3
  • 53
  • 69
7

Legacy support script

I've extended Function prototype to give access to parameter auto-adoption to all constructors. I know we should be avoiding adding functionality to global objects but if you know what you're doing it can be ok.

So here's the adoptArguments function:

var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/g;
var parser = /^function[^\(]*\(([^)]*)\)/i;
var splitter = /\s*,\s*/i;

Function.prototype.adoptArguments = function(context, values) {
    /// <summary>Injects calling constructor function parameters as constructed object instance members with the same name.</summary>
    /// <param name="context" type="Object" optional="false">The context object (this) in which the the calling function is running.</param>
    /// <param name="values" type="Array" optional="false">Argument values that will be assigned to injected members (usually just provide "arguments" array like object).</param>

    "use strict";

    // only execute this function if caller is used as a constructor
    if (!(context instanceof this))
    {
        return;
    }

    var args;

    // parse parameters
    args = this.toString()
        .replace(comments, "") // remove comments
        .match(parser)[1].trim(); // get comma separated string

    // empty string => no arguments to inject
    if (!args) return;

    // get individual argument names
    args = args.split(splitter);

    // adopt prefixed ones as object instance members
    for(var i = 0, len = args.length; i < len; ++i)
    {
        context[args[i]] = values[i];
    }
};

The resulting call that adopts all constructor call arguments is now as follows:

function Person(firstName, lastName, address) {
    // doesn't get simpler than this
    Person.adoptArguments(this, arguments);
}

var p1 = new Person("John", "Doe");
p1.firstName; // "John"
p1.lastName; // "Doe"
p1.address; // undefined

var p2 = new Person("Jane", "Doe", "Nowhere");
p2.firstName; // "Jane"
p2.lastName; // "Doe"
p2.address; // "Nowhere"

Adopting only specific arguments

My upper solution adopts all function arguments as instantiated object members. But as you're referring to CoffeeScript you're trying to adopt just selected arguments and not all. In Javascript identifiers starting with @ are illegal by specification. But you can prefix them with something else like $ or _ which may be feasible in your case. So now all you have to do is detect this specific naming convention and only add those arguments that pass this check:

var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/g;
var parser = /^function[^\(]*\(([^)]*)\)/i;
var splitter = /\s*,\s*/i;

Function.prototype.adoptArguments = function(context, values) {
    /// <summary>Injects calling constructor function parameters as constructed object instance members with the same name.</summary>
    /// <param name="context" type="Object" optional="false">The context object (this) in which the the calling function is running.</param>
    /// <param name="values" type="Array" optional="false">Argument values that will be assigned to injected members (usually just provide "arguments" array like object).</param>

    "use strict";

    // only execute this function if caller is used as a constructor
    if (!(context instanceof this))
    {
        return;
    }

    var args;

    // parse parameters
    args = this.toString()
        .replace(comments, "") // remove comments
        .match(parser)[1].trim(); // get comma separated string

    // empty string => no arguments to inject
    if (!args) return;

    // get individual argument names
    args = args.split(splitter);

    // adopt prefixed ones as object instance members
    for(var i = 0, len = args.length; i < len; ++i)
    {
        if (args[i].charAt(0) === "$")
        {
            context[args[i].substr(1)] = values[i];
        }
    }
};

Done. Works in strict mode as well. Now you can define prefixed constructor parameters and access them as your instantiated object members.

Extended version for AngularJS scenario

Actually I've written an even more powerful version with following signature that implies its additional powers and is suited for my scenario in my AngularJS application where I create controller/service/etc. constructors and add additional prototype functions to it. As parameters in constructors are injected by AngularJS and I need to access these values in all controller functions I can simply access them, via this.injections.xxx. Using this function makes it much simpler than writing several additional lines as there may be many many injections. Not to even mention changes to injections. I only have to adjust constructor parameters and I immediately get them propagated inside this.injections.

Anyway. Promised signature (implementation excluded).

Function.prototype.injectArguments = function injectArguments(context, values, exclude, nestUnder, stripPrefix) {
    /// <summary>Injects calling constructor function parameters into constructed object instance as members with same name.</summary>
    /// <param name="context" type="Object" optional="false">The context object (this) in which the calling constructor is running.</param>
    /// <param name="values" type="Array" optional="false">Argument values that will be assigned to injected members (usually just provide "arguments" array like object).</param>
    /// <param name="exclude" type="String" optional="true">Comma separated list of parameter names to exclude from injection.</param>
    /// <param name="nestUnder" type="String" optional="true">Define whether injected parameters should be nested under a specific member (gets replaced if exists).</param>
    /// <param name="stripPrefix" type="Bool" optional="true">Set to true to strip "$" and "_" parameter name prefix when injecting members.</param>
    /// <field type="Object" name="defaults" static="true">Defines injectArguments defaults for optional parameters. These defaults can be overridden.</field>
{
    ...
}

Function.prototype.injectArguments.defaults = {
    /// <field type="String" name="exclude">Comma separated list of parameter names that should be excluded from injection (default "scope, $scope").</field>
    exclude: "scope, $scope",
    /// <field type="String" name="nestUnder">Member name that will be created and all injections will be nested within (default "injections").</field>
    nestUnder: "injections",
    /// <field type="Bool" name="stripPrefix">Defines whether parameter names prefixed with "$" or "_" should be stripped of this prefix (default <c>true</c>).</field>
    stripPrefix: true
};

I exclude $scope parameter injection as it should be data only without behaviour compared to services/providers etc. In my controllers I always assign $scope to this.model member even though I wouldn't even have to as $scope is automatically accessible in view.

Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • Really cool solution. I'm just thinking the fact that you used the dolar sign to identify adoptable parameters and that would bring some confusion with AngularJS properties. This is really close to the implementation on AngularJS core to find all injectable instances on a class when you don't defined them via $inject static method or ['injectable', fn(injectable)] signature. – William Lepinski Jan 08 '15 at 12:09
  • @WilliamLepinski: Well it's funny that you mention Angular. I'm actually using this in my Angular project because I define all my controllers/services etc using constructors and their prototype functions. In order for those prototype functions to also use constructor injected arguments I call `adoptArguments` hence making them accessible on `this` within any function defined on the prototype. And in Angular I want **all** of the arguments not just some, so I'm not doing any argument filtering. I just add them all (nested under `injections`). – Robert Koritnik Jan 08 '15 at 12:13
  • So you access the properties using this.injections.$scope for example? – William Lepinski Jan 08 '15 at 12:39
  • @WilliamLepinski: Yes more or less. All I do is strip out $ prefix. So it's rather `this.injections.routeParams`. And I also don't put scope into injections, as I put scope on `this.model`. Special case as it should be model only. Other injections are behaviour thus this separation. – Robert Koritnik Jan 08 '15 at 12:53
  • Check [your linked question](http://stackoverflow.com/questions/27838477/advanced-javascript-detecting-whether-current-function-is-being-called-by-a-con) to see a bug with your `adoptArguments` function. **Adopting arguments as prototype members will set values for every instance of Type 'X'** (i.e., Person) – laconbass Jan 08 '15 at 16:53
  • 1
    This is really neat. :) Adding it to the Function prototype is especially nice, as it makes the invocation far cleaner. – Jordan Gray Jan 09 '15 at 02:20
  • @laconbass: I've updated my answer providing a non-buggy and strict mode supported version of the same function. I had to introduce two parameters, but that's not too bad I guess. – Robert Koritnik Jan 09 '15 at 03:05
  • 1
    @RobertKoritnik I see. A quick review makes me think about the possibility of `.match(parser)` returning null if it doesn't match at all for whatever reason, but I suposse this could be a very strange case. – laconbass Jan 09 '15 at 03:16
  • 1
    @laconbass: if constructor function is valid and doesn't blow up Javascript engine, parser RegExp should always return a match. If it happens to anyone for whatever other reason that I haven't tested (i.e. identifies with unicode caharacter names - \xNNNN) all that would have to be adjusted is this regular expression. Everything else should work as expected. – Robert Koritnik Jan 09 '15 at 03:23
  • 1
    I was searching for some AOP libraries for JS I just ended up running into this: https://github.com/cujojs/meld/blob/master/docs/reference.md#advising-constructors. Looks like a great companion library to be used with your solution. We can create a pluggable solution to classes in ES6. – William Lepinski Jan 10 '15 at 11:54
5

For those who stumble upon this looking for Angular 1.x solution

Here's how it could work:

class Foo {
  constructor(injectOn, bar) {
    injectOn(this);
    console.log(this.bar === bar); // true
  }
}

And here's what injectOn service does under the hood:

.service('injectOn', ($injector) => {
  return (thisArg) => {
    if(!thisArg.constructor) {
      throw new Error('Constructor method not found.');
    }
   $injector.annotate(thisArg.constructor).map(name => {
      if(name !== 'injectOn' && name !== '$scope') {
        thisArg[name] = $injector.get(name);
      }
    });
  };
});

Fiddle link


Edit: Because $scope is not a service, we cannot use $injector to retrieve it. To my knowledge, it's not possible to retrieve it without re-instantiating a class. Hence why, if you inject it and need it outside constructor method, you'll need to assign it to this of your class manually.

Maciej Gurban
  • 5,615
  • 4
  • 40
  • 55
  • This is awesome, I was looking for an angular solution and this is exactly what I wanted – Jay Jun 04 '16 at 19:02
  • Sadly, this solution is faulty by design. It's expected that AngularJS injectables may have local dependencies that aren't available with `$injector.get`. `$scope` is only a special case that commonly occurs. – Estus Flask Aug 21 '18 at 21:25
5

There is no such feature in ES6 or any current ECMAScript specification. Any workarounds that involve constructor parameter parsing aren't reliable.

Function parameter names are expected to be minified in production:

class Foo {
  constructor(bar) {}
}

becomes

class o{constructor(o){}}

Parameter names are lost and cannot be used as property names. This limits the range of possible uses to environments that don't use minification, mostly server-side JavaScript (Node.js).

Parameters in transpiled classes parameters may differ from native classes, e.g. Babel transpiles

class Foo {
  constructor(a, b = 1, c) {}
}

to

var Foo = function Foo(a) {
    var b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
    var c = arguments[2];

    _classCallCheck(this, Foo);
};

Parameters with default values are excluded from parameter list. Native Foo.length is 1 but Babel makes Foo signature impossible to parse to get b and c names.

Node.js solution

This is a workaround that is applicable to native ES6 classes but not transpiled classes involves parameter parsing. It obviously won't work in minified application as well, this makes it primarily Node.js solution.

class Base {
  constructor(...args) {
    // only for reference; may require JS parser for all syntax variations
    const paramNames = new.target.toString()
    .match(/constructor\s*\(([\s\S]*?)\)/)[1]
    .split(',')
    .map(param => param.match(/\s*([_a-z][_a-z0-9]*)/i))
    .map(paramMatch => paramMatch && paramMatch[1]);

    paramNames.forEach((paramName, i) => {
      if (paramName)
        this[paramName] = args[i];
    });
  }
}

class Foo extends Base {
  constructor(a, b) {
    super(...arguments);
    // this.b === 2
  }
}

new Foo(1, 2).b === 2;

It can be rewritten in a form of decorator function that uses class mixin:

const paramPropsApplied = Symbol();

function paramProps(target) {
  return class extends target {
    constructor(...args) {
      if (this[paramPropsApplied]) return;
      this[paramPropsApplied] = true;
      // the rest is same as Base
    }
  }
}

And used in ES.next as a decorator:

@paramProps
class Foo {
  constructor(a, b) {
    // no need to call super()
    // but the difference is that 
    // this.b is undefined yet in constructor
  }
}

new Foo(1, 2).b === 2;

Or as helper function in ES6:

const Foo = paramProps(class Foo {
  constructor(a, b) {}
});

Transpiled or function classes can use third-party solutions like fn-args to parse function parameters. They may have pitfalls like default parameter values or fail with complex syntax like parameter destructuring.

General-purpose solution with annotated properties

A proper alternative to parameter name parsing is to annotate class properties for assignment. This may involve base class:

class Base {
  constructor(...args) {
    // only for reference; may require JS parser for all syntax variations
    const paramNames = new.target.params || [];

    paramNames.forEach((paramName, i) => {
      if (paramName)
        this[paramName] = args[i];
    });
  }
}

class Foo extends Base {
  static get params() {
    return ['a', 'b'];
  }

  // or in ES.next,
  // static params = ['a', 'b'];

  // can be omitted if empty
  constructor() {
    super(...arguments);
  }
}

new Foo(1, 2).b === 2;

Again, base class could be replace with a decorator. The same recipe is used in AngularJS to annotate functions for dependency injection in a way that is compatible with minification. Since AngularJS constructors are supposed to be annotated with $inject, the solution can be seamlessly applied to them.

TypeScript parameter properties

CoffeeScript @ can be implemented in TypeScript with constructor parameter properties:

class Foo {
  constructor(a, public b) {}
}

Which is syntactic sugar for ES6:

class Foo {
  constructor(a, b) {
    this.b = b;
  }
}

Since this transform is performed at compilation time, minification doesn't affect it in negative way.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565