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
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)?
Advertisement
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:
- Pass a bound function reference (created using the
Function.prototype.bind
method) - Accept, from the
timeFunctionExecution
function, another argument to thethis
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?.