Definir automaticamente argumentos como propriedades de instância no ES6

O CoffeeScript define automaticamente os argumentos como propriedades da instância no construtor se você prefixar os argumentos com @.

Existe algum truque para realizar o mesmo no ES6?

Script de suporte legado

Eu estendi protótipo de Function para dar access a auto-adoção de parâmetro para todos os construtores. Eu sei que deveríamos estar evitando adicionar funcionalidades a objects globais, mas se você souber o que está fazendo, tudo ficará bem.

Então aqui está a function adoptArguments :

 var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/g; var parser = /^function[^\(]*\(([^)]*)\)/i; var splitter = /\s*,\s*/i; Function.prototype.adoptArguments = function(context, values) { /// Injects calling constructor function parameters as constructed object instance members with the same name. /// The context object (this) in which the the calling function is running. /// Argument values that will be assigned to injected members (usually just provide "arguments" array like object). "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]; } }; 

A chamada resultante que adota todos os argumentos de chamada do construtor é agora a seguinte:

 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" 

Adotando apenas argumentos específicos

Minha solução superior adota todos os argumentos da function como membros de objects instanciados. Mas como você está se referindo ao CoffeeScript, você está tentando adotar apenas argumentos selecionados e não todos. Em identificadores Javascript que começam com @ são ilegais por especificação . Mas você pode prefixar com algo mais como $ ou _ que pode ser viável no seu caso. Agora, tudo que você precisa fazer é detectar essa convenção de nomenclatura específica e adicionar apenas os argumentos que passam nessa verificação:

 var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/g; var parser = /^function[^\(]*\(([^)]*)\)/i; var splitter = /\s*,\s*/i; Function.prototype.adoptArguments = function(context, values) { /// Injects calling constructor function parameters as constructed object instance members with the same name. /// The context object (this) in which the the calling function is running. /// Argument values that will be assigned to injected members (usually just provide "arguments" array like object). "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]; } } }; 

Feito. Funciona no modo estrito também. Agora você pode definir parâmetros de construtor prefixados e acessá-los como membros de objects instanciados.

Versão estendida para o cenário AngularJS

Na verdade eu escrevi uma versão ainda mais poderosa com a seguinte assinatura que implica seus poderes adicionais e é adequada para o meu cenário na minha aplicação AngularJS onde eu crio controller / service / etc. construtores e adicionar funções de protótipo adicionais a ele. Como os parâmetros nos construtores são injetados pelo AngularJS e eu preciso acessar esses valores em todas as funções do controlador, eu posso simplesmente acessá-los, via this.injections.xxx . Usando esta function torna muito mais simples do que escrever várias linhas adicionais, pois pode haver muitas injeções. Nem mencionar mudanças em injeções. Eu só tenho que ajustar os parâmetros do construtor e eu imediatamente consegui propagá-los dentro this.injections .

De qualquer forma. Assinatura prometida (implementação excluída).

 Function.prototype.injectArguments = function injectArguments(context, values, exclude, nestUnder, stripPrefix) { /// Injects calling constructor function parameters into constructed object instance as members with same name. /// The context object (this) in which the calling constructor is running. /// Argument values that will be assigned to injected members (usually just provide "arguments" array like object). /// Comma separated list of parameter names to exclude from injection. /// Define whether injected parameters should be nested under a specific member (gets replaced if exists). /// Set to true to strip "$" and "_" parameter name prefix when injecting members. /// Defines injectArguments defaults for optional parameters. These defaults can be overridden. { ... } Function.prototype.injectArguments.defaults = { /// Comma separated list of parameter names that should be excluded from injection (default "scope, $scope"). exclude: "scope, $scope", /// Member name that will be created and all injections will be nested within (default "injections"). nestUnder: "injections", /// Defines whether parameter names prefixed with "$" or "_" should be stripped of this prefix (default true). stripPrefix: true }; 

Eu excluo a injeção de parâmetro de $scope como deve ser somente dados sem comportamento comparado a serviços / provedores etc. Em meus controladores eu sempre atribuo $scope a este this.model mesmo que eu não precise nem como $scope é automaticamente acessível em Visão.

O comentário de Felix Kling delineia o mais próximo que você chegará de uma solução arrumada para isso. Ele usa dois resources do ES6 – Object.assign e o valor da propriedade literal do object .

Aqui está um exemplo com tree e pot como as propriedades da instância:

 class ChristmasTree { constructor(tree, pot, tinsel, topper) { Object.assign(this, { tree, pot }); this.decorate(tinsel, topper); } decorate(tinsel, topper) { // Make it fabulous! } } 

Claro, isso não é exatamente o que você queria; você ainda precisa repetir os nomes dos argumentos, por um lado. Eu tive que escrever um método auxiliar que poderia ser um pouco mais próximo …

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

Isso atribui automaticamente todos os parâmetros cujos nomes são prefixados com um sublinhado para as propriedades da instância:

 constructor(_tree, _pot, tinsel, topper) { // Equivalent to: Object.assign({ tree: _tree, pot: _pot }); Object.autoAssign(this, arguments); // ... } 

Suporta parâmetros de descanso, mas omiti suporte para parâmetros padrão. Sua versatilidade, associada às expressões regulares anêmicas do JS, dificulta o suporte a mais de um pequeno subconjunto deles.

Pessoalmente, eu não faria isso. Se houvesse uma maneira nativa de refletir sobre os argumentos formais de uma function, isso seria realmente fácil. Como está, é uma bagunça, e não me parece uma melhoria significativa sobre Object.assign .

Para aqueles que se deparam com isso procurando por solução Angular 1.x

Veja como isso poderia funcionar:

 class Foo { constructor(injectOn, bar) { injectOn(this); console.log(this.bar === bar); // true } } 

E aqui está o que o serviço do injetode faz sob o capô:

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

Ligação do violino


Edit: Como $scope não é um serviço, não podemos usar o $injector para recuperá-lo. Para meu conhecimento, não é possível recuperá-lo sem re-instanciar uma class. Por isso, se você injetar e precisar do método constructor externo, precisará atribuí-lo a this class manualmente.

Não existe tal recurso no ES6 ou em qualquer especificação atual do ECMAScript. Quaisquer soluções alternativas que envolvam a análise de parâmetros do construtor não são confiáveis.

Espera-se que os nomes dos parâmetros de function sejam minimizados na produção:

 class Foo { constructor(bar) {} } 

torna-se

 class o{constructor(o){}} 

Os nomes dos parâmetros são perdidos e não podem ser usados ​​como nomes de propriedades. Isso limita o intervalo de usos possíveis para ambientes que não usam minificação, principalmente JavaScript do lado do servidor (Node.js).

parameters nos parâmetros das classs transpiladas podem diferir das classs nativas, por exemplo, transpiles Babel

 class Foo { constructor(a, b = 1, c) {} } 

para

 var Foo = function Foo(a) { var b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var c = arguments[2]; _classCallCheck(this, Foo); }; 

parameters com valores padrão são excluídos da lista de parâmetros. O Foo.length nativo de Foo.length é 1, mas Babel torna impossível a assinatura de Foo para obter nomes b e c .

Solução Node.js

Esta é uma solução alternativa aplicável às classs ES6 nativas, mas as classs não transpiladas envolvem a análise de parâmetros. Obviamente não funcionará também no aplicativo reduzido, isso faz com que seja principalmente a solução 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; 

Ele pode ser reescrito em uma forma de function de decorador que usa a class 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 } } } 

E usado no ES.next como decorador:

 @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; 

Ou como function auxiliar no ES6:

 const Foo = paramProps(class Foo { constructor(a, b) {} }); 

Classes transpiled ou de function podem usar soluções de terceiros como fn-args para analisar parâmetros de function. Eles podem ter armadilhas como valores de parâmetros padrão ou falhar com syntax complexa, como a desestruturação de parâmetros.

Solução de propósito geral com propriedades anotadas

Uma alternativa apropriada para a análise de nomes de parâmetros é anotar propriedades de class para atribuição. Isso pode envolver class base:

 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; 

Novamente, a class base pode ser substituída por um decorador. A mesma receita é usada no AngularJS para anotar funções para injeção de dependência de maneira compatível com a minificação. Como os construtores AngularJS devem ser anotados com $inject , a solução pode ser aplicada a eles sem problemas .

Propriedades do parâmetro TypeScript

O CoffeeScript @ pode ser implementado em TypeScript com propriedades de parâmetro do construtor :

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

Qual é o açúcar sintático para ES6:

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

Como essa transformação é executada no tempo de compilation, a minificação não afeta de maneira negativa.