0

I have a class which manages few requests. So requests are set up as a collection. I've done this way to avoid having to write lengthy switch/if else statements.

// Manager class
class makeManagerRequests() {
  private list = [];
  private createList(body) {
    this.list.push(body)
  }
  createEmployee(body) {
    this.createlist(body);
  }
  updateManager(body);
  deleteManager(body);
}

// Employee class
class makeEmployeeRequests() {

  private list = [];
  private createList(body) {
    this.list.push(body)
  }
  createEmployee(body) {
    this.createlist(body);
  }
  updateEmployee(body)
  deleteEmployee(body)
}

// Usage in a generic widget
class makeRequests() {

  requestMap = new Map();

  constructor(makeManagerRequests, makeEmployeeRequests) {

  }

  createRequestMap() {
    requestMap.set('createManager', this.makeManagerRequest.createManager.bind(this));
    requestMap.set('updateManager', this.makeManagerRequest.updateManager.bind(this));
    requestMap.set('deleteManager', this.makeManagerRequest.deleteManager.bind(this));
    requestMap.set('createEmployee', this.makeManagerRequest.createEmployee.bind(this));
    requestMap.set('updateEmployee', this.makeManagerRequest.updateEmployee.bind(this));
    requestMap.set('deleteEmployee', this.makeManagerRequest.deleteEmployee.bind(this));
  }

  makeRequest(data) {
    let req = requestMap.get(data.requestType);
    req(data.body)
  }

}

This itself works.

First thing I noticed was keyword "this" change scope when using get Map from "service" to the "widget" so that createlist() become undefined. BUT if I just do this without bind it works.

  // I can test this with mocking the makeManagerRequest
  makeRequest(data) {
    this.makeManagerRequest.updateEmployee(body)        
  }

When using map I need .bind(this). Not sure why "this" change? Issue this creates is when I try to test and mock the service and spy on service. I get spy never been called. So I think what happens is bind create new function when creating the map. So I can't spy on

spyOn(MockMakeManagerRequest, 'updateEmployee')

So I am trying to figure out how does the functions themselves stored in the Map(). I am looking for technical implications this collection and how I can make this work so I can implement tests around this.

This is actually being used with in Angular 2 app. But I don't think it would make much difference as I am trying to figure what happens to/test functions when stored as a value in Map();

MonteCristo
  • 1,315
  • 1
  • 16
  • 32
  • At least related: http://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-context-inside-a-callback, http://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work, possibly I should have used one of those (probably the second) in a close-as-duplicate vote. – T.J. Crowder Nov 28 '16 at 12:54
  • Yeah, really, if you look past the `Map` aspect, it's a duplicate of that second one. But I'll leave the community wiki answer below in case it's useful. – T.J. Crowder Nov 28 '16 at 12:58

1 Answers1

0

What's stored in the Map is a function reference, nothing more. The this problem you're having has nothing to do with Map (other than that it uses function references), it's just how functions and this work in JavaScript. Let's take a simpler example:

class Example {
  constructor(message) {
    this.message = message;
  }
  method() {
    console.log(this.message);
  }
}
const c = new Example("Hi");
c.method(); // "Hi"
const f = c.method;
f();        // Error: Cannot read property 'message' of undefined

When method is called, it's how it's called that determines what this is: If we call it using an expression that gets it from c, then this during the call to it will have the value c has. But if we don't, it usually won't, it'll have some other value.

I realize you were asking why this is wrong, not how to solve it, but having explained why it's wrong, here are three options for solving it:

  1. Function#bind solves that by creating a function that, when called, calls the original with this set to a specific value.

  2. Another solution is to use arrow functions as wrappers:

    requestMap.set('createManager', (...args) => this.makeManagerRequest.createManager(...args));
    

    Arrow functions don't have their this set by how they're called, they close over this just like closing over an in-scope variable. So since we're defining them inside a call to createRequestMap where this is the object we want to use, arrows close over that this and we're good.

  3. Another option is to change the call that uses the function reference:

    makeRequest(data) {
      let req = requestMap.get(data.requestType);
      req.call(this, data.body);
    // --^^^^^^^^^^^^
    }
    

    There, we're explicitly saying what we want this to be during the call to req.

T.J. Crowder
  • 879,024
  • 165
  • 1,615
  • 1,639
  • I was getting confused because `makeRequest(data) { this.makeManagerRequest.updateEmployee(body) }` works. BUT `requestMap.get(data.requestType)(body)` didn't work without bind. hence it made me think its something to do with how it's stored within Map. – MonteCristo Nov 28 '16 at 13:44
  • I would assume that 3rd option will solve jasmine spy issue too? – MonteCristo Nov 28 '16 at 13:44
  • @MonteCristo: I haven't used it, but I'd think so, yes. – T.J. Crowder Nov 28 '16 at 13:49