Подтвердить что ты не робот

Автоматически задавать аргументы как свойства экземпляра в ES6

CoffeeScript автоматически устанавливает аргументы как свойства экземпляра в конструкторе, если вы префикс аргументов с помощью @.

Есть ли какой-либо трюк, чтобы сделать то же самое в ES6?

4b9b3361

Ответ 1

Поддержка устаревших script

Я расширил прототип 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.

В любом случае. Обещанная подпись (исключение реализации).

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 автоматически открывается.

Ответ 2

Комментарий 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.

Ответ 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);
      }
    });
  };
});

Ссылка на скрипт


Edit: Поскольку $scope не является сервисом, мы не можем использовать $injector для его получения. Насколько мне известно, его невозможно восстановить без повторного создания экземпляра класса. Поэтому, если вы вставляете его и нуждаетесь в нем вне метода constructor, вам необходимо назначить его this вашего класса вручную.

Ответ 4

В 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.

Решение 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;
  }
}

Поскольку это преобразование выполняется во время компиляции, минимизация не влияет на него отрицательным образом.