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

Posted

技术标签:

【中文标题】在 ES6 中自动将参数设置为实例属性【英文标题】:Automatically set arguments as instance properties in ES6 【发布时间】:2015-02-16 05:35:50 【问题描述】:

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

在 ES6 中有什么技巧可以做到这一点吗?

【问题讨论】:

不,但你可以这样做Object.assign(this, arg, u, ments); 这可能是捷径,但我必须以任何一种方式输入参数名称。我认为我们需要等到 Annotations 活跃起来才能在 javascript 中执行某种 AOP。无论如何,谢谢你的回答。 您可以将参数作为对象传递。那么只需说constructor(options) Object.assign(this, options); @torazaburo 不幸的是,如果人们使用 AngularJS,这不是一个选项!好主意,我自己用过几次 @WilliamLepinski 你知道如何在没有打字稿的普通 es6+ 中做到这一点吗?看来我有同样的问题 - 如何防止 angularjs 构造函数中的依赖项重复。打算编写一个使用构造函数参数的装饰器,但构造函数中没有参数,而且很可能不包含命名参数。 【参考方案1】:

Felix Kling's comment 概述了您将获得的最接近整洁的解决方案。它使用了两个 ES6 特性——Object.assign 和 object literal property value shorthand。

这是一个使用treepot 作为实例属性的示例:

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

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


当然,这并不是你真正想要的;一方面,您仍然需要重复参数名称。我尝试编写一个可能更接近的辅助方法……

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); 
;

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

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

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

就我个人而言,我不会这样做。如果有一种本地方式来反映函数的形式参数,这将非常容易。事实上,它是一团糟,我觉得它对Object.assign 的改进并不显着。

【讨论】:

我会试一试并发布结果。【参考方案2】:

旧版支持脚本

我扩展了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];
    
;

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

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"

仅采用特定参数

我的上层解决方案采用所有函数参数作为实例化对象成员。但是,当您提到 CoffeeScript 时,您只是尝试采用选定的参数,而不是全部。在以@ 开头的Javascript 标识符中是illegal by specification。但是您可以在它们前面加上 $_ 之类的东西,这在您的情况下可能是可行的。所以现在你要做的就是检测这个特定的命名约定并只添加那些通过这个检查的参数:

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];
        
    
;

完成。也可以在严格模式下工作。现在您可以定义带前缀的构造函数参数并将它们作为您的实例化对象成员访问。

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
;

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

【讨论】:

非常酷的解决方案。我只是在想你使用美元符号来识别可采用的参数这一事实,这会给 AngularJS 属性带来一些混乱。当您没有通过 $inject 静态方法或 ['injectable', fn(injectable)] 签名定义它们时,这非常接近 AngularJS 核心上的实现来查找类上的所有可注入实例。 这真是太好了。 :) 将它添加到 Function 原型中特别好,因为它使调用更加清晰。 @RobertKoritnik 我明白了。快速回顾让我想到 .match(parser) 如果由于某种原因根本不匹配,返回 null 的可能性,但我想这可能是一个非常奇怪的情况。 @laconbass:如果构造函数有效且不会破坏 Javascript 引擎,则解析器 RegExp 应始终返回匹配项。如果由于我没有测试过的任何其他原因(即使用 unicode 字符名称 - \xNNNN 识别)发生在任何人身上,那么所有必须调整的就是这个正则表达式。其他一切都应该按预期工作。 我正在为 JS 搜索一些 AOP 库,但结果却遇到了这个问题:github.com/cujojs/meld/blob/master/docs/…。看起来像是与您的解决方案一起使用的绝佳配套库。我们可以为 ES6 中的类创建一个可插拔的解决方案。【参考方案3】:

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

它的工作原理如下:

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

这就是 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);
      
    );
  ;
);

Fiddle link


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

【讨论】:

这太棒了,我正在寻找一个角度解决方案,这正是我想要的 遗憾的是,这个解决方案在设计上有缺陷。预计 AngularJS 注入可能具有 $injector.get 不可用的本地依赖项。 $scope 只是一种常见的特殊情况。【参考方案4】:

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

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

class Foo 
  constructor(bar) 

变成

class oconstructor(o)

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

转译类参数中的参数可能与本机类不同,例如Babel transpiles

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

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

    _classCallCheck(this, Foo);
;

具有默认值的参数从参数列表中排除。原生 Foo.length 为 1,但 Babel 使 Foo 签名无法解析以获取 bc 名称。

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;

可以改写成使用类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
    
  

并在 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;

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

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

转译或函数类可以使用第三方解决方案如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;

同样,基类可以替换为装饰器。相同的配方是used in AngularJS to annotate functions for dependency injection,其方式与缩小兼容。由于AngularJS的构造函数应该用$inject注释,解决方案can be seamlessly applied to them。

TypeScript 参数属性

CoffeeScript @ 可以用 constructor parameter properties 在 TypeScript 中实现:

class Foo 
  constructor(a, public b) 

这是 ES6 的语法糖:

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

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

【讨论】:

以上是关于在 ES6 中自动将参数设置为实例属性的主要内容,如果未能解决你的问题,请参考以下文章

ES6 Class(类)

5.4 批量编号

ES6函数扩展

Python 获取类属性

ES6 函数拓展

ES6对象的新增方法