1

My attempt:

class SetOnceDict extends Function {
  constructor() {
    super('key', 'return this.get(key);')
  }
  items = {}
  add(key, value) {
    if (!this.items.hasOwnProperty(key)) {
      this.items[key] = value;
    } else {
      throw new Error(`Duplicate key ${key}`);
    }
  }
  get(key) {
    return this.items[key];
  }
}

let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');

console.log(dict.items);
console.log(dict('one'));

I'd expect this to log

{ one: 'foo', two: 'bar' }
foo

instead it errors

return this.get(key);
            ^

TypeError: this.get is not a function

This works:

console.log(dict.bind(dict)('one'));

But why would it have to be bound to itself when it should already have those properties?

Surprisingly (?) neither super.bind(this); nor this.bind(this); in the constructor fix the problem.

How can I make an extension of function using ES6+ class syntax that has custom behaviour when called?

theonlygusti
  • 11,032
  • 11
  • 64
  • 119

3 Answers3

1

Surprisingly (?) neither super.bind(this); nor this.bind(this); in the constructor fix the problem.

You can call bind in constructor. But it will create a new function so you need to:

  1. Copy own properties, because it is a new object
  2. Return the result.

class SetOnceDict extends Function {
  constructor() {
    super('key', 'return this.get(key);')
    
    const out = this.bind(this) // create binded version
    
    Object.assign(out, this) // copy own properties
    
    return out // replace
  }
  items = {}
  add(key, value) {
    if (!this.items.hasOwnProperty(key)) {
      this.items[key] = value;
    } else {
      throw new Error(`Duplicate key ${key}`);
    }
  }
  get(key) {
    return this.items[key];
  }
}

let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');

console.log(dict.items);
console.log(dict('one'));

Also you can use a Proxy :)

class SetOnceDict extends Function {
  constructor() {
    super('key', 'return this.get(key);')
    
    return new Proxy(this, {
      apply(target, thisArg, args) {
        return target.call(target, ...args)
      }
    })
  }
  items = {}
  add(key, value) {
    if (!this.items.hasOwnProperty(key)) {
      this.items[key] = value;
    } else {
      throw new Error(`Duplicate key ${key}`);
    }
  }
  get(key) {
    return this.items[key];
  }
}

let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');

console.log(dict.items);
console.log(dict('one'));
Yury Tarabanko
  • 44,270
  • 9
  • 84
  • 98
  • I had no idea about `Proxy`'s `handler.apply`. That's pretty cool, and looks like the best it's gonna get! – theonlygusti Feb 02 '23 at 19:16
  • 3
    @theonlygusti I wouldn't recommend either approach. Only for research purpose. :) Functions construction has perf penalties, Proxies have perf penalties as well. – Yury Tarabanko Feb 02 '23 at 19:20
  • In what way does Function construction have performance penalties? Only on construction of an instance of the class right? But the Proxy would impose performance penalties every time members are accessed? – theonlygusti Feb 02 '23 at 19:47
0

this does not actually refer to the function object itself within function bodies. To do that you need arguments.callee, although this is deprecated.

class SetOnceDict extends Function {
  constructor() {
    super('key', 'return arguments.callee.get(key);')
  }
  items = {}
  add(key, value) {
    if (!this.items.hasOwnProperty(key)) {
      this.items[key] = value;
    } else {
      throw new Error(`Duplicate key ${key}`);
    }
  }
  get(key) {
    return this.items[key];
  }
}

let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');

console.log(dict.items);
console.log(dict('one'));
theonlygusti
  • 11,032
  • 11
  • 64
  • 119
0

I came up with another ugly way that works using Object.assign and bind:

class SetOnceDict extends Function {
  constructor() {
    super('key', 'return this.get(key);');
    return Object.assign(this.bind(this), this);
  }
  items = {}
  add(key, value) {
    if (!this.items.hasOwnProperty(key)) {
      this.items[key] = value;
    } else {
      throw new Error(`Duplicate key ${key}`);
    }
  }
  get(key) {
    return this.items[key];
  }
}

let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');

console.log(dict.items);
console.log(dict('one'));
theonlygusti
  • 11,032
  • 11
  • 64
  • 119
  • 1
    TBH, the class syntax is just a bad fit here. Native functions just aren't really fit to do what you want. It's fitting a square peg in a round hole. [Something like this](https://jsbin.com/foroden/1/edit?js,console) makes more sense to me. – VLAZ Feb 02 '23 at 19:26
  • @VLAZ Thanks for the frame challenge and insight, and all of your comments on the question. I agree with you that that seems way more appropriate. I asked this question in the hope to learn more about JS and where my knowledge is missing the pieces to make it work (even if it's not desirable), and now because of your comments and the accepted answer I've been shown a lot. Thanks! – theonlygusti Feb 02 '23 at 19:36