1

Let's say I have a function that returns a function like this:

function createGreeter(logger) {
  return function greet(greeting) {
    logger.log(greeting + ', ' + this.name);
  }
}

And a class

class Person {
  constructor(name) {this.name = name;}
}

If I want to assign a greet method to the Person class that uses the console as the logger, I can think of several ways:

1.

class Person {
  constructor(name) {this.name = name;}

  greet(greeting) {
    return createGreeter(console).call(this, greeting);
  }
}

2.

class Person {
  constructor(name) {this.name = name;}
}

Person.prototype.greet = createGreeter(console);

However, I think both of these are somewhat ugly; 1) creates an, essentially, unnecessary wrapper method that simply binds this and calls the function, and 2) modifies the prototype outside of the class body which in my opinion makes the class API less clear.

Is there no clearer/shorter syntax for inline assignment and binding of an external function to a class method. I'm thinking something like:

class Person {
  constructor(name) {this.name = name;}

  greet: createGreeter(console)
}

...which resembles how you would be able to assign a function in an object literal. But this doesn't work, obviously. Is there something similar (now, or upcoming)?

Also, I'm wondering about the memory consumption and/or performance aspects of returning a closure as in 1) if the returned function is large. Every time the greet method is called on a Person object, a new function object will be created even though we always want to pass the same parameters (console) to it. So yet another approach could be to declare const consoleGreeter = createGreeter(console) before the class definition and implement greet as return consoleGreeter.call(this, greeting), but would it be worth it?

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
JHH
  • 8,567
  • 8
  • 47
  • 91
  • Can you create a snippet to replicate the issue? It works `Person.prototype.greet = createGreeter(console); var p1 = new Person( "abc" ); p1.greet( "10" );` – gurvinder372 Dec 19 '17 at 12:06
  • ?? I know it works. I was asking for a clearer or shorter way of accomplishing the same result. JS has a loooot of convenient shorthands, I was hoping I had missed one for this case. – JHH Dec 19 '17 at 12:09
  • You mentioned that *But this doesn't work, obviously* for `Person.prototype.greet = createGreeter(console);`. Can you replicate this? – gurvinder372 Dec 19 '17 at 12:10
  • The second approach of yours looks good to me. What kind of pitfalls that you found with that one? – Rajaprabhu Aravindasamy Dec 19 '17 at 12:11
  • 1
    @gurvinder372 The prototype approach is not the one he is talking about. It is working and it should. But he simply gave a sample code which is just for illustration and that one wont work, he said. – Rajaprabhu Aravindasamy Dec 19 '17 at 12:16
  • Exactly, thanks Rajaprabhu. My made up code "greet: createGreeter(console)" is what doesn't work. – JHH Dec 19 '17 at 12:20

3 Answers3

3
class Person {
  constructor(name) {this.name = name;}
}

Person.prototype.greet = createGreeter(console);

is the proper way to do this, unless there are other concerns (it will have problems in TypeScript).

This can be also done with class fields, which are stage 3 proposal and will likely land in ES2018:

class Person {
  greet = createGreeter(console);
}

Which is a shortcut for

class Person {
  constructor() {
    this.greet = createGreeter(console);
  }
}

First snippet evaluates createGreeter(console) once and assigns the method to class prototype. Second snippet evaluates it every time the class is instantiated and assigns the method to class instance, which is less effective.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • That is very interesting, it's actually pretty similar to my "made-up" code, but it's interesting to learn that the code will be evaluated upon construction as opposed to at declaration. I could of course store a `const consoleGreeter = createGreeter(console);` here as well to avoid the cost of creating the new function object every time, but I am definitely leaning towards the direct prototype manipulation to avoid having to do that. – JHH Dec 19 '17 at 13:14
  • See also this related answer, https://stackoverflow.com/a/46729995/3731501 . The case is really close to yours but is specific to TS to some degree, and OP deliberately tried to avoid direct `prototype` access. – Estus Flask Dec 19 '17 at 14:02
1

Ok, so here is an alternative to bind external method to class

class Person {
  constructor(name) {this.name = name;}
  get greet() { return createGreeter(console) }
}

Just checked, this is also working fine.

Vipin Kumar
  • 6,441
  • 1
  • 19
  • 25
  • Thanks, you and Patrick apparently answered essentially the same thing at the same time. Clever solution indeed. – JHH Dec 19 '17 at 13:10
1

This would achieve the same effect #1, since the wrapper is invoked each time the method is accessed. If that's okay with you to improve readability...

function createGreeter(logger) {
  return function greet(greeting) {
    logger.log(`${greeting}, ${this.name}`);
  };
}

class Person {
  constructor(name) {
    this.name = name;
  }

  get greet() {
    return createGreeter(console);
  }
}

let person = new Person('Patrick');

person.greet('Hello');
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • I have to admit this use of `get` caught me confused for a while; to have a property return a function is pretty clever indeed! And that it was automatically bound to `this` was also good to know. – JHH Dec 19 '17 at 13:09
  • Also appreciate the full, runnable code snippet, thanks! – JHH Dec 19 '17 at 13:15