Search code examples
javascriptthis

Losing reference to 'this' when invoking Reflect.get


I have the following method to map a command to a function inside an object:

const mapHandler = (str, obj) => {
  if (!Reflect.has(obj, str)) {
    return null;
  }

  const candidateHandler = Reflect.get(obj, str, obj);
  if (typeof candidateHandler !== "function") {
    return null;
  }

  return candidateHandler;
};

I have a simple class holding some commands I want to invoke using the handler:

class MyClass {
  obj;

  constructor() {
    this.obj = { a: 1, b: 2 };
  }

  create() {
    return this.obj.a;
  }

  remove() {
    return this.obj.b;
  }
}

I set things up and run:

const myobj = new MyClass();
const handler = mapHandler("create", myobj);

console.log("Found", handler);

const res = handler();

But I get this:

Found [Function: create]
/workspaces/sandbox/index.js:22
    return this.obj.a;
                ^

TypeError: Cannot read properties of undefined (reading 'obj')
    at create (/workspaces/sandbox/index.js:22:17)
    at Object.<anonymous> (/workspaces/sandbox/index.js:35:13)
    at Module._compile (node:internal/modules/cjs/loader:1241:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1295:10)
    at Module.load (node:internal/modules/cjs/loader:1091:32)
    at Module._load (node:internal/modules/cjs/loader:938:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
    at node:internal/main/run_main_module:23:47

I am clearly losing the reference to this inside myobj and most likely due to Reflect.get. How can I fix this and make sure the handler mapper does not cause the returned function to lose its reference to this?


Solution

  • When you reference a function it's not bound to any this.

    You either should use Function::call() to make a bound call, or Function::bind() before the call to invoke to function on a proper this.

    Note when using Function::bind() you cannot rebind it later (Function::call() with a different this doesn't work either), since Function::bind() makes something like:

    Function.prototype.bind = function(ctx, ...args){
      const self = this;
      return function(...extraArgs){
        return self.call(ctx, ...args, ...extraArgs);
      }
    }
    

    class MyClass {
      obj;
    
      constructor() {
        this.obj = { a: 1, b: 2 };
      }
    
      create() {
        return this.obj.a;
      }
    
      remove() {
        return this.obj.b;
      }
    }
    
    const mapHandler = (str, obj) => {
      if (!Reflect.has(obj, str)) {
        return null;
      }
    
      const candidateHandler = Reflect.get(obj, str, obj);
      if (typeof candidateHandler !== "function") {
        return null;
      }
      // here we bind the func to allow calling without a context
      return candidateHandler.bind(obj);
    };
    
    
    const myobj = new MyClass();
    const handler = mapHandler("create", myobj);
    
    console.log("Found", handler);
    
    const res = handler();