This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the javascript category.
Last Updated: 2024-11-21
I had a (Jest) mock of the browser's xhr functionality as follows:
const xhrMockClass = () => ({
// This function (`send`) is the important bit for the purpose of this article
send: () => {
this.onreadystatechange()
return jest.fn()
},
readyState: 4,
status: 200,
open: jest.fn(),
onreadystatechange: jest.fn(),
setRequestHeader: jest.fn()
})
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
The caller I wanted to test looked like this:
export function get(url, callback = () => {}) {
const xhr = new XMLHttpRequest
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
callback(xhr)
}
}
const async = true
xhr.open("GET", url, async)
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
return xhr.send()
}
When I ran the code in my test environment, I would get an error at xhr.send()
=> "Cannot read property 'onreadystatechange' of undefined"
When I debugged the mock by logging this
, it turned out to be undefined
const xhrMockClass = () => ({
send: () => {
console.log(this)
}
})
The fix was to change arrow invocation to invocation with the function
keyword:
const xhrMockClass = () => ({
...
send: function() {
this.onreadystatechange()
return jest.fn()
},
})
Why on earth did this work? Let's take a more narrow example of the original (problem) code to explain what happened.
const test = () => ({
state: 1,
foo: () => {
return this.state
}
})
With this, test().foo()
will return null
instead of the 1
you might
expect. This is because, this
in foo
is not bound to the object returned by
the test
function due to foo
being being an arrow function!
This is so even if you test objects:
window.outer = {
inner: {
state: 1,
foo: () => {
return this.state
}
},
state: "outerstate"
}
// outer.state.foo() still gives undefined
So don't use arrow functions with object methods... see here for discussion:
https://stackoverflow.com/questions/31095710/methods-in-es6-objects-using-arrow-functions
Rewrite with the method syntax instead to get it working:
const test = () => ({
state: 1,
foo() {
return this.state
}
})
Therefore stick to method syntax or normal functions when defining functionality on objects.
this
is whatever the lexical environment
is (i.e. the context it was defined
). For example, in the following code,
this
is undefined
, not xhrMockClass
. const xhrMockClass = () => ({
send: () => {
this.onreadystatechange()
}
})
Think about it:
- When we call xhrMockClass()
it returns an object straight away
- Before that object has been returned, we cannot possibly access it via
this