denysdovhan / wtfjs
- вторник, 1 августа 2017 г. в 03:13:40
A list of funny and tricky JavaScript examples
A list of funny and tricky examples of JavaScript.
JavaScript is a great language. It has a simple syntax, large ecosystem and, what is the most important, great community.
At the same time, all we know that JavaScript is a quite funny language with tricky parts. Some of them can quickly turn our everyday job into hell, some of them can make us laugh out loud.
Original idea of WTFJS belongs to Brian Leroux. This list is highly inspired by his talk “WTFJS” at dotJS 2012:
[] is equal ![]NaN is not a NaN[] is truthy, but not truenull is falsy, but not falseundefined and NumberparseInt is a bad guytrue and falseNaN is [] and null are objects0.1 + 0.2Stringconstructor property__proto__`${{Object}}`try..catchJust for fun
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
The primary goal of this list is to collect some crazy examples and explain how they work, if possible. Just because it's fun to learn something that we didn't know before.
If you are a beginner, you can use this notes to get deeper dive into the JavaScript. I hope this notes will motivate you to spend more time reading the specification.
If you are a professional developer, you can consider these examples as a great resource for interview questions and quizzes for newcomers in your company. At the same time, these examples would be handy while preparing for the interview.
In any case, just read this. Probably you're going to find something new for yourself.
// -> is used to show the result of an expression. For example:
1 + 1 // -> 2// > means the result of console.log or other output. For example:
console.log('hello, world!') // > hello, world!// is just a comment for explanations. Example:
// Assigning a function to foo constant
const foo = function () {}[] is equal ![]Array is equal not array:
[] == ![] // -> true!!'false' == !!'true' // -> true
!!'false' === !!'true' // -> trueConsider this step-by-step:
true == 'true' // -> true
false == 'false' // -> false
// 'false' is not empty string, so it's truthy value
!!'false' // -> true
!!'true' // -> trueAn old-school joke in JavaScript:
"foo" + + "bar" // -> 'fooNaN'The expression is evaluted as 'foo' + (+'bar'), which converts 'bar' to not a number.
NaN is not a NaNNaN === NaN // -> falseThe specification strictly defines the logic behind this behavior:
- If
Type(x)is different fromType(y), return false.- If
Type(x)is Number, then
- If
xis NaN, return false.- If
yis NaN, return false.- … … …
Following the definition of NaN from the IEEE:
Four mutually exclusive relations are possible: less than, equal, greater than, and unordered. The last case arises when at least one operand is NaN. Every NaN shall compare unordered with everything, including itself.
— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” at StackOverflow
You would not believe, but …
(![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]
// -> 'fail'Breaking that mass of symbols into pieces we notices, that the following patten occurs often:
(![]+[]) // -> 'false'
![] // -> falseSo we try adding [] to false. But through a number of internal function calls (binary + Operator -> ToPrimitive -> [[DefaultValue]]) we end up with converting the right operand to a string:
(![]+[].toString()) // 'false'Thinking of a string as an array we can access its first character via [0]:
'false'[0] // -> 'f'Now, the rest is obvious and can figure out it by yourself!
[] is truthy, but not trueAn array is a truthy value, however, it's not equal to true.
!![] // -> true
[] == true // -> falseHere are links to the corresponding sections in the ECMA-262 specification:
null is falsy, but not falseDespite the fact that null is falsy value, it's not equal to false.
!!null // -> false
null == false // -> falseAt the same time, other falsy values, like 0 or '' are equal to false.
0 == false // -> true
'' == false // -> trueThe explanation is the same as for previous example. Here's a corresponding link:
Number.MIN_VALUE is the smallest number, which is greater than zero:
Number.MIN_VALUE > 0 // -> true
Number.MIN_VALUEis5e-324, i.e. the smallest positive number that can be represented within float precision, i.e. that's as close as you can get to zero. It defines the best resolution floats give you.Now the overall smallest value is
Number.NEGATIVE_INFINITYalthough that's not really numeric in the strict sense.— “Why is
0less thanNumber.MIN_VALUEin JavaScript?” at StackOverflow
⚠️ A bug present in V8 v5.5 or lower (Node.js <=7)⚠️
All you know about noisy undefined is not a function. What about this?
// Declare a class which extends null
class Foo extends null {}
// -> [Function: Foo]
new Foo instanceof null
// > TypeError: function is not a function
// > at … … …This is not a part of the specification. That's just a bug and now it's fixed, so there's shouldn't be a problem with this in future.
What if you try to add two arrays?
[1, 2, 3] + [4, 5, 6] // -> '1,2,34,5,6'The concatenation happens. Step-by-step it looks like this:
[1, 2, 3] + [4, 5, 6]
// joining
[1, 2, 3].join() + [4, 5, 6].join()
// concatenation
'1,2,3' + '4,5,6'
// ->
'1,2,34,5,6'undefined and NumberIf we don't pass any argument into the Number constructor, we'll get 0. undefined is a value assigned to formal arguments which there are no actual arguments, so you might expect that Number without arguments takes undefined as a value of its parameter. However, when we pass undefined, we will get NaN.
Number() // -> 0
Number(undefined) // -> NaNAccording to the specification:
n be +0.n be ? ToNumber(value).undefined, ToNumber(undefined) should return NaN.Here's a corresponding section:
parseInt is a bad guyparseInt is famous by his quirks:
parseInt('f*ck'); // -> NaN
parseInt('f*ck', 16); // -> 15parseInt will continue parsing character-by-character until it hits a character it doesn't know. The f in 'fuck' is hexadecimal 15.
Parsing Infinity to integer is something…
//
parseInt('Infinity', 10) // -> NaN
// ...
parseInt('Infinity', 18) // -> NaN...
parseInt('Infinity', 19) // -> 18
// ...
parseInt('Infinity', 23) // -> 18...
parseInt('Infinity', 24) // -> 151176378
// ...
parseInt('Infinity', 29) // -> 385849803
parseInt('Infinity', 30) // -> 13693557269
// ...
parseInt('Infinity', 34) // -> 28872273981
parseInt('Infinity', 35) // -> 1201203301724
parseInt('Infinity', 36) // -> 1461559270678...
parseInt('Infinity', 37) // -> NaNBe careful with parsing null too:
parseInt(null, 24) // -> 23It's converting
nullto the string"null"and trying to convert it. For radixes 0 through 23, there are no numerals it can convert, so it returns NaN. At 24,"n", the 14th letter, is added to the numeral system. At 31,"u", the 21st letter, is added and the entire string can be decoded. At 37 on there is no longer any valid numeral set that can be generated andNaNis returned.— “parseInt(null, 24) === 23… wait, what?” at StackOverflow
Don't forget about octals:
parseInt('06'); // 6
parseInt('08'); // 0parseInt accepts a second argument for radix. If it is not supplied and the string starts with a 0 it will be parsed as an octal number.
true and falseLet's do some math:
true + true // -> 2
(true + true) * (true + true) - true // -> 3Hmmm…
We can coerce values to numbers with Number constructor. It's quite obvious that true will be coerced to 1:
Number(true) // -> 1The unary plus operator attempts to convert its value into a number. It can convert string representations of integers and floats, as well as the non-string values true, false, and null. If it cannot parse a particular value, it will evaluate to NaN. That means we can coerce true to 1 easier:
+true // -> 1When you're performing addition or multiplication, ToNumber method invokes. In according to the specification, this method returns:
If
argumentis true, return 1. Ifargumentis false, return +0.
That's why we can add boolean values as regular numbers and get correct results.
Corresponding sections:
You will be impressed, but <!-- (which is known as HTML comment) is a valid comment in JavaScript.
// valid comment
<!-- valid comment tooImpressed? HTML-like comments were intended to allow browsers that didn't understand the <script> tag to degrade gracefully. These browsers, eg. Netscape 1.x are no longer popular. So there is really no point in putting HTML comments in your script tags anymore.
Since Node.js is based on V8 engine, HTML-like comments are supported in the Node.js runtime too. Moreover, they're a part of specification:
NaN is Despite the fact that type of NaN is a 'number', NaN is not instance of number:
typeof NaN // -> 'number'
NaN instanceof Number // -> falseExplanations of how typeof and instanceof operators work:
[] and null are objectstypeof [] // -> 'object'
typeof null // -> 'object'
// however
null instanceof Object // falseThe behavior of typeof operator is defined in this section of the specification:
According to the specifications, the typeof operator returns a string according to Table 35: typeof Operator Results. For null, ordinary, standard exotic and non-standard exotic objects which does not implement [[Call]] it returns string "object".
However, you can check the type of object using toString method.
Object.prototype.toString.call([])
// -> '[object Array]'
Object.prototype.toString.call(new Date)
// -> '[object Date]'
Object.prototype.toString.call(null)
// -> '[object Null]'999999999999999 // -> 999999999999999
9999999999999999 // -> 10000000000000000This is caused by IEEE 754-2008 standard for Binary Floating-Point Arithmetic. Read more:
0.1 + 0.2Well known joke from JavaScript. An addition of 0.1 and 0.2 is deadly precise:
0.1 + 0.2 // -> 0.30000000000000004The answer for the ”Is floating point math broken?” question on StackOverflow:
The constants
0.2and0.3in your program will also be approximations to their true values. It happens that the closestdoubleto0.2is larger than the rational number0.2but that the closestdoubleto0.3is smaller than the rational number0.3. The sum of0.1and0.2winds up being larger than the rational number0.3and hence disagreeing with the constant in your code.
This problem is so known that even there is a website called 0.30000000000000004.com.
You can add own methods to wrapper objects like Number or String.
Number.prototype.isOne = function () {
return Number(this) === 1
}
1.0.isOne() // -> true
1..isOne() // -> true
2.0.isOne() // -> false
(7).isOne() // -> falseObviously, you can extend Number object like any other object in JavaScript. However, it's not recommended if the behavior of defined method is not a part of the specification. Here is the list of Number's properties:
1 < 2 < 3 // -> true
3 > 2 > 1 // -> falseWhy does this work that way? Well, the problem is in the first part of an expression. Here's how it works:
1 < 2 < 3 // 1 < 2 -> true
true < 3 // true -> 1
1 < 3 // -> true
3 > 2 > 1 // 3 > 2 -> true
true > 1 // true -> 1
1 > 1 // -> falseWe can fix this with Greater than or equal operator (>=):
3 > 2 >= 1 // trueRead more about Relational operators in the specification:
Often the results of an arithmetic operations in JavaScript might be quite unexpectable. Consider these examples:
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaNWhat's happening in the first four examples? Here's a small table to understand addition in JavaScript:
Number + Number -> addition
Boolean + Number -> addition
Boolean + Boolean -> addition
Number + String -> concatenation
String + Boolean -> concatenation
String + String -> concatenation
What about the rest examples? A ToPrimitive and ToString methods are being implicitly called for [] and {} before addition. Read more about evaluation process in the specification:
+)input [,PreferredType])argument)Did you know you can add numbers like this?
// Patch a toString method
RegExp.prototype.toString = function() {
return this.source
}
/7/ - /5/ // -> 2String'str' // -> 'str'
typeof 'str' // -> 'string'
'str' instanceof String // -> falseThe String construnctor returns a string:
typeof String('str') // -> 'string'
String('str') // -> 'str'
String('str') == 'str' // -> trueLet's try with a new:
new String('str') == 'str' // -> true
typeof new String('str') // -> 'object'Object? What's that?
new String('str') // -> [String: 'str']More information about the String constructor in the specification:
Let's declare a function which logs all params into the console:
function f(...args) {
return args
}No doubt, you know you can call this function like this:
f(1, 2, 3) // -> [ 1, 2, 3 ]But did you know you can call any function with backticks?
f`true is ${true}, false is ${false}, array is ${[1,2,3]}`
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]Well, this is not magic at all if you're familiar with Tagged template literals. In the example above, f function is a tag for template literal. Tags before template literal allow you to parse template literals with a function. The first argument of a tag function contains an array of string values. The remaining arguments are related to the expressions. Example:
function template(strings, ...keys) {
// do something with strings and keys…
}This is the magic behind famous library called
Link to the specification:
Found by @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2])Attention, it could break your mind! Try to reproduce this code in your head: we're applying the call method using apply method. Read more:
thisArg, ...args)thisArg, argArray)constructor propertyconst c = 'constructor'
c[c][c]('console.log("WTF?")')() // > WTF?Let's consider this example step-by-step:
// Declare a new constant which is a string 'constructor'
const c = 'constructor'
// c is a string
c // -> 'constructor'
// Getting a constructor of string
c[c] // -> [Function: String]
// Getting a constructor of constructor
c[c][c] // -> [Function: Function]
// Call the Function constructor and pass
// the body of new function as an argument
c[c][c]('console.log("WTF?")') // -> [Function: anonymous]
// And then call this anonymous function
// The result is console-logging a string 'WTF'
c[c][c]('console.log("WTF?")')() // > WTFAn Object.prototype.constructor returns a reference to the Object constructor function that created the instance object. In case with strings it is String, in case with numbers it is Number and so on.
{ [{}]: {} } // -> { '[object Object]': {} }Why does this work so? Here we're using a Computed property name TODO(add link to spec). When you pass an object between those brackets, it coerces object to a string, so we get a property key '[object Object]' and value {}.
The same way we can make brackets hell like this:
({[{}]:{[{}]:{}}})[{}][{}] // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }Read more about object litarals here:
__proto__As we know, primitives don't have prototypes. However, if we try to get a value of __proto__ for primitives, we would get this:
(1).__proto__.__proto__.__proto__ // -> nullIt happens because of when primitive doesn't have a prototype, it will be wrapped in a wrapper object using ToObject method. So, spet-by-step:
(1).__proto__ // -> [Number: 0]
(1).__proto__.__proto__ // -> {}
(1).__proto__.__proto__.__proto__ // -> nullHere is more information about __proto__:
`${{Object}}`What the result of the expression below?
`${{Object}}`The answer is:
// -> '[object Object]'We defined an object with a property Object using Shorthand property notation:
{ Object: Object }Then we've passed this object to the template literal, so the toString method calls for that object. That's why we get string '[object Object]'.
Consider this example:
let x, { x: y = 1 } = { x }; y;The example above is a great task for an interview. What the value of y? The answer is:
// -> 1let x, { x: y = 1 } = { x }; y;
// ↑ ↑ ↑ ↑
// 1 3 2 4With the example above:
x with no value, so it's undefined.x into the object property x.x using destructuring and want to assign this value to the y. If the value is not defined, then we're gonna use 1 as the default value.y.Interesting examples could be composed with spreading of arrays. Consider this:
[...[...'...']].length // -> 3Why 3? When we use spread operator TODO(link to spec), the @@iterator method calls, and the returned iterator is used to obtain the values to be iterated. The default iterator for string spreads string by character. After spreading, we're packing this characters into an array. Then spreading this array again and packing back to the array.
A '...' string consists with three ., so the length of resulting array will be 3.
Now, step-by-step:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3Obviously, we can spread and wrap the elements of array as many times as we want:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// and so on …Not so many programmers know about labels in JavaScript. They are kind of interesting:
foo: {
console.log('first');
break foo;
console.log('second');
}
// > first
// -> undefinedThe labeled statement is used with break or continue statements. You can use a label to identify a loop, and then use the break or continue statements to indicate whether a program should interrupt the loop or continue its execution.
In the example above, we identify a label foo. Then console.log('first'); executes and then we interrupt execution.
Read more about labels in JavaScript:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5Like in the case with previous example follow these links:
try..catchWhat will this expression return? 2 or 3?
(() => {
try {
return 2;
} finally {
return 3;
}
})()The answer is 3. Surprised?
Take a look at the example below:
new (class F extends (String, Array) { }) // -> F []Is this a multiple inheritance? Nope.
The interesting part is the value of the extends clause ((String, Array)). The grouping operator always returns its last argument, so the (String, Array) is actually just Array. That means we've just created a class which extends Array.
Consider this example with a generator which yields itself:
(function* f() { yield f })().next()
// -> { value: [GeneratorFunction: f], done: false }As you see, the returned value is an object with value equal f. In that case, we can do something like this:
(function* f() { yield f })().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// and again
(function* f() { yield f })().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// and again
(function* f() { yield f })().next().value().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// and so on
// …To understand why this works that way, read these sections of the specification:
Consider this obfuscated syntax playing:
(typeof (new (class { class () {} }))) // -> 'object'It seems like we're declaring a class inside of class. Should be and error, however, we get an 'object' string.
Since ECMAScript 5 era, keywords are allowed as property names. So think about it as about this simple object example:
const foo = {
class: function() {}
};And ES6 standardized shorthand method definitions. Also, classes might be anonymous. So if we drop : function part, we're going to get:
class {
class() {}
}The result of a default class is always a simple object. And its typeof should return 'object'.
Read more here:
With well-known symbols, there's a way to get rid of type coertion. Take a look:
function nonCoercible(val) {
if (val == null) {
throw TypeError('nonCoercible should not be called with null or undefined')
}
const res = Object(val)
res[Symbol.toPrimitive] = () => {
throw TypeError('Trying to coerce non-coercible object')
}
return res
}Now we can use this like this:
// objects
const foo = nonCoercible({foo: 'foo'})
foo * 10 // -> TypeError: Trying to coerce non-coercible object
foo + 'evil' // -> TypeError: Trying to coerce non-coercible object
// strings
const bar = nonCoercible('bar')
bar + '1' // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1 // -> bar1
bar === 'bar' // -> false
bar.toString() === 'bar' // -> true
bar == 'bar' // -> TypeError: Trying to coerce non-coercible object
// numbers
const baz = nonCoercible(1)
baz == 1 // -> TypeError: Trying to coerce non-coercible object
baz === 1 // -> false
baz.valueOf() === 1 // -> true