When writing event listeners in JavaScript, you should never rely on the global window.event
property.
The MDN Web Docs explain:
You should avoid using this property in new code, and should instead use the
Event
passed into the event handler function. This property is not universally supported and even when supported introduces potential fragility to your code.
I was reviewing my password toggle script yesterday and I found that I had fallen into this trap.
The problem
I had the following function...
/**
* Toggle the visibility of a single password field
* @param {Object} password The password field
*/
function togglePassword (password) {
password.type = event.target.checked ? "text" : "password";
}
...That I passed into the Array.forEach()
method at the end of another function:
/**
* Toggle all password fields inside a single form
* @param {Object} event The Event object
*/
function togglePasswords (event) {
// If this isn't a password toggle, do nothing
if (!event.target.matches("[data-toggle]")) return;
// Get the closest form
// If it doesn't exist, do nothing
var form = event.target.form;
if (!form) return;
// Get all password fields inside this form
var passwords = Array.from(
form.querySelectorAll("[data-password]")
);
// Toggle the password fields
passwords.forEach(togglePassword);
}
The problem was that I didn't explicitly define the event
as a parameter of the togglePassword()
function. This means it was relying on the global window.event
property.
The solution
To fix this, I modified the togglePassword()
function to accept the event
as a second argument:
/**
* Toggle the visibility of a single password field
* @param {Object} password The password field
* @param {Object} event The Event object
*/
function togglePassword (password, event) {
password.type = event.target.checked ? "text" : "password";
}
Then I passed in the event
explicitly:
// Toggle the password fields
passwords.forEach(function (password) {
togglePassword(password, event);
});
Lexical scoping
If you know me, you'll know that I like named functions. Therefore, it's reasonable to ask why I invoked the togglePassword()
function within an anonymous function.
This is because when you use the forEach()
method, you pass in a callback function. The forEach()
method automatically passes specific arguments into your callback function:
- The current element being processed
- The index of the current element being processed
- The array on which
forEach()
was called
You can name these arguments whatever you like. Therefore, if I did the following, as I did originally...
// Toggle the password fields
passwords.forEach(togglePassword);
...Then the forEach()
method would call the togglePassword()
function with the index of the current element as the second argument.
Although the togglePassword()
function has event
defined as its second parameter, it's just a name. The forEach()
method would pass in the index of the current element.
This would throw a TypeError
, because it would be trying to access the target
property of a number, which doesn't exist and is therefore undefined
:
Uncaught TypeError: Cannot read property 'checked' of undefined
Instead, by using the anonymous function...
// Toggle the password fields
passwords.forEach(function (password) {
togglePassword(password, event);
});
...The call to forEach()
works.
This is because the togglePassword()
function can access the event
from the lexical scope of the outer togglePasswords()
function.
More information
For more information on event delegation and scoping, I recommend the following articles by Chris Ferdinandi and Todd Motto: