Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

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

Comments

@heavysixer
Copy link

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");}()
@reissbaker
Copy link
Contributor

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.

@solidsnack
Copy link

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.

@heavysixer
Copy link
Author

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.

@hshoff
Copy link
Member

hshoff commented Feb 4, 2013

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

@reissbaker
Copy link
Contributor

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.

@mjschlot
Copy link

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?

@reissbaker
Copy link
Contributor

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!

@reissbaker
Copy link
Contributor

(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.)

@mjschlot
Copy link

Aaah. I get it now. Thanks.

@dmitriz
Copy link

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
Copy link

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
Copy link

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.

@rstacruz
Copy link

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants