1

I have two file index.js and actor.js

Index.js

const {Actors} = import("./Actors");

const act1= new Actors({
  name: 'Tony Stark',
  alias: 'Iron Man',
  gender: 'man',
  age: 38,
  powers: ["intelligence", "durability", "magnetism"]
})

const act2= new Actors({
  name: 'Natasha Romanoff',
  alias: 'Black Widow',
  gender: 'woman',
  age: 35,
  powers: ["agility", "martial arts"]
})

const examine = (avenger) => {
  console.count('Actors');
  console.group('*** Actors introduced ***');
  console.log(avenger.toString());
  console.groupEnd();
  console.group('*** Actors called ***');
  console.log(avenger());
  console.groupEnd();
  console.group('*** Actors\'s internals ***');
  console.log(avenger, '\n');
  console.groupEnd();
}

examine(act1);

examine(act2);

Actors.js

class Actors {
    constructor(obj) {
        this.name = obj.name;
        this.age = obj.age;
        this.alias = obj.alias;
        this.gender = obj.gender;
        this.powers = obj.powers;
    }


    toString() {
        return `name:${this.name} \ngender:${this.gender}\nage:${this.age}`;
    }
    avenger() {
        let str = '';
        this.powers.forEach(element => {
            str += `$element \n`;
        });
        return `${this.alias}` + str;
    }


}
module.exports.Actors= Actors;

How should I change class Actors, so that the function call avenger() in console.log(avenger()); works, and produces this expected result:

IRON MAN
intelligence
durability
magnetism
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • 1
    you are possibly looking for `console.log(avenger.avenger());` – eamanola Aug 11 '21 at 08:52
  • problem that I can only change Actors.js , and method avenger() it`s wrong my realization – Вадим Иващенко Aug 11 '21 at 09:10
  • FYI ... looking into the implementation of the `Actors` class constructor function and how `Actors` gets instantiated/used I would change the class name to `Avenger` and the constructor can be simplified to ... `class Avenger { constructor(obj) { return Object.assign(this, obj); } }`. The prototypal `avenger` method might be renamed to `performanceRecord` or just `record`. Then it becomes more clear what one is doing with an `Avenger` instance ... `const character1 = new Avenger( ... ); console.log( character1.record() );` – Peter Seliger Aug 11 '21 at 10:07
  • 2nd comment ... the implementation of the current prototypal `avenger` method is broken. – Peter Seliger Aug 11 '21 at 10:15
  • What is the expected output for `console.log(avenger, '\n');`? – trincot Aug 11 '21 at 10:56
  • @trincot [Function: Avenger] { toString: [Function (anonymous)]} – Вадим Иващенко Aug 11 '21 at 12:11
  • Your code is not outputting that either. So are you asking two questions here? – trincot Aug 11 '21 at 12:17
  • Is OP looking to make something like the built in `String` object, which can be called as both a class and a function? – lejlun Aug 11 '21 at 18:35
  • @lejlun ... 1/2 ... until now it seems that the OP wants to have a callable type, an `Actors` instance (I assume more an `avenger` type/object) which holds all the properties, but also is callable itself like a function object. On top of that, from the OP's provided example code, the implementation of the prototypal `avenger` method is broken. Thus it was not clear either whether the OP was looking for that. The result of calling the instance directly should be equal to the one which a less error prone `avenger` method does/would return. – Peter Seliger Aug 11 '21 at 21:44
  • @lejlun ... 2/2 ... In addition it was not clear in the beginning whether the OP was confused about how to invoke `avenger` from, within the `examine` function either `avenger()` or `avenger.avenger()`. To most people it first looked like a `ReferenceError` problem. – Peter Seliger Aug 11 '21 at 21:45
  • @ВадимИващенко ... From all the provided answers, are there any questions left? – Peter Seliger Aug 13 '21 at 10:43
  • fyi ... There is another thread on this matter with a high rated in-depth answer ... [How to extend Function with ES6 classes?](https://stackoverflow.com/questions/36871299/how-to-extend-function-with-es6-classes) ... and an even far older thread ... [Constructor for callable object in JavaScript](https://stackoverflow.com/questions/12656079/constructor-for-callable-object-in-javascript) ... of cause mostly based on a now outdated/discouraged `__proto__` approach. – Peter Seliger Aug 13 '21 at 14:15

3 Answers3

2

If you really need an instance of Actors to be callable, then that constructor must return a function object. There are essentially two ways to do that:

  • The constructor creates a local function, assigns all other properties to that object, and returns it. To make sure that this function object identifies as an Actors instance, you would need to change its prototype from Function to Actors.

  • Make Actors a subclass of Function, and call super to make sure the instance executes the code we need it to execute. If we want that code to dynamically access the other properties of the instance, we must overcome the fact that the function will be called without specific this binding. So either:

    • we need to bind that function beforehand and let the constructor return that function, or
    • we get a reference to the executing function itself, using the deprecated arguments.callee reference.

Either way, it is going to be ugly, but I suppose this was a code challenge. Still, you should not really use this pattern in serious coding.

There is another obstacle here: name is a read-only property of the Function prototype, and so you cannot just assign it a new value with a property accessor. Instead you need to be explicit in stating that you want the instance to get its own property with that name.

1. Return local function object

Here is a working solution using the first option:

class Actors {
    constructor(obj) {
        const Avenger = () => `${Avenger.alias.toUpperCase()}\n${Avenger.powers.join("\n")}`;
        Avenger.toString = () => `name:${Avenger.name} \ngender:${Avenger.gender}\nage:${Avenger.age}`;
        const {name, ...rest} = obj;
        Object.defineProperty(Avenger, "name", {value: name});
        Object.assign(Avenger, rest);
        Object.setPrototypeOf(Avenger, Actors.prototype);
        return Avenger;
    }
}


const act1= new Actors({
  name: 'Tony Stark',
  alias: 'Iron Man',
  gender: 'man',
  age: 38,
  powers: ["intelligence", "durability", "magnetism"]
})

const act2= new Actors({
  name: 'Natasha Romanoff',
  alias: 'Black Widow',
  gender: 'woman',
  age: 35,
  powers: ["agility", "martial arts"]
})

const examine = (avenger) => {
  console.count('Actors');
  console.group('*** Actors introduced ***');
  console.log(avenger.toString());
  console.groupEnd();
  console.group('*** Actors called ***');
  console.log(avenger());
  console.groupEnd();
  console.group('*** Actors\'s internals ***');
  console.log(avenger, '\n');
  console.groupEnd();
  console.log(avenger instanceof Actors);
}

examine(act1);
examine(act2);

Note that Stack Snippets has its own implementation of console, and when you run the above snippet, the output of console.log(avenger) is not the source of the function, like you will get elsewhere (Chrome, Firefox, ...).

2. Subclass Function

Here is a possible implementation of the second option I listed above.

Advantages:

  • Does not alter prototypes of existing objects
  • Constructor does not return a different object, but this
  • As a consequence of the previous point: Actors prototype methods can be used on the instance

Disadvantages:

  • Function constructor is called -- necessarily with a string argument, which leads to runtime code parsing every time the constructor is called (unless the engine applies some optimisation).
  • Deprecated arguments.callee is used. This is needed because the function will be called without this binding, and so the only available reference to the instance is this callee reference.
  • The source of the function will show an anonymous function, not one with the name Avenger (a request that was clarified in comments).

Here it is:

class Actors extends Function {
    constructor(obj) {
        super(`return arguments.callee.avenger()`);
        let {name, ...rest} = obj;
        Object.defineProperty(this, "name", {value: name});
        Object.assign(this, rest);
    }
    toString() {
        return `name: ${this.name}\ngender: ${this.gender}\nage: ${this.age}`;
    }
    avenger() {
        return `${this.alias.toUpperCase()}\n${this.powers.join("\n")}`;
    }
}

const act1= new Actors({
  name: 'Tony Stark',
  alias: 'Iron Man',
  gender: 'man',
  age: 38,
  powers: ["intelligence", "durability", "magnetism"]
})

const act2= new Actors({
  name: 'Natasha Romanoff',
  alias: 'Black Widow',
  gender: 'woman',
  age: 35,
  powers: ["agility", "martial arts"]
})

const examine = (avenger) => {
  console.count('Actors');
  console.group('*** Actors introduced ***');
  console.log(avenger.toString());
  console.groupEnd();
  console.group('*** Actors called ***');
  console.log(avenger());
  console.groupEnd();
  console.group('*** Actors\'s internals ***');
  console.log(avenger, '\n');
  console.groupEnd();
  console.log(avenger instanceof Actors);
}

examine(act1);
examine(act2);

None of the available options represent good coding practice. There is no good reason why you would want a constructor to behave as you require in the question, except when this answers some nifty code challenge, which has no other purpose than that.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Writing the code like this renders the existence of an `Actors` class entirely useless. The latter now is merely a crippled factory function, an empty envelop (no prototypal methods anymore) cheating about an instance's true type. The instance itself still can be identified as arrow function but it denies being a `Function` instance, instead it dishonestly claimes to be an instance of `Actors`. But of cause *trincot* knows that. I address this comment to the OP ... @ВадимИващенко. – Peter Seliger Aug 11 '21 at 16:48
  • @ВадимИващенко ... 1/2 ... Dear OP, if one finds oneself with the need of type designs like the one above, one might rethink ones approach. One should not abuse a true *class* based approach for this kind of needs. Why ist that? The advantages of using *class* syntax comes with the easy design of a specific type or even type systems. For both, if one especially makes use of prototypal methods. For the latter one does always enjoy the easiness of sub-typing/classing. – Peter Seliger Aug 11 '21 at 16:57
  • @ВадимИващенко ... 2/2 ... Unusual but valid design approaches like with the callable behavior of an `Actors` object should not be obfuscated by *class* envelopes. A factory function which returns either a classic function or an arrow function as totally valid `actor` object and also does take care of preserving such an object's state and does also glue all the needed methods to it, is in my opinion the far more readable and honest solution. – Peter Seliger Aug 11 '21 at 17:05
  • 1
    I agree with all that Peter writes above. As I had mentioned two options in my initial answer, I have now also added a section that expands on that second option. Still, this uses some tools that are not in line with best practices. We claim here that what you want, @ВадимИващенко, is not what you *should* want. Any solution that fulfils your question's requirements is doomed to use some bad practice. There is no good reason to want such behaviour from a constructor. I assume you ask this in the context of some tricky code challenge, without intentions to actually *use* this pattern. – trincot Aug 11 '21 at 18:56
  • 1
    Well explained, and your writing to me always sound less harsh than what I do provide. I think the until now explained / shown 4 approaches pretty much cover what can be done on the OP's matter. ... And yet a note to the OP ... with the second approach from above which does extend `Function`, an instance like `act1` is callable. It of cause has a callable slot `avanger` which holds the implementation thus `act1()` and `act1.avenger()` both return the same result. – Peter Seliger Aug 11 '21 at 21:18
1

Instead of

console.log(avenger());

try to call

console.log(avenger.avenger());
Mateusz
  • 175
  • 9
1

Since the OP's real problem besides the incorrect targeting of avenger() versus avenger.avenger() seemed to be the wrongly implemented prototypal avenger method itself, I do not only recommend fixing the implementation of the latter but also a refactoring and/or renaming of the class and its methods (see my comments on the OP's question above).

A code refactoring towards a more explicit wording, including code optimization for the constructor function and the prototypal toString method as well as fixing the former prototypal avenger method and renaming it to record, could result in something similar to the next provided code ...

class Avenger {
  constructor(obj) {
    return Object.assign(this, obj); 
  }
  toString() {
    return [

      `name: ${ this.name }`,
      `gender: ${ this.gender }`,
      `age: ${ this.age }`,

    ].join('\n');
  }
  record() {
    return [
    
      this.alias.toUpperCase(),
      this.powers.join('\n'),

    ].join('\n');
  }
}

const character1 = new Avenger({
  name: 'Tony Stark',
  alias: 'Iron Man',
  gender: 'man',
  age: 38,
  powers: ["intelligence", "durability", "magnetism"]
});
const character2 = new Avenger({
  name: 'Natasha Romanoff',
  alias: 'Black Widow',
  gender: 'woman',
  age: 35,
  powers: ["agility", "martial arts"]
});

const examineAvenger = (avenger) => {
  console.count('Avenger');
  console.group('*** Avenger introduced ***');
  // console.log(avenger.toString());
  // console.log(String(avenger));
  console.log(avenger + '');
  console.groupEnd();
  console.group('*** Avenger called ***');
  console.log(avenger.record());
  console.groupEnd();
  console.group('*** Avenger\'s internals ***');
  console.log(avenger, '\n');
  console.groupEnd();
}

examineAvenger(character1);
examineAvenger(character2);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit

Having commented heavily on the class based (ab)use of how to provide callable types, I hereby provide a factory function based approach which does not obfuscate the true nature of a callable avenger object ...

function toAvengerString(state) {
  return [

    `name: ${ state.name }`,
    `gender: ${ state.gender }`,
    `age: ${ state.age }`,

  ].join('\n');
}
function toAvengerRecord(state) {
  return [

    state.alias.toUpperCase(),
    state.powers.join('\n'),

  ].join('\n');
}

function createAvenger(initialState) {
  // does create closures over/for each callable avenger ('s `localState`).

  // loosely decouple the `localState` from its initial configuration object.
  const localState = Object.assign({}, initialState);

  // create the callable avenger's behavior
  // as forwarder to a single outer `toAvengerRecord` function.
  const callableAvenger = () => toAvengerRecord(localState);

  // implement the `toString` behavior
  // as forwarder to a single outer `toAvengerString` function.
  Object.defineProperty(callableAvenger, 'toString', {
    value: () => toAvengerString(localState),
  });
  // implement any avenger object's property by iterating
  // over the own properties of the initially provided state
  // by creating setter/getter functionality for each property
  // which read from and write to the encapsulated `localState`.
  Object
    .getOwnPropertyNames(localState)
    .forEach(key => Object.defineProperty(callableAvenger, key, {
      set: value => localState[key] = value,
      get: () => localState[key],
      enumerable: true,
    }));

  // return the custom writable and callable avenger type.
  return callableAvenger;
}

const character1 = createAvenger({
  name: 'Tony Stark',
  alias: 'Iron Man',
  gender: 'man',
  age: 38,
  powers: ["intelligence", "durability", "magnetism"]
});
const character2 = createAvenger({
  name: 'Natasha Romanoff',
  alias: 'Black Widow',
  gender: 'woman',
  age: 35,
  powers: ["agility", "martial arts"]
});

const examineAvenger = (avenger) => {
  console.count('Avenger');
  console.group('*** Avenger introduced ***');
  // console.log(avenger.toString());
  // console.log(String(avenger));
  console.log(avenger + '');
  console.groupEnd();
  console.group('*** Avenger called ***');
  console.log(avenger());
  console.groupEnd();
  console.group('*** Avenger\'s internals ***');
  console.log(avenger, '\n');
  console.groupEnd();
}

examineAvenger(character1);
examineAvenger(character2);

console.log(
  "character1.name = 'Mark Ruffalo' ...",
  character1.name = 'Mark Ruffalo'
);
console.log(
  "character1.alias = 'Hulk' ...",
  character1.alias = 'Hulk'
);
console.log(
  "character1.gender = 'male' ...",
  character1.gender = 'male'
);
console.log(
  "character1.age = 'not important' ...",
  character1.age = 'not important'
);
console.log(
  "character1.powers = ['super human', 'super smart', 'super strong', 'super durable'] ...",
  character1.powers = ['super human', 'super smart', 'super strong', 'super durable']
);
examineAvenger(character1);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37