在ES6中自动将参数设置为实例属性

Wil*_*ski 23 javascript coffeescript ecmascript-6

如果在参数前面加上@,CoffeeScript会自动将参数设置为构造函数中的实例属性.

有什么技巧可以在ES6中完成同样的工作吗?

Jor*_*ray 22

费利克斯克林的评论概述了你最接近一个整洁的解决方案.它使用两个ES6功能 - Object.assign以及对象文字属性值的简写.

这是一个tree以及pot作为实例属性的示例:

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

    decorate(tinsel, topper) {
        // Make it fabulous!
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,这不是你想要的; 一方面,你仍然需要重复参数名称.我有一个编写辅助方法的方法可能会更接近......

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); }
};
Run Code Online (Sandbox Code Playgroud)

这会将名称带有下划线前缀的所有参数自动分配给实例属性:

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

它支持rest参数,但是我省略了对默认参数的支持.它们的多功能性加上JS的贫乏正则表达式,使得很难支持它们中的一小部分.

就个人而言,我不会这样做.如果有一种本地方式来反映函数的形式参数,那么这将非常简单.事实上,这是一个烂摊子,并没有打击我作为一个重大改进Object.assign.


Rob*_*nik 6

旧版支持脚本

我扩展了Function原型,以便为所有构造函数提供对参数自动采用的访问.我知道我们应该避免向全局对象添加功能,但是如果你知道你在做什么就可以了.

所以这是adoptArguments功能:

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];
    }
};
Run Code Online (Sandbox Code Playgroud)

采用所有构造函数调用参数的结果调用现在如下:

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"
Run Code Online (Sandbox Code Playgroud)

只采用特定的论点

我的上层解决方案采用所有函数参数作为实例化对象成员.但是,正如你所说的CoffeeScript,你试图采用的只是选择的参数而不是全部.在Javascript标识符开头@是非法规范.但是你可以在它们之前添加其他类似的东西$或者_在你的情况下可能是可行的.所以你现在需要做的就是检测这个特定的命名约定,只添加那些通过这个检查的参数:

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];
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

完成.也可以在严格模式下工作.现在,您可以定义前缀构造函数参数并将其作为实例化对象成员进行访问.

AngularJS场景的扩展版本

实际上我已经编写了一个更强大的版本,带有以下签名,暗示了它的附加功能,适用于我在我创建控制器/服务/等的AngularJS应用程序中的场景.构造函数并向其添加其他原型函数.由于构造函数中的参数是由AngularJS注入的,我需要在所有控制器函数中访问这些值,我可以通过它访问它们this.injections.xxx.使用此功能比使用多个额外的线更简单,因为可能有许多注射.甚至没有提到注射的变化.我只需要调整构造函数参数,然后立即将它们传播到内部this.injections.

无论如何.承诺签名(实施除外).

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
};
Run Code Online (Sandbox Code Playgroud)

我排除了$scope参数注入,因为它应该只是数据而没有与服务/提供者等相比的行为.在我的控制器中,我总是分配$scope给this.model成员,即使我甚至不需要$scope在视图中自动访问.


Mac*_*ban 5

对于那些偶然发现这个寻找Angular 1.x解决方案的人

以下是它的工作原理:

class Foo {
  constructor(injectOn, bar) {
    injectOn(this);
    console.log(this.bar === bar); // true
  }
}
Run Code Online (Sandbox Code Playgroud)

以下是injectOn服务的内幕:

.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);
      }
    });
  };
});
Run Code Online (Sandbox Code Playgroud)

小提琴链接


编辑: 因为$scope不是服务,我们无法使用$injector它来检索它.据我所知,如果不重新实例化一个类,就无法检索它.因此,如果你注入它并在constructor方法之外需要它,你需要this手动将它分配给你的类.


Est*_*ask 5

ES6 或任何当前的 ECMAScript 规范中都没有这样的功能。任何涉及构造函数参数解析的解决方法都是不可靠的。

函数参数名称预计在生产中被缩小:

class Foo {
  constructor(bar) {}
}
Run Code Online (Sandbox Code Playgroud)

变成

class o{constructor(o){}}
Run Code Online (Sandbox Code Playgroud)

参数名称丢失并且不能用作属性名称。这限制了不使用缩小的环境的可能使用范围,主要是服务器端 JavaScript (Node.js)。

转译类中的参数可能与原生类不同,例如Babel 转译

class Foo {
  constructor(a, b = 1, c) {}
}
Run Code Online (Sandbox Code Playgroud)

到

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

    _classCallCheck(this, Foo);
};
Run Code Online (Sandbox Code Playgroud)

具有默认值的参数不包括在参数列表中。NativeFoo.length是 1,但是 Babel 使得Foo签名无法解析以获取b和c名称。

Node.js 解决方案

这是一种解决方法,适用于原生 ES6 类,但不适用于涉及参数解析的转译类。显然它也无法在缩小的应用程序中工作,这使得它主要是 Node.js 解决方案。

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;
Run Code Online (Sandbox Code Playgroud)

它可以以使用类 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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

并在 ES.next 中用作装饰器:

@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;
Run Code Online (Sandbox Code Playgroud)

或者作为 ES6 中的辅助函数:

const Foo = paramProps(class Foo {
  constructor(a, b) {}
});
Run Code Online (Sandbox Code Playgroud)

转译或函数类可以使用第三方解决方案,例如fn-args解析函数参数。它们可能存在默认参数值等陷阱,或者因参数解构等复杂语法而失败。

具有注释属性的通用解决方案

参数名称解​​析的正确替代方法是注释类属性以进行赋值。这可能涉及基类:

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;
Run Code Online (Sandbox Code Playgroud)

同样,基类可以用装饰器替换。AngularJS 中使用相同的方法以与缩小兼容的方式注释依赖注入的函数。由于 AngularJS 构造函数应该用 进行注释$inject,因此该解决方案可以无缝地应用于它们。

TypeScript 参数属性

CoffeeScript可以通过构造函数参数属性@在 TypeScript 中实现:

class Foo {
  constructor(a, public b) {}
}
Run Code Online (Sandbox Code Playgroud)

这是 ES6 的语法糖:

class Foo {
  constructor(a, b) {
    this.b = b;
  }
}
Run Code Online (Sandbox Code Playgroud)

由于此转换是在编译时执行的,因此缩小不会对其产生负面影响。