Keywords: ES6 modules | export default | mutual function calls | ReferenceError | module scope
Abstract: This article provides an in-depth analysis of the ReferenceError that occurs when functions within an ES6 default export object attempt to call each other. By examining the fundamental differences between module scope and object properties, it systematically presents three solutions: explicit property referencing, using the this keyword, and declaring functions in module scope before exporting. Each approach includes refactored code examples with detailed explanations of their mechanisms and appropriate use cases. Additionally, the article discusses strategies for combining named and default exports, offering comprehensive guidance for module design.
In the ES6 module system, the export default syntax provides a convenient way to export a module's default value. However, when the exported object contains multiple functions that call each other, developers often encounter ReferenceError issues. This article examines a typical example to analyze the root cause of this problem and presents multiple effective solutions.
Problem Analysis
Consider the following code snippet:
export default {
foo() { console.log('foo') },
bar() { console.log('bar') },
baz() { foo(); bar() }
}
While this code appears reasonable, calling baz() throws ReferenceError: foo is not defined. This occurs because export default { ... } is essentially syntactic sugar for:
const funcs = {
foo() { console.log('foo') },
bar() { console.log('bar') },
baz() { foo(); bar() }
}
export default funcs
The key issue is that foo, bar, and baz are not independent functions in the module scope but properties of an anonymous object (effectively funcs). Inside the baz function, foo and bar are treated as free variables, but they don't exist in the module scope, leading to the reference error.
Solution 1: Explicit Property Referencing
The most straightforward solution is to explicitly reference object properties within functions. By naming the object, you can clearly specify the function context:
const tokenManager = {
revokeToken(headers) {
// implementation details
},
expireToken(headers) {
// implementation details
},
verifyToken(req, res, next) {
jwt.verify(..., (err) => {
if (err) {
tokenManager.expireToken(req.headers); // explicit reference
}
});
}
};
export default tokenManager;
This approach clearly demonstrates dependencies between functions but requires assigning a variable name to the object.
Solution 2: Using the this Keyword
When functions are called as object methods, the this keyword can reference the current object:
export default {
foo() { console.log('foo') },
bar() { console.log('bar') },
baz() { this.foo(); this.bar() } // using this reference
}
This method is concise but requires attention to this binding. If functions are extracted or passed as callbacks, this may not point to the expected object.
Solution 3: Module Scope Function Declarations
Declaring functions in the module scope and then exporting them as an object avoids scope confusion:
function foo() { console.log('foo') }
function bar() { console.log('bar') }
function baz() { foo(); bar() } // direct reference to module scope functions
export default { foo, bar, baz };
This approach allows functions to be directly accessible within the module while maintaining export simplicity. It is particularly suitable for scenarios requiring internal function reuse.
Advanced Pattern: Combined Export Strategy
For modules requiring flexible imports, you can combine named and default exports:
// util.js
export function foo() { console.log('foo') }
export function bar() { console.log('bar') }
export function baz() { foo(); bar() }
export default { foo, bar, baz };
Usage examples:
// Using default export
import util from './util';
util.foo();
// Using named export
import { bar } from './util';
bar();
// Using namespace import
import * as util from './util';
util.baz();
This pattern offers maximum flexibility, allowing users to choose import methods based on their needs.
Conclusion and Best Practices
The issue of mutual function calls in ES6 modules stems from scope confusion. When selecting a solution, consider the following factors:
- Code Clarity: Explicit property referencing makes dependencies most apparent.
- Maintainability: Module scope declarations reduce context binding issues.
- Use Case: Combined export strategies are most appropriate when individual function imports are needed.
In practice, choose the solution based on module complexity and usage patterns. For simple utility modules, module scope declarations are often optimal; for objects requiring internal state maintenance, explicit referencing or the this keyword is more suitable.