如果您使用@ 作为参数的前缀,CoffeeScript 会自动将参数设置为构造函数中的实例属性。
在 ES6 中是否有任何技巧可以完成相同的操作?
如果您使用@ 作为参数的前缀,CoffeeScript 会自动将参数设置为构造函数中的实例属性。
在 ES6 中是否有任何技巧可以完成相同的操作?
Felix Kling 的评论概述了最接近此问题的整洁解决方案。它使用两个 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!
}
}
当然,这并不是你真正想要的;一方面,您仍然需要重复参数名称。我尝试编写一个可能更接近的辅助方法……
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
.
我已经扩展了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 注入的,我需要在所有控制器函数中访问这些值,我可以通过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
参数注入,因为与服务/提供者等相比,它应该是没有行为的数据。在我的控制器中,我总是分配$scope
给this.model
成员,即使我什至不必像$scope
视图中自动访问的那样。
对于那些偶然发现这个寻找 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
手动将它分配给您的类。
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
签名无法解析以获取b
和c
名称。
这是一种适用于原生 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
,因此该解决方案可以无缝地应用于它们。
CoffeeScript@
可以在带有构造函数参数属性的TypeScript 中实现:
class Foo {
constructor(a, public b) {}
}
这是 ES6 的语法糖:
class Foo {
constructor(a, b) {
this.b = b;
}
}
由于此转换是在编译时执行的,因此缩小不会对其产生负面影响。