Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
@kangax's ES6 quiz, explained

@kangax's ES6 quiz, explained

@kangax created a new interesting quiz, this time devoted to ES6 (aka ES2015). I found this quiz very interesting and quite hard (made myself 3 mistakes on first pass).

Here we go with the explanations:

Question 1:
(function(x, f = () => x) {
  var x;
  var y = x;
  x = 2;
  return [x, y, f()];
})(1)
  • [2, 1, 1]
  • [2, undefined, 1]
  • [2, 1, 2]
  • [2, undefined, 2]

The most complex question for me in this quiz. I didn't get it right initially until read the spec and clarified with @kangax. First I answered [2, undefined, 1], which is "almost correct", except one subtle thing. The correct answer here is the first one, [2, 1, 1], and let's see why.

As we know, parameters create extra scope in case of using default values.

Parameter f is always the function (the default value, since it's not passed), and it captures x exactly from the parameters scope, that is 1.

Local variable x shadows the parameter with the same name, var x;. It's hoisted, and is assigned default value... undefined? Yes, usually it would be assigned value undefined, but not in this case, and this is the subtle thing we mentioned. If there is a parameter with the same name, then the local binding is initialized not with undefined, but with the value (including default) of that parameter, that is 1.

So the variable y gets the value 1 as well, var y = x;.

Next assignment to local variable x happens, x = 2, and it gets value 2.

By the time of the return, we have x is 2, y is 1, and f() is also 1. It's also a tricky part: since f was created in the scope of parameters, its x refers to the parameter x, which is still 1.

And the final return value is: [2, 1, 1].


Question 2:
(function() {
  return [
    (() => this.x).bind({ x: 'inner' })(),
    (() => this.x)()
  ]
}).call({ x: 'outer' });
  • ['inner', 'outer']
  • ['outer', 'outer']
  • [undefined, undefined]
  • Error

Arrow functions have lexical this value. This means, they inherit this value from the context they are defined. And later it stays unchangeable, even if explicitly bound or called in a different context.

In this case both arrow functions are created within the context of {x: 'outer'}, and .bind({ x: 'inner' }) applied on the first function doesn't make difference.

So the answer is: ['outer', 'outer'].


Question 3:
let x, { x: y = 1 } = { x }; y;
  • undefined
  • 1
  • { x: 1 }
  • Error

Variable y will eventually have value 1 since:

First, let x defines x with the value undefined.

Then, destructuring assignment { x: y = 1 } = { x } on the right hand side has a short notation for an object literal: the {x} is equivalent to {x: x}, that is an object {x: undefined}.

Once it's destructured the pattern { x: y = 1 }, we extract variable y, that corresponds to the property x. However, since property x is undefined, the default value 1 is assigned to it.

So the answer is: 1.


Question 4:
(function() {
  let f = this ? class g { } : class h { };
  return [
    typeof f,
    typeof h
  ];
})();
  • ["function", "undefined"]
  • ["function", "function"]
  • ["undefined", "undefined"]
  • Error

This IIFE is executed with no explicit this value. In ES6 it means it will be undefined (the same as in strict mode in ES5).

So the variable f is bound to the class h {}. Its typeof is a "function", since classes in ES6 is a syntactic sugar on top of the constructor functions.

However, the class h {} itself is created in the expression position, that means its name h is not added to the environment. And testing the typeof h should return "undefined".

And the answer is: ["function", "undefined"].


Question 5:
(typeof (new (class { class () {} })))
  • "function"
  • "object"
  • "undefined"
  • Error

This is an obfuscated syntax playing, but let's try to figure it out :)

First of all, since ES5 era, keywords are allowed as property names. So on a simple object example, it can look like:

let foo = {
  class: function() {}
};

And ES6 standardized concise method definitions, that allows dropping the : function part, so we get the:

let foo = {
  class() {}
};

This is exactly what corresponds to the inner class () {} -- it's a method inside a class.

The class itself is anonymous, so we can rewrite the example:

let c = class {
  class() {}
};

new c();

Now, instead of assigning to the varialbe c, we can instantiate it directly:

new class {
  class() {}
};

The result of a default class is always a simple object. And its typeof should return "object":

typeof (new class {
  class() {}
});

And the answer is: "object".


Quetion 6:
typeof (new (class F extends (String, Array) { })).substring
  • "function"
  • "object"
  • "undefined"
  • Error

Here we have a similar obfuscated example (but we already figured out this inlined typeof, new, and class thing above ;)), though the interesting part is the value of the extends clause. It's the: (String, Array).

The grouping operator always returns its last argument, so the (String, Array) is actually just Array.

So what we've got here is:

class F extends Array {}

let f = new F();

typeof f.substring; // "undefined"

Since array instances do not have substring method, and our extended class F didn't provide it either, the answer is "undefined".


Question 7:
[...[...'...']].length
  • 1
  • 3
  • 6
  • Error

Here we deal with the spread operator. It allows to spread all the elements to the array. It can work with any iterable object.

Strings are iterable, meaning that we can iterate over their chars (in this case char by char). So the inner [...'...'] results to an array: ['.', '.', '.']:

