CoffeeScript автоматически устанавливает аргументы как свойства экземпляра в конструкторе, если вы префикс аргументов с помощью @.
Есть ли какой-либо трюк, чтобы сделать то же самое в ES6?
CoffeeScript автоматически устанавливает аргументы как свойства экземпляра в конструкторе, если вы префикс аргументов с помощью @.
Есть ли какой-либо трюк, чтобы сделать то же самое в ES6?
Я расширил прототип 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
.
В любом случае. Обещанная подпись (исключение реализации).
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
автоматически открывается.
Комментарий 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
.
Для тех, кто наткнулся на это, ищет решение 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);
}
});
};
});
Edit:
Поскольку $scope
не является сервисом, мы не можем использовать $injector
для его получения. Насколько мне известно, его невозможно восстановить без повторного создания экземпляра класса. Поэтому, если вы вставляете его и нуждаетесь в нем вне метода constructor
, вам необходимо назначить его this
вашего класса вручную.
В ES6 или любой текущей спецификации ECMAScript такой функции нет. Любые обходные пути, которые связаны с разбором параметров конструктора, ненадежны.
Ожидается, что имена функциональных параметров будут уменьшены в производстве:
class Foo {
constructor(bar) {}
}
становится
class o{constructor(o){}}
Имена параметров теряются и не могут использоваться как имена свойств. Это ограничивает диапазон возможных применений для сред, которые не используют минимизацию, в основном серверный JavaScript (Node.js).
Параметры параметров transpiled classes могут отличаться от исходных классов, например, 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);
};
Параметры со значениями по умолчанию исключаются из списка параметров. Native Foo.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;
}
}
Поскольку это преобразование выполняется во время компиляции, минимизация не влияет на него отрицательным образом.