Self-invoking functions are normal functions, but don't have a name. That makes them impossible to call except immediately after being declared, which effectively makes them a one-time-use function that is forever hidden from other code.
As you point out, the form
function foo() {
console.log("foo");
}
foo()
and
(function () {
console.log("foo");
})()
behave identically.
However, the first form with function foo() {...
creates a named function that lives in that scope until the scope is destroyed. For the global scope, that will never happen, leaving foo
a valid and callable identifier for any other code in your application.
That means that at any time, anybody else can do
foo()
and either initialize your module again or, potentially, obtain a handle to a new copy of the module.
At best, this can cause confusion and duplicate state. At worst, depending on how your IIFE is structured, they may be able to dig into hidden state and start manipulating it in ways you don't expect.
Writing a function that is both anonymous and immediately invoked prevents anyone from ever invoking it again, making it a clean and safe way to create and use a completely hidden scope. That's why you see it used so often to provide private state or construct a module.