Tuesday, November 26, 2013

Pitfalls for JavaScript beginners

Eval. According to ES5, indirect calls to eval like var a = eval; a(code) or (0, eval)(code) or window.eval(code) are treated as a global evals, i.e., they can't access or declare variables in the lexical scope. For example,
var x=1; (function(){ var x=0; (eval)("console.log(x)"); })();   // prints 0
var x=1; (function(){ var x=0; (0,eval)("console.log(x)"); })(); // prints 1
You can check this post by Juriy Zaytsev for more information about global eval. According to another post of the same author, which examines the delete operator and eval, you can also delete a variable declared in eval:
(function (){ var x = 10; return delete x; })()          // false
(function (){ eval('var x = 10;'); return delete x; })() // true
In words, eval-introduced bindings do not have the DontDelete property set on them, so they can be deleted unlike proper lexical variables. Note that the behaviours of eval will change if strict mode is used.

Variable scope. In JavaScript, variables declared by var are scoped by closures, not by blocks. In particular, variables declared in a block can overwrite a global variable. This feature may confuse programmers familiar with languages such as C++ and Java, where variables are scoped by blocks.
var name = "Joe";
if(true) { var name = "Jack" }        // name is "Jack" now
(function() { var name = "Joe" })()  // name is still "Jack"
One therefore has to use a closure to declare a genuine local variable. The let keyword introduced in ECMA 6 declares block-scoped variables and helps avoid this pitfall.

Binding vs assignment. In most imperative languages including JavaScript, variable declaration creates a reference to a mutable value. In the following example, variable i enclosed in the anonymous function points to a value which is mutated by further iterations of the loop.
for (var i=0; i<3; ++i) {
  if(i==1) setTimeout(function (){ console.log(i) }, 1000);
}// prints 3
To capture the value of i at creation time, we can exploit a feature of JavaScript that functions are called by value when the parameters are of primitive types. In the following, the value of i is copied to parameter j when the anonymous function is created. Thus the value of i is captured as expected.
for (var i=0; i<3; ++i) {
  if(i==1) setTimeout((function (j){ 
      return function (){ console.log(j) }
  })(i), 1000);
}// prints 1
In general, binding creates a new variable within the current context, while assignment changes the value of an existing variable within the narrowest scope. In languages such as SML and Go, you can use different syntactic rules to choose between binding and assignment. In JavaScript, the "=" symbol always denotes an assignment. One however can bind a variable through a call-by-value function parameter, as is shown in above example.

Function declaration. When a JavaScript program executes, it runs with context (variable bindings, call stack, etc) and process (statements to be invoked in sequence). Declarations contribute to the context when the execution scope is entered. They are different from statements and are not subject to the order in which statements are invoked. In the following example, function foo is returned even though the code that defines it is unreachable at runtime:
(function (){
  return foo; function foo(){} 
})();
In ES5, function declarations are forbidden within non-function blocks (such as an if block). However, all browsers allow them and interpret them in different ways. For example, consider
(function (){
  if(false) { function foo(){} } return foo;
})();
In Firefox, the function declaration is interpreted as a statement. Thus foo is not defined when it is returned, which would cause a runtime error. In IE, Chrome and Safari, however, the function is returned as expected with standard function declarations.

Declaration hoist. In JavaScript, declarations of functions and variables are hoisted (moved) to the beginning of their innermost enclosing scope. Note that it is the declaration that got hoisted, not the assignment expressions. In the following example, the global variable x is shadowed by the local one due to declaration hoist.
var x = 0;
(function (){ 
  console.log(x);
  var x = 1;
})() // prints "undefined"
equals
to
var x = 0;
(function (){
  var x;
  console.log(x);
  x = 1;
})() // prints "undefined" 
When a function declaration is hoisted, the entire function definition is lifted with it. The following example shows an interested consequence of the difference between hoisting variable declaration and function declaration:
(function (){
  function foo() { return 0 }
  return foo();
  function foo() { return 1 }
})(); // returns 1
(function (){
  var foo = function (){ return 0 }
  return foo();
  var foo = function (){ return 1 }
})(); // returns 0
In the left snippet, the definition of the second foo is hoisted and thus shadows that of the first foo. In the right snippet, only the declaration of the second foo is hoisted, and this doesn't change the result of the first assignment. See also the "function declaration" paragraph.

Array allocation. JavaScript arrays should be treated as a special hash table. When you initialize an array with a statement like var a = new Array(10), you don't allocate a memory of 10 cells as you do in Java and C++. Instead, you create an Array object with length property of value 10. You can use any type of keys to store any type of values in an array, e.g., a[-1] = 1, a["fruit"] = "apple", etc. The length property of an array is updated automatically to accommodate the largest non-negative integer key of its contained values. Whatever the keys are, an array traversal only visit the values with non-negative integer keys explicitly assigned, as is shown in this code: (see this thread for more details)
var a = new Array();
a[-1] = a["a"] = a[11] = 1;
console.log(a.length); // prints 12
// Map traversal: visit all keys ever set
for(var key in a) { if(a.hasOwnProperty(key) console.log(key + ': ' + a[key]) }
// Array traversal: visit all valid array indices
a.forEach(function (val, index){ console.log(index + ': ' + val) }) // prints "11: 1"
To actual allocate a memory block of 10 cells, use Array.apply(null, Array(10)). To assign initial values to an array, use either this trick or utility functions such as _.range of underscore.js.

Trailing comma. ES3 does not allow a trailing comma when defining an object literal. For example, we should write {foo1:"bar1",foo2:"bar2"} instead of {foo1:"bar1",foo2:"bar2",}. However, most browsers (except IE) go against the spec and allow both usages. ES5 resolved this issue by going with the majority and legitimizing the trailing comma in the spec. Note that a trailing comma is still not allowed in JSON according to ES5. Thus '{foo:"bar",}' does not represent a valid JSON object.

Non-commutative operators. Check this table for a detailed list of the surprising behaviors of operators +, *, == and ===. These behaviors result from JavaScript's eccentric type coercion rules. One can make use of these coercion rules to write extremely puzzling code, see this script for instance. If you are not that familiar with these rules, be extremely careful when you have to do arithmetic operations over objects of different types.

Discrete floating point. Numbers in JavaScript are internally stored in double-precision floating-point format. Hence, not all numbers can be exactly represented, even for those falling in the seeming reasonable range. For example,
var x = 9999999999999999;   // x == 10000000000000000
var eq = (.3 == .1 + .2);   // eq is false because .1 + .2 == .30000000000000004
If you need to check equality between numbers, you have to take relative error into account, e.g.:
function eq(x, y) { // x and y are numbers
  return (x==y) || (!(x>0)^(y>0) && Math.abs(x - y) < Number.EPSILON) 
}

(more to come in the future)

No comments:

Post a Comment