let s = '...';

let a = [...s];

console.log(a); // ['.', '.', '.']

Array are iterable as well. So the outer spread is applied on our new array:

let result = [...a];

console.log(result); // ['.', '.', '.']
console.log(result.length); // 3

As we can see spreading the array happens element by element, so the resulting array just copied all the elements, and looks the same -- with just 3 string dots.

And the answer is: 3.


Question 8:
typeof (function* f() { yield f })().next().next()
  • "function"
  • "generator"
  • "object"
  • Error

In this example we encounter a generator function. When executed, they return a generator object:

let g = (function* f() { yield f })();

Generator objects have next method, that returns the next value at the yield position. The returned value has iterator protocol format:

{value: <returned value>, done: boolean};

So on first next() we get:

g.next(); // {value: f, done: false}

As we see, the returned value itself doesn't have method next(), so trying to call it as a chain would result to an error:

g.next().next(); // error

Notice though, that we could normally call it as:

g.next(); // {value: f, done: true}
g.next(); // {value: undefined, done: true}

So the answer is: Error.


Question 9:
typeof (new class f() { [f]() { }, f: { } })[`${f}`]
  • "function"
  • "undefined"
  • "object"
  • Error

The obfuscated example results to a Syntax Error since class name f() is not correct.

The answer is Error.


Question 10:
typeof `${{Object}}`.prototype
  • "function"
  • "undefined"
  • "object"
  • Error

This one is very tricky :)

First, we deal with template strings.

They are capable to render values of variables directly in the strings:

let x = 10;

console.log(`X is ${x}`); // "X is 10"

However, in the example we have something that looks a bit strange: it's not ${Object} how it "should be", but the ${{Object}}.

No, it's not another special syntax of template strings, it's still a value inside ${}, and the value is {Object}.

What is {Object}? Well, as we mentioned earlier above, ES6 has short notation for object literals, so in fact it's just the: {Object: Object} -- a simple object with the property named "Object", and the value Object (the built-in Object constructor).

Now it's becoming more clear:

let x = {Object: Object};
let s = `${x}`;

console.log(s); // "[object Object]"

See what's happened? The ${x} is roughly equivalent to the:

'' + x;

// or the same:

x.toString(); // "[object Object]"

Now, the string "[object Object]" obviously doesn't have property prototype:

"[object Object]".prototype; // undefined

typeof "[object Object]".prototype; // "undefined"

So the answer is: "undefined".


Question 11:
((...x, xs)=>x)(1,2,3)
  • 1
  • 3
  • [1,2,3]
  • Error

This one is the simplest. Rest parameters can appear only at the last postion. In this case ...x goes as a first argument of an IIFE arrow function, so results to a Parse Error.

And the answer is: Error.


Question 12:
let arr = [ ];
for (let { x = 2, y } of [{ x: 1 }, 2, { y }]) {
  arr.push(x, y);
}
arr;u
  • [2, { x: 1 }, 2, 2, 2, { y }]
  • [{ x: 1 }, 2, { y }]
  • [1, undefined, 2, undefined, 2, undefined]
  • Error

Several topics combined here: destructuring assignment, default values, and for-of loop.

However, we can quickly identify it's an error, because of two one thing:

EDIT 1: @fkling42 pointed out that the variable y is in the environment, but is not initialized yet (being under TDZ -- Temportal Dead Zone), and that's the reason why it cannot be accessed

EDIT 2: @getify pointed out, that value 2 actually normally passes RequireObjectCoercible check, and hence there would be no error in destructuring let { x = 2, y } = 2;.

  • { y } is a short notation of {y: y} and will fail, since variable y doesn't exist in the scope; The variable y is in the scope, but is under TDZ, so cannot be accessed
  • (we wouldn't reach this, because of the frist error, but): trying to destructure 2 will fail too will not fail, since to object coercion will be normally applied.

So the answer is: Error.


Question 13:
(function() {
  if (false) {
    let f = { g() => 1 };
  }
  return typeof f;
})();
  • "function"
  • "undefined"
  • "object"
  • Error

This example is only on attention, since it's a syntax error: the arrow function => cannot be defined in this way, since we have a an object with the g (consice) method.

And the answer is: Error.


Conclusion

I like such tricky quiz questions, it's always fun to track the runtime semantics and parsing process manually. Of course, most of the things here are far from practical production code, and are interesting mostly from the theoretical viewpoint. Still I found it enjoyable.

I'll be glad to discuss all the questions in the comments.

Good luck with ES6 ;)

Written by: Dmitry Soshnikov

http://dmitrysoshnikov.com

@arash-hacker
Copy link

arash-hacker commented Aug 20, 2018

Q#13 is unreachbale code so can't be run(es7 with Quokka vscode).
and Q#12 has extra u keyword?

@sabit990928
Copy link

sabit990928 commented Dec 24, 2021

Typo here. Frist -> first

(we wouldn't reach this, because of the frist error, but): trying to destructure 2 will fail too will not fail, since to object coercion will be normally applied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment