邹明潮 Notes: YDKJS Types & Grammar

邹明潮
5 min readMay 30, 2017

Type

undefined vs undeclared

  • undeclared: a variable that has not been formally declared
  • undefined: a variable that has been declared without a value in it
var a;

a; // undefined
b; // ReferenceError: b is not defined
/* the typeof operator returns "undefined" even for "undeclared" variables. This is a special safety guard in the behavior of typeof */
typeof a; // "undefined"
typeof b; // "undefined"

Check a variable(DEBUG) wether is declared in either global scope or built-in API.

// oops, this would throw an error!
if (DEBUG) {
console.log( "Debugging is starting" );
}

// this is a safe existence check
if (typeof DEBUG !== "undefined") {
console.log( "Debugging is starting" );
}

Unlike referencing undeclared variables, there is no ReferenceError thrown if you try to access an object property that doesn't exist.

if (window.DEBUG) {
// ..
}

Values

String

JavaScript strings are immutable, while arrays are quite mutable. And none of the string methods that alter its content in-place, but rather must create and return new strings.

Numbers

Small Decimal Values

/* the representations for 0.1 and 0.2 in binary floating-point are not exact, so when they are added, the result is not exactly 0.3. It's really close: 0.30000000000000004. */
0.1 + 0.2 === 0.3; // false

The most commonly accepted practice is to use a tiny “rounding error” value as the tolerance for comparison. This tiny value is often called “machine epsilon,” which is commonly 2^-52 for the kind of numbers in JavaScript.

if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

Value Versus Reference

Simple values (primitives) are always assigned/passed by value-copy: null, undefined, string, number, boolean, and ES6's symbol.

Compound values — object and function -- always create a copy of the reference on assignment or passing.

var a = 2;
var b = a; // `b` is always a copy of the value in `a`
b++;
a; // 2
b; // 3

var c = [1,2,3];
var d = c; // `d` is a reference to the shared `[1,2,3]` value
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]

The type of the value solely controls wether that value will be assigned by value-copy or reference-copy.

/* The problem is that the underlying scalar primitive value is not mutable. If a Number object holds the scalar primitive value 2, that exact Number object can never be changed to hold another value; you can only create a whole new Number object with a different value.When x is used in the expression x + 1, the underlying scalar primitive value 2 is unboxed (extracted) from the Number object automatically, so the line x = x + 1 subtly changes x from being a shared reference to the Number object, holding the scalar primitive value 3 as a result of the addition operation 2 + 1. Therefore, b on the outside still references the original unmodified/immutable Number object holding the value 2. */function foo(x) {
x = x + 1;
x; // 3
}

var a = 2;
var b = new Number( a ); // or equivalently `Object(a)`

foo( b );
console.log( b ); // 2, not 3

In JavaScript, references are not like references/pointers in other language — they’re never pointed at other variables/references, only at the underlying values.

Natives

Here’s a list of the most commonly used natives:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol() -- added in ES6!

As you can see, these natives are actually built-in functions.

JavaScript provides object wrappers around primitive values, known as natives (String, Number, Boolean, etc). These object wrappers give the values access to behaviors appropriate for each object subtype (String#trim() and Array#concat(..)).

If you have a simple scalar primitive value like "abc" and you access its length property or some String.prototypemethod, JS automatically "boxes" the value (wraps it in its respective object wrapper) so that the property/method accesses can be fulfilled.

Grammar

Operator

? : & = are right-associative

a ? b : c ? d : e                     //a ? b : (c ? d : e)var a, b, c;
a = b = c = 42;

Preference: && > || > ? : > =

var a = 42;
var b = "foo";
var c = false;

var d = a && b || c ? c || b ? a : c && b : a;

d; // 42
((a && b) || c) ? ((c || b) ? a : (c && b)) : a

Let’s solve it now:

  1. (a && b) is "foo".
  2. "foo" || c is "foo".
  3. For the first ? test, "foo" is truthy.
  4. (c || b) is "foo".
  5. For the second ? test, "foo" is truthy.
  6. a is 42.

Conclusion: use operator precedence/associativity where it leads to shorter and cleaner code, but use ( ) manual grouping in places where it helps create clarity and reduce confusion.

Arguments

/* Never refer to a named parameter and its corresponding arguments slot at the same time */function foo(a) {
a = 42;
console.log( arguments[0] );
}

foo( 2 ); // 42 (linked)
foo(); // undefined (not linked)

try..finally

Case 1:

/* The return 42 runs right away, which sets up the completion value from the foo() call. This action completes the tryclause and the finally clause immediately runs next. Only then is the foo() function complete, so that its completion value is returned back for the console.log(..) statement to use. */function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}

console.log( "never runs" );
}

console.log( foo() );
// Hello
// 42

Case 2:

/* A return inside a finally has the special ability to override a previous return from the try or catch clause, but only if return is explicitly called */function baz() {
try {
return 42;
}
finally {
// override previous `return 42`
return "Hello";
}
}
baz(); // "Hello"

switch

Case 1:

/* This works because the case clause can have any expression (not just simple values), which means it will strictly match that expression's result to the test expression (true). Since a == 42 results in true here, the match is made. */var a = "42";

switch (true) {
case a == 10:
console.log( "10 or '10'" );
break;
case a == 42:
console.log( "42 or '42'" );
break;
default:
// never gets here
}
// 42 or '42'

Case 2:

/* The way this snippet processes is that it passes through all the case clause matching first, finds no match, then goes back up to the default clause and starts executing. Since there's no break there, it continues executing in the already skipped over case 3 block, before stopping once it hits that break. */var a = 10;

switch (a) {
case 1:
case 2:
// never gets here
default:
console.log( "default" );
case 3:
console.log( "3" );
break;
case 4:
console.log( "4" );
}
// default
// 3

--

--