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 number
s 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.prototype
method, 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:
(a && b)
is"foo"
."foo" || c
is"foo"
.- For the first
?
test,"foo"
is truthy. (c || b)
is"foo"
.- For the second
?
test,"foo"
is truthy. a
is42
.
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