JS Tip of the Day: Attribute Event Scopes

Attribute Event Scopes
Level: Intermediate

We’ve seen how event attributes in HTML create functions in JavaScript that get assigned to the element’s respective event callback property. But there’s actually a lot more to how this function gets created than you may realize. Specifically, what you don’t see are the scopes involved in creating that function. Scopes determine what variables are available to the function that aren’t local to the function itself. All functions have access to the global scope, but they would also have access to any other scopes they were in when they were defined.

Unfortunately, you don’t exactly see how or where callback functions for attribute events are defined. So it’s not something you’d be able to identify through source code as you might be able to with your other functions. But that’s ok, because I’m about to show you exactly how it might look right now.

Here, we’re going to be using a <button> element represented as button that has been given an onclick attribute to demonstrate. The creation of the respective onclick callback function on the element object would look something like this:

with (document) {
    with (button.form) {
        with (button) {
            button.onclick = eval(`(
                function onclick (event) {
                    ${button.getAttribute('onclick')}
                }
            )`);
        }
    }
}

Immediately you should be able to recognize the collection of with blocks leading to the actual onclick assignment. This represents the extra scopes that the function is defined with. What this means is that if you use the name of a property in either document, button.form (i.e. a <form> ancestor of the <button>), or the button element itself in the onclick function, you’ll be referencing the respective property in any one of these objects.

<button onclick="console.log(doctype)">
    Log doctype
</button>
<!-- click:
<!doctype html> (<- document.doctype)
-->

While this can be convenient, especially for easily accessing properties of the button directly, it can also be confusing, notably when trying to access would-be global properties that might share a name with any of the properties in any of these objects.

<script>
let method = 'Credit card';
</script>
<form>
    <button onclick="console.log(method)">
        Log method
    </button>
</form>
<!-- click:
get (<- form.method)
-->

Here, though you might expect to be referencing the method declared in the <script> tag, you’re actually referencing the method in the <form> element the <button> is a child of. This will happen for every property within each those object, of which there are many.

For this reason, if you’re using event attributes at all (which is not really recommended anyway), you’re better off calling functions in those attributes that you’ve defined somewhere else, rather than relying on code defined directly within the attribute. Just make sure the function you call also does not clash with any of those object properties.

<script>
function logMethod (event) {
    console.log(method);
}
let method = 'Credit card';
</script>
<form>
    <button onclick="logMethod(event)">
        Log method
    </button>
</form>
<!-- click:
Credit card
-->

More info: