1

There's a particular JavaScript pattern that has always bothered me, and I never really figured out the proper way to solve it. Instead I usually just ignore it because 99% of JavaScript interpreters support function hoisting so no run-time errors arise.

Consider the following:

function onOpen()
{
    console.log("Connected!");
    ws.removeEventListener("open", onOpen);
    ws.removeEventListener("error", onError);
}

function onError()
{
    console.log("Failed to connect!");
    ws.removeEventListener("message", onMessage);
    ws.removeEventListener("error", onError);
}

var ws = new WebSocket("...");
ws.addEventListener("open", onOpen);
ws.addEventListener("error", onError);

In this code, within the onOpen function, I'm referencing onError before onError has been defined lower in the code. This isn't actually a problem, because the onOpen method won't run until after onError has been defined, but it's still bad practice and trips up ESLint's no-use-before-define rule

In a more general sense, this is an error that will arise whenever two functions exist that each need to reference each other:

function a(x) {
    return x === 1 ? b(x) : 2;
}
function b(x) {
    return x === 2 ? a(x) : 1;
}

Is there a design pattern for eliminating this circular dependency? In my simple generic example the easy solution is "only have one function":

function a(x) {
    return x === 1 ? 1 : 2;
}

However when binding event listeners this doesn't always seem possible.

4
  • I don't want to get into a religious war, but… some eslint rules are not worth it. Relying on function hoisting is not a bad practice, especially in this case I'd say it is perfectly good and readable JavaScript, and you're wasting your time trying to work around a non-issue. Commented Dec 18, 2018 at 18:05
  • BTW, some recursive functional patterns need function hoisting. Enforcing that eslint rule simply makes your code less expressive, not better. Commented Dec 18, 2018 at 18:07
  • And finally, contrary to what you imply, function hoisting does allow you to actually call the function "before" (lexically) its declaration. As long as you don't redefine the same function multiple times or shadow the same identifier (and useful eslint rules should easily prevent that), there is no way to create a bug because of function hoisting. Commented Dec 18, 2018 at 18:13
  • "99% of JavaScript interpreters support function hoisting" - uh, if it doesn't support declarations then it's not a JavaScript interpreter. Commented Dec 18, 2018 at 19:41

2 Answers 2

3

It's still bad practice and trips up ESLint's no-use-before-define rule

No, it's not a bad practice, it's very much a necessity. It is the preferred pattern when you have a circular functional dependency. Configure your ESLint accordingly (with { "functions": false }).

Is there a design pattern for eliminating this circular dependency?

Not really. There are several workarounds however, such as declaring the function up-front with a var (which is enough to make ESLint happy). Alternatively, you could pass a reference to the function around via parameters:

function _a(x, f) {
    return x === 1 ? f(x) : 2;
}
function b(x) {
    return x === 2 ? _a(x, b) : 1;
}
function a(x) {
    return _a(x, b);
}

Other crazy hacks with closures, following up on the Y combinator idea, are imaginable. This is not really suitable for your event listener scenario though.

Sign up to request clarification or add additional context in comments.

2 Comments

I guess the reason I think it's bad practice is because in many other languages (for instance, in C++) you at least have to provide function signatures before you use them, even if the implementation comes later. That said, this is less of a "I can't do my job because ESLint is mad" and more of a "The fact that ESLint has a rule against this suggests there's a workaround, but I could never figure out what it was"
@stevendesu That many other (older) languages have a deficiency doesn't make this bad practice :-) Most modern languages do not require forward declarations.
2

I don't think this is a real problem, because usually everything works fine. Also you worry about the fact that you mention onError before it is declared, but you don't care about mentioning ws. But if yous still need to solve this, I hope this approach will do the work.

var eventListenerListOnOpen = [];
var eventListListenerOnError = [];
var eventListListenerOnMessage = [];

var ws;

function removeListedEventListeners(object, eventName, eventListenerList) {
  eventListenerList.forEach(function(listener) {
    object.removeEventListener(eventName, listener);
  });
}

function onOpen() {
  removeListedEventListeners(ws, "open", eventListenerListOnOpen);
  removeListedEventListeners(ws, "error", eventListenerListOnError);
}

function onError() {
  removeListedEventListeners(ws, "message", eventListenerListOnMessage);
  removeListedEventListeners(ws, "error", eventListenerListOnError);
}

ws = new WebSocket("...");
ws.addEventListener("open", onOpen);
eventListenerListOnOpen.push(onOpen);
ws.addEventListener("error", onError);
eventListenerListOnError.push(onError);

1 Comment

Nothing really prevents moving my instantiation of ws above the function definitions. That was just an oversight as I scribbled down the question. That said, I really like this answer since it provides a generic solution that could be applied in alternate scenarios, and actually looks like a design pattern.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.