简体   繁体   中英

ES6 Proxies: trapping undefined function executions

Let's say I have the following object with two functions as properties:

const foo = {
  f1: () => {...},
  f2: () => {...},
}

I would like to perform a specific action (for example, throw a custom error) when someone tries to execute a function that doesn't exist on the foo object.

I've tried using a get proxy , but that throws an error even when I'm not trying to execute f3 , such as in the following code:

if (foo.f3) {...}

So how can I write my proxy in such a way that foo.f3 returns undefined as it usually would, but foo.f3() does throw an error?

Here's a partial solution, inspired by Unmiss .

const handler = {
  get: function(obj, prop) {
    if (prop in obj) {
      return obj[prop];
    } else {
      return () => {
        throw new Error(`Foo.${prop} is undefined`);
      }
    }
  }
};

The problem with this is that while it accomplishes the goal of only throwing an error when you actually try to execute Foo.f3() , since Foo.f3 is now equal to that anonymous function is doesn't return undefined anymore, meaning that (as far as I can tell) if (Foo.f3) {...} will always return true .

Edit: as @paulpro points out:

You absolutely cannot do that. foo.f3 is either undefined or some callable with custom logic; it cannot be both.

The best we could do is trap f3 in foo statements using the has trap, but this would mean if (f3 in foo) and if (foo.f3) would now have different results, which seems like a big red flag.

Is this what your asking for? https://jsfiddle.net/MasterJames/bhesz1p7/23/

[obviously you need to F12 your dev tools to see the console output or change as desired]

Only real difference is to return undefined after throwing. It's as if the function executed without doing anything since it doesn't exist.

I'm sure there's a different solution based on the actual use case, but I like the idea/question. Keeps things more stable etc.

let foo = new Proxy(
    {
    f1: function (val) {
      console.log('  F1 value:' + val);
      return 'called OKAY with', val;
    }
  },
  {
    get: function(obj, prop) {
      console.log("obj:", obj, " prop:", prop);
      if (prop in obj) {
        console.log("Found:", prop);
        return obj[prop];
      }
      else {
        console.log("Did NOT find:", prop);
        throw new Error(`Foo.${prop} is undefined not called returning undefined`);
        return undefined;
      }
    }
  });

console.log("\nFoo Tester started");

console.log(' Does F1 exists', foo.f1 !== undefined);
console.log(' called F1 result:', foo.f1('passed') );

try {
    console.log(' Does F2 exists', foo.f2 !== undefined);
    console.log(' called F2 result:', foo.f2('passed') );
}
catch (err) {
    console.log(' Error calling F2:', err );
}

console.log("Foo Tester finished");

Not sure you want to try-catch or not that's also up to you so in the end checking if it's real and a function is the same difference depending on how your going to handle the error.

if (foo.f2 && foo.f2.constructor === Function && foo.f2()) console.log("okay!");

Again you call build a safeCall wrapper more like this or something in between? possible calling foo's 'customThrow' if it exists or what-have-you, so many possibilities with JS.

Okay so it took me sometime but I have a solution now.

I was not fully understanding your question, which I reformulated as a question within the question for myself to understand the issue better as it is complicated.

Basically you want to know if it's being called or not so the function you need in the proxies 'get' is 'isCalling'.

The solution is not clean in JS Fiddle because it's messy there at least for this kind of problem's solution.

Basically the solution is a sentence is, "you have to use an error to get a stack trace then retrace the source code that is calling and look for a right bracket or not.", to determine how it's being called and return whatever you want then).

[Please note this depends on your code and how you call it so you would adjust as needed.]

Since you have to find the location in the source code that's being called from it's way better if there is no inline script tag as is the case in this JSFiddle example. I'm using outerHTML to get the source, when arguments.callee.caller.toString() is better from an actual JS file. You'll also not the location from the stacktrace is skewed by odd behavior here, so with a normal JS file the code would align properly using other solutions are recommended. If anyone knows how to get a clean source that aligns with the error trace every time with script-tag blocks etc. Also note coming but not existing yet are things like Error.lineNumber.

[Please don't bother with the version history it was a nightmare to sort this one out. And again you would be better to use other npm packages to do the source code from stack trace parts.]

Anyway the example I believe achieves what you want but in principle demonstrates what you'd need to do better in a given real (no Fiddle) situation. I'm pretty sure doing this is not a great solution in production either and I've not tested the timing (performance speed) but if it really was that important to your cause (and no other better solution which I doubt) then it will work.

Originally I discovered this technique when I was doing something experimental, and instead of just sending another argument I was checking to see what was actually calling it and adjusting the functions action depending.

Usages are extensive when you start to think more about it as I did last year when I first did something like this. Examples are as an extra function execution Security Check, Realtime mystery-bug Debug Solution, a way to execute the function differently without passing more arguments, runaway recursive loops (how long is the stack), to name a few.

https://jsfiddle.net/MasterJames/bhesz1p7/90/

let foo = new Proxy(
    {
    f1: function (val) {
      console.log('  F1 value:' + val);
      return 'called OKAY with', val;
    }
  },
  {
    isCalling: function() {
      let stk = new Error();
      let sFrms = this.stkFrms(stk.stack);
      console.log("stkFrms:", sFrms);

      //BETTER From real pure JS Source
      //let srcCod = arguments.callee.caller.toString()

      let srcCod = document.getElementsByTagName('html')[0].outerHTML.split("\n");
      let cItm = sFrms[(sFrms.length - 1)];
      if(cItm !== undefined) {
        let cRow = (parseInt(cItm[1]) - 3);
        let cCol = (parseInt(cItm[2]) + 1);
        let cLine = srcCod[cRow];
        let cCod = cLine.substr(cCol, 1);
        if(cCod === '(') return true;
      }
      return false;
    },
    stkFrms: function (stk) {
      let frmRegex1 = /^.*at.*\(.*\:([0-9]*)\:([0-9]*)\)$/;
      let frmRegex2 = new RegExp(frmRegex1.source, 'gm');
      let res = [], prc, mtch, frms = stk.match(frmRegex2);
      for(mtch of frms) {
        prc = frmRegex1.exec(mtch);
        res.push(prc);
      }
      return res;
    },
    get: function(obj, prop) {
      if (prop in obj) {
        console.log("Found:", prop);
        return obj[prop];
      }
      else {
        if(this.isCalling() === false) {
          console.log("Did NOT find:", prop);
          return undefined;
        }
        else {
          console.log("Did NOT find return custom throw function:", prop);
          return function() {throw new Error(`Foo.${prop} is undefined`);}
        }
      }
    }
  });

console.log("foo.f1:", foo.f1);
console.log("foo.f1('passed'):", foo.f1('passed'));

console.log("foo.f2:", foo.f2);
try {
    console.log("foo.f2('passed2'):", foo.f2('passed2'));
}
catch(err) {
    console.log("foo.f2('passed2') FAILED:", err);
}
console.log("'f2' in foo:", 'f2' in foo);

Okay so a verbal run through:

You want to check foo.f2 is undefined so it returns that because it's not being called.

If you do call it (f2) without simply checking first and erroring as needed, and you don't want to try-catch to throw your custom error based on the function name, you want it to return an actual function that will throw a custom error.

You also want to use 'in' to see that it's undefined, which is the same as false (maybe hack it further to send false instead of undefined via something like isCallingFromIn too.

Did I miss anything? Is this not what you all thought was impossible?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM