Skip to content

Instantly share code, notes, and snippets.

@DmitrySoshnikov
Last active March 10, 2024 06:08
Show Gist options
  • Save DmitrySoshnikov/3928607cb8fdba42e712 to your computer and use it in GitHub Desktop.
Save DmitrySoshnikov/3928607cb8fdba42e712 to your computer and use it in GitHub Desktop.
@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

@WebReflection
Copy link

I think you failed more tests than you think, or maybe the test changed its results since this morning (there were less Errors IIRC). Anyway, one thing I'd like to underline, the this as well as arguments with arrow functions are problematic and could fail once transpiled.

More from Kyle on the arrow matter in here: http://blog.getify.com/arrow-this/ (there's NO lexical this)

[edit] just to double check, the 12th was the third this morning, now you are saying it's an Error. Which one is true?

@getify
Copy link

getify commented Nov 4, 2015

@WebReflection -- several of the questions have been fixed since the quiz first came out. :)

@fkling
Copy link

fkling commented Nov 4, 2015

Not sure the explanation for #12 is correct. The spec says:

  1. Let oldEnv be the running execution context’s LexicalEnvironment.
  2. If TDZnames is not an empty List, then
    1. Assert: TDZnames has no duplicate entries.
    2. Let TDZ be NewDeclarativeEnvironment(oldEnv).
    3. For each string name in TDZnames, do
      1. Let status be TDZ.CreateMutableBinding(name, false).
      2. Assert: status is never an abrupt completion.
    4. Set the running execution context’s LexicalEnvironment to TDZ.
  3. Let exprRef be the result of evaluating the production that is expr.

So, the expression ([{ x: 1 }, 2, { y }]) seems to be evaluated in an environment where x and y are defined, but they haven't been initialized yet (TDZ) and that's why it errors (not because y doesn't exist but because it is uninitialized).

@DmitrySoshnikov
Copy link
Author

@fkling, yes, absolutely correct. Though, it's still confusing 😄 Thanks, I edited.

@DmitrySoshnikov
Copy link
Author

@WebReflection,

one thing I'd like to underline, the this as well as arguments with arrow functions are problematic and could fail once transpiled

Do you have examples in mind? Seems transpiling them as lexical variable makes perfect sense?

(function () {
  return (y) => console.log(this.x + arguments[0] + y);
}).call({x: 10}, 20)(30); // 60

Which in transpiled code can look like (probably the closest to the semantics):

(function () {
  var _this = this,
      _arguments = arguments;

  return function (y) {
    return console.log(_this.x + _arguments[0] + y);
  };
}).call({x: 10}, 20)(30); // 60

(there's NO lexical this)

There is. ES6 moved this from the execution context to the environment record, and exactly in order to preserve it lexically, when an arrow function is created and captures that environment.

Of course, any other function would capture the same environment with lexical this sitting there, but on practice it's observable only with arrow functions because any other function sets a dynamic this (in a newly created, activation environment) at calling, as always was. So when ResolveThisBinding is called, it of course finds this directly in the first, activation environment.

In contrast, arrow functions do not receive direct [[thisValue]] in their activation environment, and it is resolved in the scope chain, and is found in the first environment that defined it.

That's said, in ES5 environment never contained this, it was on execution context, and in ES6 it's always there except for arrows.

So there definitely is lexical this: technically it's always lexical, since is captured by a function in the environment, and practically is observed only for arrows (that directly correspond to their [[ThisMode]] which is "lexical").

@getify
Copy link

getify commented Nov 5, 2015

there's NO lexical this

...means there's no lexical this in the arrow function itself, in which case it's looked up lexically in the scope chain; exactly as you've described. so, nothing to worry about here. @WebReflection was only pointing out how most people don't understand that, and referencing my blog post which tries to clear it up.

@WebReflection
Copy link

@getify I see, and now that I've read about the TDZ makes also sense

@DmitrySoshnikov I meant what @getify said and I've probably abused the word "transpiled" ... most explanations about arrow functions are superficially comparing it against .bind(context) but there's much more than that. Babel transpiles it right but AFAIK traceour had a bug. Thinking arrow just as bind with a single argument is also wrong and thinking about any this or any arguments mechanism within fat arrow makes basically no sense.

Thanks for the other edit about the 12th, I guess I've got that wrong too then last morning.

Cheers

@vandy
Copy link

vandy commented Nov 10, 2015

(function(x, f = () => x) {
  var x;
  var y = x;
  x = 2;
  return [x, y, f()];
})(1)
// >> Array [ 2, 1, 2 ]

Firefox 42.0

@bergus
Copy link

bergus commented Nov 25, 2015

#4 was a bit too simple for me. I pondered a bit about whether we're in strict mode (it's not even specified), before I realised that the value of this doesn't even matter for the result.

But #9 caught me off-guard, I completely missed the syntax errors (() in class name, comma in class body, : "property" with initialiser). So now, can anyone explain what the result for

typeof (new class f { [f]() { } f() { } })[`${f}`]

would be?

@oldergod
Copy link

frist ⇒ first
have a an object

thanks a lot for the explanation.

@a-savka
Copy link

a-savka commented Apr 25, 2016

(function(x, f = () => x) {
var x;
var y = x;
x = 2;
return [x, y, f()];
})(1)
// >> Array [ 2, 1, 2 ]

The same result with babel-node 6.7.5. As it stated in "Exploring ES6" book, var does nothing if parameter with same name exists. And looks like it is working that way. after var x line, x still have binding to parameter, not to a new variable inside function body scope. So assignment x = 2 will change parameter itself because there is no x variable in function body scope which shadows parameter.

@eburillo
Copy link

eburillo commented Dec 8, 2016

#3:

Not sure if i understood you but i think you're wrong.

let x, { x: y = 1 } = { x }; y;

First it creates a variable 'x' without any value so it's set to 'undefined';
After it, an object is defined with one property 'x' that takes the value of 'y' which is automatically created global with value '1'.
Next, this object is assigned to an object declared like this {x}. This 'x' is taking the value of the first 'let x' so that is the same as write '{x: undefined}', but this is totally irrelevant for the final result.
Then we print the global 'y' variable defined in the first object declaration.

@kirilloid
Copy link

kirilloid commented Nov 3, 2017

@eburillo, no that's not so.
{ x: y = 1 } is a syntax with destructuring assignment and default value.
let { x: y = 1 } = {};let y = 1;
let { x: y = 1 } = { x: 2 };let y = 2;

It might be helpful to see what the code is transpiled into by the babel: https://babeljs.io/repl/
BUT babel does not correctly implement all crazy corner-cases. For example, the first question.

@arash-hacker
Copy link

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

@sabit990928
Copy link

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