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

IT技术 javascript coffeescript ecmascript-6
2021-01-28 02:43:11

如果您使用@ 作为参数的前缀,CoffeeScript 会自动将参数设置为构造函数中的实例属性。

在 ES6 中是否有任何技巧可以完成相同的操作?

4个回答

Felix Kling 的评论概述了最接近此问题的整洁解决方案。它使用两个 ES6 特性——Object.assign以及对象字面量属性值简写

这是一个使用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);
    // ...
}

它支持其余参数,但我省略了对默认参数的支持。它们的多功能性,再加上 JS 的乏味正则表达式,使得它很难支持超过它们的一小部分。

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

我会对此进行测试并发布结果。
2021-04-02 02:43:11

旧版支持脚本

我已经扩展了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 中,以 开头的标识符在规范中@非法的但是您可以使用其他类似$_在您的情况下可能可行的内容作为前缀所以现在你所要做的就是检测这个特定的命名约定,只添加那些通过这个检查的参数:

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.

反正。Promise的签名(不包括实施)。

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参数注入,因为与服务/提供者等相比,它应该是没有行为的数据。在我的控制器中,我总是分配$scopethis.model成员,即使我什至不必像$scope视图中自动访问的那样。

我正在寻找的JS我一些AOP库刚刚结束了运行到这一点:github.com/cujojs/meld/blob/master/docs/...看起来是一个很棒的配套库,可以与您的解决方案一起使用。我们可以为 ES6 中的类创建一个可插入的解决方案。
2021-03-15 02:43:11
@RobertKoritnik 我明白了。快速回顾让我想到.match(parser)如果它因任何原因根本不匹配则返回 null的可能性,但我认为这可能是一个非常奇怪的情况。
2021-03-22 02:43:11
非常酷的解决方案。我只是在想这样一个事实,即您使用美元符号来识别可采用的参数,这会给 AngularJS 属性带来一些混淆。当您没有通过 $inject 静态方法或 ['injectable', fn(injectable)] 签名定义它们时,这非常接近 AngularJS 核心上的实现来查找类上的所有可注入实例。
2021-03-24 02:43:11
@laconbass:如果构造函数有效并且不会炸毁 Javascript 引擎,则解析器 RegExp 应始终返回匹配项。如果由于我尚未测试的任何其他原因(即使用 unicode 字符名称 - \xNNNN 标识)而发生在任何人身上,那么所有需要调整的就是这个正则表达式。其他一切都应该按预期工作。
2021-03-28 02:43:11
这真的很整洁。:) 将它添加到 Function 原型中特别好,因为它使调用更加清晰。
2021-04-14 02:43:11

对于那些偶然发现这个寻找 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);
      }
    });
  };
});

小提琴链接


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

遗憾的是,这个解决方案在设计上是有问题的。预计 AngularJS 可注入对象可能具有本地依赖项,而$injector.get. $scope只是经常发生的特殊情况。
2021-03-30 02:43:11
这太棒了,我正在寻找一个有角度的解决方案,这正是我想要的
2021-04-03 02:43:11

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

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

class Foo {
  constructor(bar) {}
}

变成

class o{constructor(o){}}

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

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

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

从参数列表中排除具有默认值的参数。NativeFoo.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;

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

TypeScript 参数属性

CoffeeScript@可以在带有构造函数参数属性的TypeScript 中实现

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

这是 ES6 的语法糖:

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

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