Automatically set arguments as instance properties in ES6

前端 未结 4 956
孤街浪徒
孤街浪徒 2020-11-27 19:27

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

Is there any trick to accomplish the sam

4条回答
  •  無奈伤痛
    2020-11-27 20:03

    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.

提交回复
热议问题