在 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。
这是一个使用tree
和pot
作为实例属性的示例:
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
签名无法解析以获取 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;
可以改写成使用类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 中自动将参数设置为实例属性的主要内容,如果未能解决你的问题,请参考以下文章