JavaScript The Bad Parts

Overview

  • Declarations.

  • Problems because of automatic conversions.

  • Automatic semi-colon insertion problems.

Joke

Source: reddit.

Variable Declarations

  • Variables can be declared using var or the newer let. Note that declaration is simply var x = 1 or let x = 1; there is no type as js variables do not have types.

  • Constant variables (cannot be assigned after initialization) are declared const. Note that if a const variable contains an object, then the insides of that object can still be changed (unless it is dynamically frozen using Object.freeze()).

  • Every js system has an implicit global object: window in a browser, the current module in a nodejs script. If an undeclared variable is assigned to, then it is created in this global object. Can be avoided using "use strict".

  • Variables have lexical scope; i.e. their scope is limited to the syntactic construct within which they are declared.

Variable Hoisting

All variables declared using var are implicitly hoisted as though they were declared at the start of the containing function. (Note: logs edited for readability).

> var x = 3
undefined
> function f(a) { 
    var y = 5;
    if (a > 1) {
      var x = y;
    }
    return x + y;
  }
undefined
> f(2)
10
>

Variable Hoisting Effect

The var x declaration in the previous function is hoisted to the start of f(). Hence the scope of all variables declared using var within a function is the entire function, irrespective of the point where the variable was actually declared.

function f(a) {
  var x;
  var y = 5;
  if (a > 1) {
    x = y;
  }
  return x + y;
}

This behavior can be prevented by using let.

Declaring Variables Using let

> var x = 3;
undefined
> function f_let(a) {
    let y = 5;
    if (a > 1) {
      let x = y;
    }
    return x + y;
  }
undefined
> f_let(2)
8

Behavior of let has fewer surprises; prefer let over var in new code.

Surprising Effects of Variable Hoisting

> var x = 1
undefined
> function f(a) {
  //var x effectively declared here
    x = 2;
    if (a) { var x = 3 } //declaration hoisted
    return x;
  }
undefined
> f(1)
3
> f(0)
2
> x
1
>

Unsurprising Effects using let

> let x = 1
> function f(a) {
    x = 2;
    if (a) { let x = 3 } //{...} block effectively a NOP
    return x;
  }
> x
1
> f(1)
2
> x
2
> f(0)
2
> x
2
>		  

Temporal Dead Zones using let

A let declaration takes effect at the start of the block in which it occurs. Leads to temporal dead zones.

> a = 1
1
> function f() {
    //let a at this point
    let b = a + 2; //not external a
    let a = 5;
    return b + a;
  }
undefined
> f()
ReferenceError: a is not defined
    at f (repl:1:24)
> 

Declarations

  • My order of descending preference: const, let, var.

  • Convention is to use all uppercase names for manifest constants.

  • Many JS programs have multiple declarations using a single specifier:

           let var1 = value1,
               var2 = value2,
               ...
               varN = valueN;
    

    I consider that error prone and would prefer that each declaration stand alone using its own let specifier. Prefer destructuring declaration using array literal notation on both sides as in

        let [var1, var2, ..., varN] = [value1, value2, ..., valueN];
    
  • If you assign to an undeclared variable, then that variable will be created as a property of the global object. Force error by always specifying "use strict" within scripts. Not an issue for modules which are implicitly "use strict".

Declarations Summary

  • The scope of all JavaScript declarations are hoisted to start of a syntactic construct.

  • var declarations are hoisted to start of containing function; let and const declarations are hoisted to start of containing block or loop.

  • Use of var variable within function before initialization results in undefined.

  • Use of const or let variable within scope before declaration results in ReferenceError because of temporal dead-zone.

  • Behavior of const and let less surprising because of smaller scope.

  • Avoid var in new code; use const and let.

Octal Numbers

  • Misfeature inherited from C.

  • Integer literal starting with 0 treated as octal number when it does not contain any non-octal digits.

  • Integer literals with leading 0 cause error when 'use strict' in effect.

> 077
63
> 078  //non-octal digit results in silent base-10
78
> 0o77 //newer syntax
63
> 0o78 //non-octal digits cause error
0o78
^^
Uncaught SyntaxError: Invalid or unexpected token

Conversions

When used without new, Number(), String(), Boolean() can be used to explicitly convert between primitives. Recommended.

> Number('3')
3
> Number(false)
0
> Number(undefined)
NaN
> Number(null)
0
> Number(true)
1
> String(true)
'true'
> String(3+1)
'4'
> String(undefined)
'undefined'

Evolution of Undesirability of Implicit Conversion

$ perl -de1  #crude perl REPL
Loading DB routines from perl5db.pl version 1.51 ...
  DB<1> print 1 + '2'
