programacion orientada objetos objeto es6 ejemplos ecmascript clases javascript coffeescript ecmascript-6

orientada - objeto window javascript ejemplos



Establecer automáticamente argumentos como propiedades de instancia en ES6 (4)

Script de soporte heredado

He ampliado el prototipo de Function para dar acceso a la adopción automática de parámetros a todos los constructores. Sé que deberíamos evitar agregar funcionalidad a los objetos globales, pero si sabes lo que estás haciendo puede estar bien.

Así que aquí está la función 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]; } };

La llamada resultante que adopta todos los argumentos de llamada del constructor es ahora como sigue:

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"

Adoptando sólo argumentos específicos.

Mi solución superior adopta todos los argumentos de función como miembros de objeto instanciados. Pero como te refieres a CoffeeScript, estás tratando de adoptar solo argumentos seleccionados y no todos. En Javascript los identificadores que comienzan con @ son ilegales por especificación . Pero puede prefijarlos con otra cosa como $ o _ que puede ser factible en su caso. Así que ahora todo lo que tiene que hacer es detectar esta convención de nomenclatura específica y solo agregar los argumentos que pasan esta verificación:

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

Hecho. Funciona en modo estricto también. Ahora puede definir parámetros de constructor prefijados y acceder a ellos como miembros de objeto instanciados.

Versión extendida para el escenario AngularJS

En realidad, he escrito una versión aún más poderosa con la siguiente firma que implica sus poderes adicionales y es adecuada para mi escenario en mi aplicación AngularJS donde creo un controlador / servicio / etc. Constructores y añadir funciones de prototipo adicionales. Como los parámetros en los constructores son inyectados por AngularJS y necesito acceder a estos valores en todas las funciones del controlador, simplemente puedo acceder a ellos, a través de this.injections.xxx . El uso de esta función lo hace mucho más simple que escribir varias líneas adicionales, ya que puede haber muchas inyecciones. Ni siquiera mencionar los cambios a las inyecciones. Solo tengo que ajustar los parámetros del constructor e inmediatamente los propagaré dentro de this.injections .

De todas formas. Firma prometida (implementación excluida).

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

Excluyo la inyección del parámetro $scope , ya que debería ser solo datos sin comportamiento en comparación con los servicios / proveedores, etc. En mis controladores, siempre asigno $scope a this.model miembro de este this.model , aunque no tendría que hacerlo porque $scope se puede acceder automáticamente en ver.

CoffeeScript establece automáticamente los argumentos como propiedades de instancia en el constructor si prefieres los argumentos con @.

¿Hay algún truco para lograr lo mismo en ES6?


El comentario de Felix Kling describe lo más cerca que se puede encontrar de una solución ordenada para esto. Utiliza dos características de ES6: Object.assign y el valor de la propiedad literal del objeto .

Aquí hay un ejemplo con tree y pot como las propiedades de la instancia:

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

Por supuesto, esto no es realmente lo que querías; Todavía necesita repetir los nombres de los argumentos, por un lado. Intenté escribir un método de ayuda que podría estar un poco más cerca ...

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

Esto asigna automáticamente cualquier parámetro cuyos nombres tengan como prefijo un guión bajo a las propiedades de la instancia:

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

Es compatible con los parámetros de resto, pero omití la compatibilidad con los parámetros predeterminados. Su versatilidad, junto con las expresiones regulares anémicas de JS, hace que sea difícil soportar más que un pequeño subconjunto de ellas.

Personalmente, yo no haría esto. Si hubiera una manera nativa de reflexionar sobre los argumentos formales de una función, esto sería realmente fácil. Tal como está, es un desastre, y no me parece una mejora significativa sobre Object.assign .


No hay tal característica en ES6 ni en ninguna especificación actual de ECMAScript. Cualquier solución que implique el análisis de parámetros del constructor no es confiable.

Se espera que los nombres de los parámetros de función se minimicen en producción:

class Foo { constructor(bar) {} }

se convierte en

class o{constructor(o){}}

Los nombres de los parámetros se pierden y no se pueden usar como nombres de propiedades. Esto limita el rango de usos posibles para los entornos que no utilizan la minificación, principalmente JavaScript del lado del servidor (Node.js).

Los parámetros en las clases transpiladas los parámetros pueden diferir de las clases nativas, por ejemplo, las transferencias de Babel

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

a

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

Los parámetros con valores predeterminados se excluyen de la lista de parámetros. El Foo.length nativo es 1, pero Babel hace que la firma de Foo sea ​​imposible de analizar para obtener los nombres de b y c .

Solución de Node.js

Esta es una solución alternativa que es aplicable a las clases nativas de ES6, pero no a las clases transpiladas implica el análisis de parámetros. Obviamente, tampoco funcionará en una aplicación minificada, esto lo convierte principalmente en la solución 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;

Se puede reescribir en una forma de función decoradora que usa la mezcla de clase:

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

Y usado en 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;

O como función auxiliar en ES6:

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

Transpiled o clases de función pueden usar soluciones de terceros como fn-args para analizar los parámetros de la función. Pueden tener dificultades como valores de parámetros predeterminados o fallar con una sintaxis compleja como la desestructuración de parámetros.

Solución de propósito general con propiedades anotadas.

Una alternativa adecuada al análisis del nombre del parámetro es anotar las propiedades de la clase para la asignación. Esto puede implicar la clase 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;

De nuevo, la clase base podría ser reemplazada por un decorador. La misma receta se usa en AngularJS para anotar funciones para la inyección de dependencia de una manera que sea compatible con la minificación. Dado que se supone que los constructores de AngularJS están anotados con $inject , la solución se puede aplicar sin problemas a ellos .

Propiedades de los parámetros de TypeScript

CoffeeScript @ se puede implementar en TypeScript con propiedades de parámetros de constructor :

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

Que es el azúcar sintáctico para ES6:

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

Dado que esta transformación se realiza en el momento de la compilación, la minificación no la afecta de forma negativa.


Para aquellos que se topan con esto buscando la solución Angular 1.x.

Así es como podría funcionar:

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

Y esto es lo que hace el servicio InjectOn bajo el 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); } }); }; });

Enlace violín

Editar: Como $scope no es un servicio, no podemos usar $injector para recuperarlo. Que yo sepa, no es posible recuperarlo sin volver a crear una instancia de una clase. Por lo tanto, si lo inyectas y lo necesitas fuera del método del constructor , deberás asignarlo a this de tu clase manualmente.