3

Suppose I have a function, foo, and a bound version of it, bar. Is there a built-in method I can test if bar is a bound version of foo more definitively than checking the name?

function isBoundTo(bound, fn) {
  return /bound (.+)$/.exec(bound.name)[1] === fn.name;
}

function foo() {
  return this && this.x !== undefined ? this.x : 'foo';
}

function notfoo() { 
  return 'notfoo'; 
}

const bar = foo.bind({ x: 'bar' });
const boundbar = bar.bind({ x: 'boundbar' });

const baz = {
  foo() { 
    return 'baz.foo';
  }
};

const results = [
  ['foo()'],
  ['notfoo()'],
  ['bar()'],
  ['boundbar()'],
  ['baz.foo()'],
  ['isBoundTo(bar, foo)'],
  ['isBoundTo(bar, notfoo)'],
  ['isBoundTo(boundbar, bar)'],
  ['isBoundTo(boundbar, foo)'],
  ['isBoundTo(bar, baz.foo)', 'false positive'],
].reduce((obj, [exp, comment]) => {
  const val = eval(exp);
  return {
    ...obj,
    [exp]: {
      result: typeof val === 'string' ? JSON.stringify(val) : val,
      comment: comment || ''
    }
  };
}, {});

console.table(results);
document.querySelector('.as-console-table thead th').innerText = 'expression';
/*<ignore>*/console.config({maximize:true,timeStamps:false,autoScroll:false});/*</ignore>*/
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>

I know I could overwrite Function.prototype.bind with a proxy. I'd just rather not (though the more I've worked on this question, the less I'm opposed to using a proxy).

Function.prototype.bind = new Proxy(Function.prototype.bind, {
  apply: function (target, thisArg, args) {
    let fn = Reflect.apply(target, thisArg, args);

    Object.defineProperty(fn, 'targetFunction', {
      value: thisArg.targetFunction || thisArg,
      configurable: false,
      writable: false
    });

    return fn;
  }
});

function foo() {
  return this && this.x !== undefined ? this.x : 'foo';
}

function notfoo() {
  return 'notfoo';
}

const bar = foo.bind({ x: 'bar' });
const boundbar = bar.bind({ x: 'boundbar' });

const baz = {
  foo() {
    return 'baz.foo';
  }
};

const results = [
  'foo()',
  'notfoo()',
  'bar()',
  'boundbar()',
  'baz.foo()',
  'foo.targetFunction',
  'bar.targetFunction === foo',
  'bar.targetFunction === notfoo',
  'boundbar.targetFunction === bar',
  'boundbar.targetFunction === foo',
  'bar.targetFunction === baz.foo'
].reduce((obj, exp) => {
  const val = eval(exp);
  return {
    ...obj,
    [exp]: {
      result: typeof val === 'string' ? JSON.stringify(val) : val
    }
  };
}, {});

console.table(results, ['expression', 'result']);
document.querySelector('.as-console-table thead th').innerText = 'expression';
/*<ignore>*/console.config({maximize:true,timeStamps:false,autoScroll:false});/*</ignore>*/
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>

When I debugged this and inspected fn, there was a property listed as [[TargetFunction]], but I have no idea how to reference it. It doesn't appear to be a symbol (which are denoted [Symbol('name')]). I thought maybe it was specific to Node.JS, but it also appears in Chrome. It doesn't appear in Firefox, but since I'm only using this for testing in Node, I don't really care. I imagine it must be accessible somehow if the debugger knows it exists and can retrieve the value of it.

Edit

'Cause some of ya'll want me to justify the need for this, I wrote up a simplified version of what I'm working on. (I didn't choose the pattern for opening a dialog, but I do need to test it.) Note: The code doesn't actually run because repl.it wouldn't let me install the dependencies needed for Enzyme. https://repl.it/repls/BitesizedWeirdElectricity

dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
  • I'll bite, why? – Adam Jenkins Jun 14 '20 at 21:41
  • Testing, primarily. If my React component renders a list of components with handlers that are bound with specific arguments, I want to be able to test that the function they're bound to is the correct function. – dx_over_dt Jun 14 '20 at 21:43
  • 4
    Properties enclosed in double bracers are internal properties not available from JS https://stackoverflow.com/a/17174868/8583739 – Isidrok Jun 14 '20 at 21:46
  • 1
    In your scenario, your react component isn't responsible for what its inputs are - all it's supposed to do is produce the correct output given a specific input. It doesn't make any sense for this purpose. EDIT: During testing, **you supply the inputs** you don't need to test them. – Adam Jenkins Jun 14 '20 at 21:46
  • I wouldn't go as far as to so doesn't make sense, but it's definitely a dirty hack. It's probably useful for debugging if your team does that kind of binding excessively (almost sounds like they're using functions like mutatable state) and cause conflicts. – user120242 Jun 14 '20 at 21:49
  • @Isidrok That's what I figured. How does the debugger know they exist? – dx_over_dt Jun 14 '20 at 21:51
  • There are much more things exposed to the debugger than to user land – Isidrok Jun 14 '20 at 21:56
  • Consider testing the useful observable properties instead, e.g. by calling the bound function/causing it to be called and getting it to report on its `this` value/other arguments. – Ry- Jun 14 '20 at 22:03
  • @dx_over_dt - as most often with SO questions, you don't want to know how to do this because you don't **need** to know how to do this. What you've done is arrived at a conclusion - you need to determine if a function is bound - in order to solve some other problem you're having. You've arrived at the wrong conclusion. Post your actual question (the one about how you're trying to unit test something) and get an answer on that one. – Adam Jenkins Jun 14 '20 at 22:19
  • @Adam actually, I have this particular problem solved already. The implementation is just clunky. I've thought through other possible solutions, and if this one works, it would be ideal because it would be very simple. – dx_over_dt Jun 14 '20 at 22:28
  • 2
    The problem with that kind of tests is you are not gonna test functionality but instead some concrete implementation. That means you will need to maintain those tests if you change that piece of code even if the functionality remains the same, which is a terrible experience (know it first hand). – Isidrok Jun 14 '20 at 22:32
  • @Isidrok I updated the question with a link to an example of the test I have to write. – dx_over_dt Jun 14 '20 at 23:09
  • 1
    @dx_over_dt, as described, you're testing implementation details, not functionality. In your code, you should not be testing ItemFactory at all - the pure existence of ItemFactory is an implementation detail - you could replace it with something that provides similar functionality and your components will all still work correctly - test the component output, not their implementation details. What you should be doing is rendering `MyComponent` with certain inputs and asserting it renders an array of `OpenDialogButton`'s with the correct props. – Adam Jenkins Jun 15 '20 at 00:57
  • Secondly, you're misusing react - you shouldn't use instance variables, you should use state - `items` shouldn't be using an instance variable, it should be state. – Adam Jenkins Jun 15 '20 at 01:00
  • @dx_over_dt pretty much what @Adam said but if you still want to test your `componentFactory` because it is used all over the place you can stub the `openDialog` action and check it is correctly invoked from your items, which is what they are for. Don't test how they do it i.e. Should your tests fail if you change your factory implementation to `onClick: () => Actions.openDialog(Dialog, ...)` ? – Isidrok Jun 15 '20 at 06:13
  • @Isidrok that's actually the solution I currently have in place, and you're right about it needing to be resilient to the wrapper function vs a bind call. I should have also mentioned that half the reason I posted this question was curiosity about what was possible in JavaScript. – dx_over_dt Jun 15 '20 at 16:50

2 Answers2

1

There's no way to do that. [[TargetFunction]] is an internal property, that isn't exposed.

However, you might implement a custom bind function, that keeps track of that:

const boundFns = new WeakMap()
function bind(fn, thisArg, ...args){
  const newFn = function (...moreArgs) {
    const allArgs = args.concat(moreArgs)
    return new.target 
      ? Reflect.construct(fn, allArgs, new.target)
      : Reflect.apply(fn, thisArg, allArgs)
  }
  Object.defineProperty(newFn, 'name', {value: 'bound ' + fn.name})
  Object.defineProperty(newFn, 'length', {value: fn.length})
  Object.defineProperty(newFn, 'toString', {
    value: function toString(){
      return fn.toString()
    },
    configurable: true
  })
  boundFns.set(newFn, fn)
  return newFn
}

function getTargetFunction(fn){
  return boundFns.get(fn)
}

Or even simpler (as @Ry- pointed out), that uses native bind:

const boundFns = new WeakMap()
function bind(fn, ...args){
  const newFn = fn.bind(...args)
  boundFns.set(newFn, fn)
  return newFn
}

function getTargetFunction(fn){
  return boundFns.get(fn)
}
FZs
  • 16,581
  • 13
  • 41
  • 50
  • 1
    Why not `const newFn = fn.bind(thisArg, ...args)`? – Ry- Jun 14 '20 at 22:07
  • 1
    @Ry- Well, appearently I've thought so much about a custom implementation, that I've accidentally reinvented the wheel. Oops... – FZs Jun 14 '20 at 22:10
0

You can check if the original function prototype is the prototype of objects created from the bound function but you will have to execute it:

function foo(){}
var bar = foo.bind({},1,2,3);
function baz() {}
Object.setPrototypeOf(foo.prototype, baz.prototype);
foo.prototype === Object.getPrototypeOf(new bar()); // true
baz.prototype === Object.getPrototypeOf(new bar()); // false

Edit: check it is the same prototype instead of part of the prototype chain.

Isidrok
  • 2,015
  • 10
  • 15
  • 3
    That’s a pretty cool property, but it doesn’t distinguish the bound function from functions that call it as a constructor or share its prototype, and probably more importantly doesn’t work on bindable functions that aren’t constructors, like ones created with the method syntax `({ foo() {} }).foo` (or arrow functions even though there’s no reason to bind them). – Ry- Jun 14 '20 at 21:59