3
$ node   #js REPL
> 1 + '2'
'12'
$ python3 #python3 REPL
Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
...
>>> 1 + '2'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
$ irb #ruby REPL
> 1 + '2'
...
TypeError (String can't be coerced into Integer)
$

Implicit Conversions within Expressions

  • The fact that 0, "" and NaN are treated as falsy values within boolean contexts can often cause surprises.

  • Never use x === NaN (in any language); use isNaN(x) instead.

  • Very complex conversion rules; best to avoid in new code, but need to handle legacy code.

  • Operators where conversions occur include + (both prefix and infix), - (both prefix and infix), other arith/relational ops.

  • + is used for both strings (concatenation) and numbers (addition). If either operand is a string then we are doing concatenation.

Some Simple Conversions

> 1 + '2'
'12'
> '2' * 3
6
> false * 6
0
> null + 5
5
> undefined * 4
NaN
> true * '5'
5
> + '123'  //Number to String idiom; prefer Number('123')
123

More Conversion Examples

> 1 + 2 + "3" + 4  //left-assoc +: ((1 + 2) + "3") + 4
'334'
> a = '1'
'1'
> a = a + 3 + 6  //concat "3" + "6" to a.
'136'
> a += 3 + 6     //numeric add 3 + 6
'1369'
>

A Glimpse at Conversion Rules

Arithmetic and concatenation expressions are evaluated using primitive operands. Specifically, if we are looking for a primitive operand as a Number:

  1. If operand is primitive, then nothing needs to be done.

  2. If operand is an object obj and obj.valueOf() returns a primitive object, then return that primitive object.

  3. If operand is an object obj and obj.toString() returns a primitive object, then return that primitive object.

  4. Otherwise throw a typeerror.

If we are looking for a primitive operand as a String, then interchange steps 2 and 3.

Object Conversion Examples

> x = { toString: function() { return "5"; },
...     valueOf: function() { return 2; } }
{ [Number: 2] toString: [Function: toString],
  valueOf: [Function: valueOf] }
> x + 3
5
> x + '3' //+ calls valueOf() first for both operands
'23'
> String(x)
'5'
>

Equality

  • js has both == and === operators along with corresponding != and !== operators.

  • Loose equality operator == tries to convert its operands to the same type before comparison.

  • Strict equality operator === does not do type conversion; simply returns false if types are different.

  • Almost always use === and !==; do not use == or !=.

  • Do a google search on js wtf.

Equality Examples

> '1' == 1
true
> '1' === 1
false
> undefined == null
true
> undefined === null
false
> '' == 0
true
> 0 == '0'
true
> '' == '0'
false               //breaks transitivity
>

Brace Ambiguity

  • Braces have two purposes within JavaScript syntax:

    1. Serve to delimit object literals. Braces are treated as object literal delimiters when they occur in an expression context.

    2. Serve to delimit code blocks. Braces are treated as code block delimiters when they are in a non-expression context.

  • When braces occur in an ambiguous context, they are always treated as code block delimiters.

  • For example, an attempt to write an anonymous function x => { value: x } to wrap parameter x in an object is wrong, since the { } are treated as code delimiters. The function should be rewritten as x => ({ value: x }) instead.

Semicolon Insertion

Automatic Semicolon Insertion (ASI):

  • Insert semicolon at newline if that fixes syntax error.

  • Always insert semicolon after return, break, continue when followed by a newline.

  • Always insert semicolon if next line starts with ++ or --.

> function f() {
    return 5
      + 3
  }
undefined
> f()
8

Semicolon Insertion Continued

Can cause problems:

> function f() { //silently returns undefined
    return     
     {         //start of unreachable code block
       a:      //label!
       false   //expression statement
     }
  }
undefined
> f()
undefined

Object Wrappers for Primitives

The primitive types string, number, boolean can be wrapped as objects using constructors new String(), new Number(), new Boolean(). Wrapping and unwrapping are done automatically as needed.

> x = new Number(3) //use wrapper constr
[Number: 3]
> typeof x          //x is an object
'object'
> typeof 3 
'number'
> x + 1             //automatically unwrapped
4
> x.a = 2           //object property assign
2
> x.a + x           //property + unwrap
5

Wrappers Continued

We can even define properties on primitive literals, but not of much use.

> 3.x = 22
3.x = 22
^^
SyntaxError: Invalid or unexpected token
> > 3.0.x = 22
22
> 3.0.x
undefined
>

Wrapper object automatically created, but since we do not retain a reference to it, we cannot access it. This behavior turned off in strict mode.

eval() Can Be Dangerous

  • eval(code) compiles and executes String code.

  • Can be a tremendous security risk if code depends on an untrusted source.

  • Normally, it runs in the local scope, but runs in global scope if called indirectly.

  • Note that the Function() constructor provides a similar capability which is somewhat more secure.

Using eval() To Implement a Spreadsheet

ss-using-eval.html. Running App.

  • No error checking.

  • Cell values maintained in global window variables.

  • eval() used for evaluating spreadsheet formulas.

  • Since there is no validation on formula strings, it is clear that this can be a security problem if used within a sensitive context.

The with Statement

  • A with statement:

        with (expression) statement
    

    executes statement with the object specified by expression interposed into the scope chain.

  • Not recommended; forbidden when strict which means it cannot be used in ES6 modules (which are always strict).

  • Can degrade performance.

  • Instead of with assign result of expression to a temporary object and replace references to "variables" in statement to property references on the temporary object.

Using eval() and with To Implement a Spreadsheet

ss-using-eval-and-with.html; Running App.

  • No error checking.

  • Cell values are not maintained in global window variables; instead they are maintained in a values object.

  • When eval() is used for evaluating a spreadsheet formula, the eval() is done using with (values) eval(formula).