Skip to content

Instantly share code, notes, and snippets.

@nathansmith
Last active March 1, 2022 16:11
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nathansmith/eda4ca58deda2d48203cd6bd4c229b0c to your computer and use it in GitHub Desktop.
Save nathansmith/eda4ca58deda2d48203cd6bd4c229b0c to your computer and use it in GitHub Desktop.
JavaScript "gotchas" from a presentation to coworkers at TandemSeven.
/*
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