Defensive programming benefit of the bang method ("!") is not correct. #44

Closed
heavysixer opened this Issue Feb 2, 2013 · 13 comments

Comments

Projects
None yet
9 participants

First off this is a great document, and thanks for sharing it. I have read the entire file end-to-end as I work on my own personal style guide. However, I was looking at your module example and you assert that "!" protects you from concatenation errors. However, I don't think this is correct. Consider the following example:

// => SyntaxError: Unexpected token !
!function(){console.log("ham");}()!function(){console.log("cheese")}();

Maybe I am misunderstanding how you expected things to work.

I think you still need to prepend a semicolon to truly protect yourself from concatenation errors.

;!function(){console.log("ham");}()
Contributor

reissbaker commented Feb 4, 2013

You would have to prepend a semicolon... If the cat command concatenated files together that way. Fortunately, though, it doesn't:

$ cat ham.js cheese.js
> !function(){console.log("ham");}()
> !function(){console.log("cheese");}();

The cat command (and *nix-land concatenation in general) concatenates files onto separate lines. That's why bang modules work: the ! at the start of the line allows Javascript ASI to kick in on the previous line and insert any missing semicolons for you. As proof, try evaluating the following in the console:

!function(){console.log("ham");}() // Missing semicolon!
!function(){console.log("cheese");}();

As you can see, there's no TypeError. The only similarly safe equivalent in the traditional module-land is ;(function(){}());, which is just silly.

I can't speak for file concatenation in Windows environments — all I can say is we don't use them for deployment at Airbnb, and so we don't worry about their behavior. My hunch is things work the same way over there too, though.

UNIX cat merely concatenates streams of bytes; it's not really aware of
whether the stream is text with newlines or not.

The xxd tool, which presents binary data in hexadecimal format, can be a
great help for investigating these things.

 :; echo 0x61 | xxd -p -r > onea      # Put a single ASCII 'a' in a file.
 :; echo 0x62 | xxd -p -r > oneb      # Put a single ASCII 'b' in a file.
 :; echo 0x0a | xxd -p -r > onenl     # Put a single ASCII newline in a file.
 :; wc -c onea oneb onenl             # Each file has only one byte.
       1 onea
       1 oneb
       1 onenl
 :; wc -l onea oneb onenl             # Only the last file has a "line".
       0 onea
       0 oneb
       1 onenl
 :; cat onea oneb onenl               # The 'a' and 'b' are joined on one line.
  ab

Many people see UNIX is all about flat text files, but really it's all about
flat binary files and then text has to fit within that. So if your JS files
don't end with a newline...

Maybe in this case, instead of inserting semicolons you could insert newlines.
A newline is just as many bytes as a semicolon and definitely easier to read.

Thanks @reissbaker and @solidsnack, I learned something here. Both of you are right when I was trying to understand how the bang method was defensive I did simulate file concatenation manually.

However, as you point out the protection is not derived from the method itself, rather its a function of the fact that it works when preceded with a newline, which happens to be how *NIX environments handle things. I think I might make a blog post about this, because I think there are others under the same misconception as myself. Thanks again for sharing your JS knowledge.

heavysixer closed this Feb 4, 2013

Owner

hshoff commented Feb 4, 2013

Thanks for the discussion, this was a good read! 🍻

Contributor

reissbaker commented Sep 30, 2013

Wow, only recently got linked back to this thread. @solidsnack, thank you, that was enlightening!

One more note on !-modules:

Maybe in this case, instead of inserting semicolons you could insert newlines.
A newline is just as many bytes as a semicolon and definitely easier to read.

Unfortunately, a newline isn't sufficient to prevent later (non-!-style) modules from throwing errors. For example, run the following in a Javascript console:

!function() {}()
(function() {}());

Despite the presence of a newline following the !-module, the above code will throw a TypeError because the ( on the second line prevents ASI on the first. In general it's dangerous to leave out semicolons at the end of a file, because a file that works standalone may break when concatenated with other files in various orders.

However, a !-module with a terminating semicolon is safe regardless of the safety of what precedes or follows it, as running the following example should illustrate:

(function() {}())
!function() {}();
(function() {}())

I suppose the reason !-modules generally are safe is because most text editors terminate files with the LF character (Windows editors use both a CR and an LF, but the existence of the LF is sufficient). I'd imagine this is a relatively safe thing to rely on, especially in an all-*nix environment — for an example of another tool that expects this behavior, GCC will issue warnings when trying to compile files that don't end with a newline.

TL;DR: Using !-modules with a terminating semicolon will prevent accidental errors that arise due to concatenation using the cat program or other programs like it, assuming all files were written in suitable text editors that insert terminating LF characters.

This discussion is quite old, but I have to ask... Why would ";" be silly? It seems like a more logical option. A "!" actually seems very illogical. Is there a reason other than personal preference?

Contributor

reissbaker commented Feb 21, 2014

Unfortunately, a ; wouldn't work — the following function would be considered a function declaration, rather than a function expression, and attempting to immediately invoke it would throw a SyntaxError. Try it in your JS console!

Contributor

reissbaker commented Feb 21, 2014

(I'm assuming you mean the following:

;function(){
  // module
}();

vs. our current recommendation:

!function(){
  // module
}();

Unfortunately, the semicolon-prefixed function will cause the invocation to throw an error.)

Aaah. I get it now. Thanks.

dmitriz commented Apr 19, 2014

I am surprised there is no mention of:
;(function(){
}());

This seems to be the safest of all and removing leading ';' won't break it.

iod commented Jun 27, 2014

For javascript < ES6 I am personally a fan of using void instead of ! (bang), because I find the bang distorts the intention whereas void seems much more appropriate:

// module 1
void function module1(context){
  // module 1 code...

  // local variables are declared like
  var localModuleVariable = 'some local variable'
  // export to parent context
  context.module1 = 'module 1 loaded';
}(this);

// module 2
void function module2(context){
  // module 2 code...
  context.module2 = 'module 2 loaded';
}(this);

// anonymous iife
void function(){
  // anonymous iife code...
}();

But in ES6 we get block scoping with let variables so you could do:

// module 1
{
  // module 1 code...

  // local variables are now declared with let to be scope-able
  let localModuleVariable = 'some local variable';
  // export to parent context
  var module1 = 'module 1 loaded';
}

// module 2
{
  // module 2 code...
  var module2 = 'module 2 loaded'
}

// anonymous iife
{
  // anonymous iife code...
}

p3k commented Mar 9, 2015

@iod you still would have to prefix void with a semicolon if you want to prevent errors with sloppy code on concatenation. Nevertheless, I like the idea with void very much.

@foca foca added a commit to 13Floor/philote-js that referenced this issue Apr 1, 2015

@foca foca Use the "bang-module" pattern 7edab7c

rstacruz commented Jul 8, 2015

or a comment would be fine @p3k, which you might have when using eslint:

/* global define */
void function () {
}(...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment