How do you run functions from one function that’s been passed inside another one?

Tags:



I’m writing a function to time how long it takes other functions to run. The code works with some functions, but not others.

When it fails, the errors are like:

Uncaught TypeError: this.query is not a function

I’ve found the documentation for .apply(), .bind(), and .call() which talk about changing this. It seems like a solution is there, but I haven’t been able to puzzle one out.

Here’s a sample that demonstrates the issue. It’s with lunr (via npm install -D lunr) in an Electron app. It’s running in the index.html browser page with electron setup to allow node integration on that part of the app. I’d make it more generic, but I don’t know how.

const fs = require('fs')
const lunr = require('lunr')

const timeFunctionExecution = (func, args) => {
    const t0 = performance.now()
    const payload = func(args)
    const t1 = performance.now()
    const time = t1 - t0

    const report = {
        "payload": payload, 
        "time": time
    }
    return report
}


function createLunrSearchIndex(contentDir) {

    const searchIndex = lunr(function () {
        this.ref('filename')
        this.field('content')

        let fileList = fs.readdirSync(contentDir)

        fileList.forEach(function(filename) {
            let content = fs.readFileSync(`${contentDir}/${filename}`, 'utf8')
            this.add(
                {
                   'filename': filename,
                    'content': content
                }
            )
        }, this)
    })

    return searchIndex
}


// This works and verifies the basic timer works
let report1 = timeFunctionExecution(createLunrSearchIndex, 'data')
console.log(report1)

// This works and verifies the search index works
let basicResults = report1.payload.search("chicken")
console.log(basicResults)

// Combine the two though, and it fails
let report2 = timeFunctionExecution(report1.payload.search, "chicken")
console.log(report2)

The first set of results from timeFunctionExecution work if you call them directly, but when I pass it through the timeFunctionExecution again, I get the error.

Inspecting the console.log(report1) call shows the query function exists

console.log output showing the .query() function is available.

basicResults has valid data so .query() would seem to be working in general but not when passed through the timing function.

Is there a way to pass functions like this while retaining the ability to call functions inside them (assuming that’s the problem)?

Answer

Be aware that report1.payload is an instance of the lunr.Index constructor (which may be a class, for example).

So, when you do something like this:

report1.payload.search("chicken");

You are calling the search method under the report1.payload instance, which is of type lunr.Index. The problem is that, when you pass an (unbound) reference to this function, it loses its context.

That means that when you do something like:

timeFunctionExecution(report1.payload.search, "chicken");

You just take a reference to the search function without any context. Think in context here as the implicit this parameter that the search method expects to receive.

So you have to options to fix this:

  1. Pass a bound function reference (created using the Function.prototype.bind method)
  2. Accept, from the timeFunctionExecution function, another argument to the this value

So, with a bound method, you may do something like (option 1 example):

class Index {
  query(...args) {
    return { args };
  }
  search(...args) {
    return {
      context: this,
      // Notice that this method (`search`) requires a context
      // to work, as it depends on `query`, another method
      // that can only be accessed through `this`.
      queryResult: this.query(args)
    };
  }
}

function indirectCall(fn, argsArr = []) {
  const payload = fn(...argsArr);
  return payload;
}

const ins1 = new Index();
// Bound the function to the instance (`ins1`). No problems here.
console.log(indirectCall(ins1.search.bind(ins1), ['a', 'b']));

const ins2 = new Index();
// The function is not bound. An error will arise.
console.log(indirectCall(ins1.search, ['a', 'b']));

And with a new thisArg (option 2 example):

class Index {
  query(...args) {
    return { args };
  }
  search(...args) {
    return {
      context: this,
      // Notice that this method (`search`) requires a context
      // to work, as it depends on `query`, another method
      // that can only be accessed through `this`.
      queryResult: this.query(args)
    };
  }
}

function indirectCall(fn, argsArr = [], thisArg = null) {
  const payload = fn.apply(thisArg, argsArr);
  return payload;
}

const ins1 = new Index();
// Given a `thisArg` (the instance).
console.log(indirectCall(ins1.search, ['a', 'b'], ins1));

const ins2 = new Index();
// The `thisArg` is not given. An error will arise.
console.log(indirectCall(ins1.search, ['a', 'b']));

You can learn more in What does this statement do? console.log.bind(console) and in How does the “this” keyword work?.



Source: stackoverflow