Last active
March 1, 2022 16:11
-
-
Save nathansmith/eda4ca58deda2d48203cd6bd4c229b0c to your computer and use it in GitHub Desktop.
JavaScript "gotchas" from a presentation to coworkers at TandemSeven.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The following are examples of quirky and/or funny | |
JavaScript "gotchas". This file isn't meant to be | |
run in its totality. Rather, each section that is | |
deliniated with "// === //" style comments should | |
be run separately, using a browser's dev console. | |
I suggest trying these snippets on localhost | |
or "example.com" in order to see some of the | |
"phantom domain" image and iframe examples. | |
*/ | |
// =================== // | |
// =================== // | |
// ISO `Date` gotchas. // | |
// =================== // | |
// =================== // | |
// https://coderwall.com/p/0svzjq/gotcha-with-parsing-iso-8601-dates-in-javascript | |
console.log( | |
/* | |
Parsed as local time zone, because | |
it is _NOT_ a valid ISO 8601 string. | |
*/ | |
// "Mon May 01 2017 00:00:00 GMT-0500 (CDT)" | |
new Date('2017/05/01') | |
) | |
console.log( | |
/* | |
Parsed as GMT time zome, because | |
it _IS_ a valid ISO 8601 string. | |
*/ | |
// "Sun Apr 30 2017 19:00:00 GMT-0500 (CDT)" | |
new Date('2017-05-01') | |
) | |
// ===================== // | |
// ===================== // | |
// Global scope gotchas. // | |
// ===================== // | |
// ===================== // | |
/* | |
Same as `window.foo = 'TEST'`. | |
*/ | |
foo = 'TEST' | |
console.log(foo) // "TEST" | |
console.log(window.foo) // "TEST" | |
// Works like `delete window.foo`. | |
delete foo | |
console.log(foo) // `undefined` | |
console.log(window.foo) // `undefined` | |
/* | |
Globally scoped `var`. | |
*/ | |
var bar = 'TEST' | |
console.log(bar) // "TEST" | |
console.log(window.bar) // "TEST" | |
// Doesn't work. | |
delete bar | |
console.log(bar) // "TEST" | |
console.log(window.bar) // "TEST" | |
/* | |
Globally scoped `let`. (Same for `const`.) | |
*/ | |
let baz = 'TEST' | |
console.log(baz) // "TEST" | |
console.log(window.baz) // `undefined` | |
// Doesn't work. | |
delete baz | |
console.log(baz) // "TEST" | |
console.log(window.baz) // `undefined` | |
// ==================== // | |
// ==================== // | |
// Block scope gotchas. // | |
// ==================== // | |
// ==================== // | |
if (true) { | |
// `var` leaks outside `if`. | |
var foo = 'TEST' | |
// `const` does not leak. | |
const bar = 'TEST' | |
// `let` does not leak. | |
let baz = 'TEST' | |
} | |
console.log(typeof foo) // "string" | |
console.log(typeof bar) // "undefined" | |
console.log(typeof baz) // "undefined" | |
// ======================================== // | |
// ======================================== // | |
// Change "truthy" and "falsey" to boolean. // | |
// ======================================== // | |
// ======================================== // | |
console.log(1) // `1` | |
console.log(!!1) // `true` | |
console.log(0) // `0` | |
console.log(!!0) // `false` | |
console.log( | |
false || {} // `{}` | |
) | |
console.log( | |
!!(false || {}) // `true` | |
) | |
// =================== // | |
// =================== // | |
// Comparison gotchas. // | |
// =================== // | |
// =================== // | |
console.log( | |
typeof null === typeof {} // `true` | |
) | |
console.log( | |
typeof null === typeof undefined // `false` | |
) | |
console.log(null == undefined) // `true` | |
console.log(null === undefined) // `false` | |
console.log(0 == '0') // `true` | |
console.log(0 === '0') // `false` | |
console.log(0 === 0) // `true` | |
console.log(-0 === +0) // `true` | |
console.log( | |
Object.is(0 === 0) // `true` | |
) | |
console.log( | |
Object.is(-0 === +0) // `false` | |
) | |
// `num3` is +1 too big! | |
const num1 = 9007199254740991 | |
const num2 = 9007199254740992 | |
const num3 = 9007199254740993 | |
// Correct. | |
console.log( | |
num1 === num2 // `false` | |
) | |
// Incorrect. | |
console.log( | |
num2 === num3 // `true` | |
) | |
// ====================== // | |
// ====================== // | |
// Type coercion gotchas. // | |
// ====================== // | |
// ====================== // | |
console.log(1 * 1) // `1` | |
console.log(1 * '1') // `1` | |
console.log(1 + 1) // `2` | |
console.log(1 + '1') // `11` | |
console.log('-' + 1 - 2) // `-3` | |
console.log('100' - 0 + 1) // `101` | |
console.log([] + []) // "" | |
console.log({} + []) // "[object Object]" | |
console.log([] + {}) // "[object Object]" | |
console.log( | |
eval('{} + []') // `0` | |
) | |
console.log( | |
eval('[] + {}') // "[object Object]" | |
) | |
// ======================== // | |
// ======================== // | |
// Existence check gotchas. // | |
// ======================== // | |
// ======================== // | |
const geolocation = { | |
lat: 0, | |
lon: 0 | |
} | |
/* | |
This never logs. I once made a Google Maps "bug" | |
that wouldn't render anything along the equator. | |
*/ | |
if ( | |
geolocation.lat && | |
geolocation.lon | |
) { | |
console.log('Notice me, senpai!') | |
} | |
// Existence checker. | |
function exists (x) { | |
return ( | |
x !== null && | |
typeof x !== 'undefined' | |
) | |
} | |
// This works. | |
if ( | |
exists(geolocation.lat) && | |
exists(geolocation.lon) | |
) { | |
console.log('Notice me, senpai!') | |
} | |
// ======================== // | |
// ======================== // | |
// Poor man's "deep" clone. // | |
// ======================== // | |
// ======================== // | |
function cloneDeep (o) { | |
return JSON.parse( | |
JSON.stringify(o) | |
) | |
} | |
console.log( | |
cloneDeep({a: null}) // `{a: null}` | |
) | |
console.log( | |
cloneDeep({a: undefined}) // `{}` | |
) | |
// ============================= // | |
// ============================= // | |
// Poor man's "deep" comparison. // | |
// ============================= // | |
// ============================= // | |
// Doesn't work. | |
console.log( | |
[1, 2, 3] === [1, 2, 3] // `false` | |
) | |
// Doesn't work. | |
console.log( | |
{a: true} === {a: true} // `false` | |
) | |
function isEqual (a, b) { | |
const f = JSON.stringify | |
return f(a) === f(b) | |
} | |
const obj1 = {a: true} | |
const obj2 = {a: true, b: undefined} | |
console.log( | |
isEqual(obj1, obj2) // `true` | |
) | |
// ================================ // | |
// ================================ // | |
// Object/Array detection: gotchas. // | |
// ================================ // | |
// ================================ // | |
// "BAD" object detection. | |
function isObject (x) { | |
return typeof x === 'object' | |
} | |
// "BAD" array detection. | |
function isArray (x) { | |
return ( | |
typeof x === 'object' && | |
typeof x.length === 'number' | |
) | |
} | |
// Incorrect. | |
console.log( | |
isObject(null) // `true` | |
) | |
// Incorrect. | |
console.log( | |
isObject([]) // `true` | |
) | |
// Incorrect. | |
console.log( | |
isArray({length: 0}) // `true` | |
) | |
// =========================================== // | |
// =========================================== // | |
// Object/Array detection: Abusing `toString`. // | |
// =========================================== // | |
// =========================================== // | |
// "GOOD" object detection. | |
function isObject (x) { | |
x = toString.call(x) | |
x = x.toLowerCase() | |
return x === '[object object]' | |
} | |
// "GOOD" array detection. | |
function isArray (x) { | |
x = toString.call(x) | |
x = x.toLowerCase() | |
return x === '[object array]' | |
} | |
// Correct. | |
console.log( | |
isObject({}) // `true` | |
) | |
// Correct. | |
console.log( | |
isObject(null) // `false` | |
) | |
// Correct. | |
console.log( | |
isObject([]) // `false` | |
) | |
// Correct. | |
console.log( | |
isArray([]) // `true` | |
) | |
// Correct. | |
console.log( | |
isArray({length: 0}) // `false` | |
) | |
// ======================================== // | |
// ======================================== // | |
// Object/Array detection: Modern browsers. // | |
// ======================================== // | |
// ======================================== // | |
// "GOOD" object detection. | |
function isObject (x) { | |
return ( | |
x !== null && | |
!Array.isArray(x) && | |
typeof x === 'object' | |
) | |
} | |
// "GOOD" array detection. | |
function isArray (x) { | |
return Array.isArray(x) | |
} | |
// Correct. | |
console.log( | |
isObject({}) // `true` | |
) | |
// Correct. | |
console.log( | |
isObject(null) // `false` | |
) | |
// Correct. | |
console.log( | |
isObject([]) // `false` | |
) | |
// Correct. | |
console.log( | |
isArray([]) // `true` | |
) | |
// Correct. | |
console.log( | |
isArray({length: 0}) // `false` | |
) | |
// =============== // | |
// =============== // | |
// Number gotchas. // | |
// =============== // | |
// =============== // | |
console.log( | |
// `true` (1 === 1) | |
Number('1.0') === parseFloat('1.0') | |
) | |
// Does conversion. | |
console.log( | |
Number(true) // `1` | |
) | |
// Does not convert. | |
console.log( | |
parseFloat(true) // `NaN` | |
) | |
// `NaN` means "Not a Number". | |
console.log( | |
isNaN(NaN) // `true` | |
) | |
// `NaN` means "Not a Number"!? | |
console.log( | |
typeof NaN // "number" | |
) | |
// `NaN` isn't the same as `NaN`. | |
console.log( | |
NaN === NaN // `false` | |
) | |
// Except when it is. `NaN` wut!? | |
console.log( | |
Object.is(NaN, NaN) | |
) | |
// Is "dog" a number? | |
console.log( | |
Number('dog') // `NaN` | |
) | |
// Okay, "dog" is not a number. | |
console.log( | |
isNaN('dog') // `true` | |
) | |
// Or is it? Coercion strikes again! | |
console.log( | |
Number.isNaN('dog') // `false` | |
) | |
// What, wut!? I give up. | |
console.log( | |
Number.isNaN(NaN) // `true` | |
) | |
// Parse integer, with radix. | |
console.log( | |
parseInt('0xF', 10) // `0` | |
) | |
// Parse integer, without radix. | |
console.log( | |
parseInt('0xF') // `15` | |
) | |
/* | |
Basically, never use either of these. | |
Just type the number `1` directly. :) | |
*/ | |
// `Number(…)` using `new`. | |
console.log( | |
typeof new Number('1') // {"[[PrimitiveValue]]": 1} | |
) | |
// `Number(…)` without `new`. | |
console.log( | |
typeof Number('1') // "number" | |
) | |
// ===================== // | |
// ===================== // | |
// Finite number gotcha. // | |
// ===================== // | |
// ===================== // | |
// Correct. | |
console.log( | |
isFinite(0) // `true` | |
) | |
// Correct. | |
console.log( | |
isFinite('0') // `true` | |
) | |
// Correct. | |
console.log( | |
Number.isFinite(0) // `true` | |
) | |
// Incorrect. | |
console.log( | |
Number.isFinite('0') // `false` | |
) | |
// ================ // | |
// ================ // | |
// Boolean gotchas. // | |
// ================ // | |
// ================ // | |
console.log( | |
true + true // `1` | |
) | |
console.log( | |
true * true // `1` | |
) | |
console.log( | |
true * false // `0` | |
) | |
console.log( | |
false / true // `0` | |
) | |
console.log( | |
true / false // `Infinity` | |
) | |
// ======================= // | |
// ======================= // | |
// Floating point gotchas. // | |
// ======================= // | |
// ======================= // | |
// http://floating-point-gui.de/basic | |
console.log( | |
0.2 - 0.1 // `0.1` | |
) | |
console.log( | |
0.2 + 0.1 // `0.30000000000000004` | |
) | |
console.log( | |
0.3 - 0.2 // `0.09999999999999998` | |
) | |
console.log( | |
0.3 + 0.2 // `0.5` | |
) | |
console.log( | |
1 + 1 // `2` | |
) | |
console.log( | |
1.2 + 1.1 // `2.3` | |
) | |
console.log( | |
1.2 - 1.1 // `0.09999999999999987` | |
) | |
// =================== // | |
// =================== // | |
// "BAD" add function. // | |
// =================== // | |
// =================== // | |
function add (a, b) { | |
return a + b | |
} | |
// Incorrect. | |
console.log( | |
add('1', '1') // `11` | |
) | |
// Incorrect. | |
console.log( | |
add('0.1', '0.2') // "0.10.2" | |
) | |
// ====================== // | |
// ====================== // | |
// "BETTER" add function. // | |
// ====================== // | |
// ====================== // | |
function add (a, b) { | |
a = parseFloat(a) | |
b = parseFloat(b) | |
return a + b | |
} | |
// Correct. | |
console.log( | |
add('1', '1') // `2` | |
) | |
// Incorrect. | |
console.log( | |
add('0.1', '0.2') // `0.30000000000000004` | |
) | |
// ==================== // | |
// ==================== // | |
// "BEST" add function. // | |
// ==================== // | |
// ==================== // | |
function add () { | |
const a = arguments | |
const f = Array.prototype.forEach | |
let total = 0 | |
f.call(a, function (item) { | |
item = item * 100 | |
if (!isNaN(item)) { | |
total += item | |
} | |
}) | |
return parseFloat( | |
(total / 100).toFixed(2) | |
) | |
} | |
// Correct. | |
console.log( | |
add('1', '1', '1') // `3` | |
) | |
// Correct. | |
console.log( | |
add('0.1', '0.2', '0.3') // `0.6` | |
) | |
// ============================ // | |
// ============================ // | |
// DOM element gotchas: inputs. // | |
// ============================ // | |
// ============================ // | |
document.body.innerHTML = ` | |
<!-- Enabled. --> | |
<input id="input1" /> | |
<!-- HTML disabled. --> | |
<input id="input2" disabled /> | |
<!-- XHTML disabled. --> | |
<input id="input3" disabled="disabled" /> | |
` | |
const input1 = document.getElementById('input1') | |
const input2 = document.getElementById('input2') | |
const input3 = document.getElementById('input3') | |
console.log(input1.disabled) // `false` | |
console.log(input2.disabled) // `true` | |
console.log(input3.disabled) // `true` | |
console.log(input1.getAttribute('disabled')) // `null` | |
console.log(input2.getAttribute('disabled')) // "" | |
console.log(input3.getAttribute('disabled')) // "disabled" | |
// ============================ // | |
// ============================ // | |
// DOM element gotchas: get ID. // | |
// ============================ // | |
// ============================ // | |
/* | |
A "hacky" way to get element by ID. | |
This wouldn't work if your element had | |
the same ID as a native `window.*` item. | |
*/ | |
document.body.innerHTML = ` | |
<!-- There is _NOT_ a *.foo global. --> | |
<br id="foo" /> | |
<!-- There _IS_ a *.navigator global. --> | |
<br id="navigator" /> | |
` | |
// DOM element. | |
console.log(window.foo) | |
// `navigator` object. | |
console.log(window.navigator) | |
// ===================================== // | |
// ===================================== // | |
// DOM element gotchas: Phantom domains. // | |
// ===================================== // | |
// ===================================== // | |
document.body.innerHTML = ` | |
<img src="file.jpg" alt="" /> | |
<iframe src="file.pdf"></iframe> | |
` | |
const img = document.querySelector('img') | |
const iframe = document.querySelector('iframe') | |
console.log(img.src) // Domain plus "file.jpg". | |
console.log(img.getAttribute('src')) // "file.jpg" | |
console.log(iframe.src) // Domain plus "file.pdf". | |
console.log(iframe.getAttribute('src')) // "file.pdf" | |
img.src = '' | |
iframe.src = '' | |
console.log(img.src) // Domain. | |
console.log(iframe.src) // Domain. | |
// ====================== // | |
// ====================== // | |
// BONUS: SILLY QUESTION! // | |
// ====================== // | |
// ====================== // | |
/* | |
When the following code is pasted into | |
the dev console, what does it output? | |
*/ | |
;(function() { | |
const hello = 'HELLO WORLD' | |
const arr = [ | |
'\x21', | |
'\x6E', | |
'\x61', | |
'\x6D', | |
'\x74', | |
'\x61', | |
'\x42' | |
] | |
let str = '' | |
let i = 16 | |
while (i--) { | |
str += 1 * hello | |
str += i % 2 === 0 ? '\x2C\x20' : '' | |
} | |
str = str.replace(/\x4E+/g, '\x6E') | |
str = str.replace(/\x6E\x2C/g, '\x2C') | |
str = str.slice(0, 1).toUpperCase() + str.slice(1, str.length) | |
str += arr.reverse().join('') | |
console.log(str) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment