BFE.dev solution for JavaScript Quiz
6. Arrow Function

TL;DR

Arrow function doesn't have its own this, so this inside of arrow functions is pointed to the this at outer scope.

const obj = {
  dev: 'bfe',
  a: function () {
    return this.dev
  }
}
console.log(obj.a())
// `'bfe'`, `this` points to `obj`

const obj = {
  dev: 'bfe',
  b() {
    return this.dev
  }
}
console.log(obj.b())
// `'bfe'`, shorthand of above

const obj = {
  dev: 'bfe',
  c: () => {
    return this.dev
  }
}

console.log(obj.c())
// undefined, arrow function doesn't have its own `this`
// `this` is pointed to outer scope which is global so undefined is logged

const obj = {
  dev: 'bfe',
  d: function () {
    return (() => {
      return this.dev
    })()
  }
}
console.log(obj.d())
// `'bfe'`
// arrow function doesn't have its own `this`
//  `this` here points to its outer scope, which is the function.
// when obj.d() is called, `this` points to obj,
// so arrow function inside has `this` points to `obj`.

const obj = {
  dev: 'bfe',
  b() {
    return this.dev
  },
  e: function () {
    return this.b()
  }
}
console.log(obj.e())
// `'bfe'`
// when `obje.e()` is called, its `this` is obj
// so for b, its `this` is also obj

const obj = {
  dev: 'bfe',
  b() {
    return this.dev
  },
  f: function () {
    return this.b
  }
}
console.log(obj.f()())
// undefined
// think about obj.f()() as someExpression().
// obj.f() returns function b(), but there is no dot in someExpression,
// so `this` is global and undefined is logged

const obj = {
  dev: 'bfe',
  c: () => {
    return this.dev
  },
  g: function () {
    return this.c()
  }
}
console.log(obj.g())
// undefined
// in g(), `this` is obj
// but for c(), it is arrow function, it doesn't have its own `this`
// so it goes up to global and  undefined is logged

const obj = {
  dev: 'bfe',
  c: () => {
    return this.dev
  },
  h: function () {
    return this.c
  }
}
console.log(obj.h()())
// undefined
// obj.h() returns the arrow function, now the call expression is someExpression(),
// in which there is no dot , also c() is arrow function,
// `this` points to global, so undefined is logged

const obj = {
  dev: 'bfe',
  i: function () {
    return () => {
      return this.dev
    }
  }
}

console.log(obj.i()())
// 'bfe'
// obj.i() returns the anonymous arrow function
// arrow function itself doesn't have `this`,
// but its outer scope, the anonymous function has `this` pointed to obj.
// so the result is 'bfe'

Below is detailed explanation, hang on

CallMemberExpression

Let's first look at how below piece of code is evaluated.

obj.a()

This expression has two parts. 'obj.a' and '()'.

'obj.a' is MemberExpression and '()' is Arguments, as a whole it is CallMemberExpression.

And here is the internal logic from ECMAScript Spec.

  1. Let expr be the CallMemberExpression that is covered by CoverCallExpressionAndAsyncArrowHead.
  2. Let memberExpr be the MemberExpression of expr.
  3. Let arguments be the Arguments of expr.
  4. Let ref be ? Evaluation of memberExpr.
  5. Let func be ? GetValue(ref).
  6. If ref is a Reference Record, IsPropertyReference(ref) is false, and ref.[[ReferencedName]] is "eval", then
    1. If SameValue(func, %eval%) is true, then 1
      1. Let argList be ? ArgumentListEvaluation of arguments.
      2. If argList has no elements, return undefined.
      3. Let evalArg be the first element of argList.
      4. If the source text matched by this CallExpression is strict mode code, let strictCaller be true. Otherwise let strictCaller be false.
      5. Return ? PerformEval(evalArg, strictCaller, true).
  7. Let thisCall be this CallExpression.
  8. Let tailCall be IsInTailPosition(thisCall).
  9. Return ? EvaluateCall(func, ref, arguments, tailCall).

We can see that what it does is

  1. extract each parts, the MemberExpression and Arguments
  2. evaluate and get value of MemberExpression
  3. get arguments from Arguments
  4. forward it to EvaluateCall

How Property accessor is evaluated?

Let's first take a look at how obj.a is evaluated.

From ECMAScript spec, property accessor MemberExpression . IdentifierName is evaluated as blow.

  1. Let baseReference be ? Evaluation of MemberExpression.
  2. Let baseValue be ? GetValue(baseReference).
  3. If the source text matched by this MemberExpression is strict mode code, let strict be true; else let strict be false.
  4. Return EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict).

It forwads to EvaluatePropertyAccessWithIdentifierKey().

  1. Let propertyNameReference be ? Evaluation of expression.
  2. Let propertyNameValue be ? GetValue(propertyNameReference).
  3. Let propertyKey be ? ToPropertyKey(propertyNameValue).
  4. Return the Reference Record { [[Base]]: baseValue, [[ReferencedName]]: propertyKey, [[Strict]]: strict, [[ThisValue]]: empty }.

So obj.a in this question acutually returns a Reference Record, which is something like

{
  [[Base]]: obj,
  [[ReferencedName]]: 'a',
  [[Strict]]: false,
  [[ThisValue]]: empty
}

Now let's look at EvaluateCall

EvaluateCall

According to the ECMAScript spec.

  1. If ref is a Reference Record, then
    1. If IsPropertyReference(ref) is true, then
      1. Let thisValue be GetThisValue(ref).
    2. Else,
      1. Let refEnv be ref.[[Base]].
      2. Assert: refEnv is an Environment Record.
      3. Let thisValue be refEnv.WithBaseObject().
  2. Else,
    1. Let thisValue be undefined.
  3. Let argList be ? ArgumentListEvaluation of arguments.
  4. If func is not an Object, throw a TypeError exception.
  5. If IsCallable(func) is false, throw a TypeError exception.
  6. If tailPosition is true, perform PrepareForTailCall().
  7. Return ? Call(func, thisValue, argList).

We can clearly see in the first if branch, that the [[Base]] of Reference Record is used as thisValue through GetThisValue, which is obj in our case.

How this is set for function call?

Now let's jump into the () - function call part.

From here, we see F.[[Call]](V, argumentsList) is used in above Call(_func_, _thisValue_, argList), in which V means this.

And inside of [[[Call]] ( thisArgument, argumentsList )](https://tc39.es/ecma262/#sec-ecmascript-function-objects-call-thisargument-argumentslist), there is a step which bind this to local scope.

OrdinaryCallBindThis ( F, calleeContext, thisArgument )

  1. Let thisMode be F.[[ThisMode]].
  2. If thisMode is lexical, return unused.
  3. Let calleeRealm be F.[[Realm]].
  4. Let localEnv be the LexicalEnvironment of calleeContext.
  5. If thisMode is strict, let thisValue be thisArgument.
  6. Else,
    1. If thisArgument is undefined or null, then
      1. Let globalEnv be calleeRealm.[[GlobalEnv]].
      2. Assert: globalEnv is a Global Environment Record.
      3. Let thisValue be globalEnv.[[GlobalThisValue]].
    2. Else,
      1. Let thisValue be ! ToObject(thisArgument).
      2. NOTE: ToObject produces wrapper objects using calleeRealm.
  7. Assert: localEnv is a Function Environment Record.
  8. Assert: The next step never returns an abrupt completion because localEnv.[[ThisBindingStatus]] is not initialized.
  9. Perform ! localEnv.BindThisValue(thisValue).
  10. Return unused.

In our case thisArgument is obj so local scope's [[ThisValue]] is set to obj.

Now it's time to look at function body

return this.dev

How this is evaluated ?

From ECMAScript spec, it is evaluated by ResolveThisBinding.

  1. Let envRec be GetThisEnvironment().
  2. Return ? envRec.GetThisBinding().

GetThisEnvironment() is very important, because we can see the recursive checks along the parent scopes.

GetThisEnvironment ()

  1. Let env be the running execution context's LexicalEnvironment.
  2. Repeat,
    1. Let exists be env.HasThisBinding().
    2. If exists is true, return env.
    3. Let outer be env.[[OuterEnv]].
    4. Assert: outer is not null.
    5. Set env to outer.

In step 2, we can see there is a loop that keeps checking the existence of this in [[OuterEnv]] which is what we commonly call outer scope .

So here is the full picture of the first case.

const obj = {
  dev: 'bfe',
  a: function () {
    return this.dev
  }
}
console.log(obj.a())

// `obj.a` returns a reference record of {[[base]]: obj, [[ReferencedName]]: 'a'}
// () calls the function, it evaluates above reference record, gets `obj` as `this`,
// and set it as `[[ThisValue]]` of local scope
// `this` in function body tries recursively search for `[[ThisValue]]` along the lexical environment chain
// and instantly gets `obj`, `'bfe'` is logged.

Method definition

In second case it is called method definition, semantically different from property with a function.

const obj = {
  dev: 'bfe',
  b() {
    return this.dev
  }
}

We can find the core part in DefineMethodProperty

  1. Assert: homeObject is an ordinary, extensible object with no non-configurable properties.
  2. If key is a Private Name, then
    1. Return PrivateElement { [[Key]]: key, [[Kind]]: method, [[Value]]: closure }.
  3. Else,
    1. Let desc be the PropertyDescriptor { [[Value]]: closure, [[Writable]]: true, [[Enumerable]]: enumerable, [[Configurable]]: true }.
    2. Perform ! DefinePropertyOrThrow(homeObject, key, desc).
    3. Return unused.

And in DefinePropertyOrThrow, internal method [[DefineOwnProperty]] on object is triggered.

Methods allows us to add get or set keyword in the front it has more capability that setting a function as normal property.

But in this question, it is the same as normal function property, so 'bfe' is logged following above analysis.

const obj = {
  e: function () {
    return this.b()
  }
}
console.log(obj.e())

For above case, this.b is the same as obj.b() so 'bfe' is logged.

Function object internally has [[ThisMode]] to indicate how to handle this.

According to ECMAScript spec, It has 3 values lexical, strict or global.

lexical means that this refers to the this value of a lexically enclosing function. strict means that the this value is used exactly as provided by an invocation of the function. global means that a this value of undefined or null is interpreted as a reference to the global object, and any other this value is first passed to ToObject.

Guess you have noticed it, for arrow functions, this field should be lexical meaning this inside of arrow functions refers to this from outer scope.

Arrow functions are created with [[ThisMode]] set to lexical and this arg won't be set

From ECMAScript spec, we can clearly see that lexical-this is passed to OrdinaryFunctionCreate().

  1. If name is not present, set name to "".
  2. Let env be the LexicalEnvironment of the running execution context.
  3. Let privateEnv be the running execution context's PrivateEnvironment.
  4. Let sourceText be the source text matched by ArrowFunction.
  5. Let closure be OrdinaryFunctionCreate(%Function.prototype%, sourceText, ArrowParameters, ConciseBody, lexical-this, env, privateEnv).
  6. Perform SetFunctionName(closure, name).
  7. Return closure.

And inside of OrdinaryFunctionCreate, [[ThisMode]] is set to lexical at step 9.

When obj.b() is called, this arg is tried to set but is rejected by flag - [[ThisMode]] in OrdinaryCallBindThis

OrdinaryCallBindThis ( F, calleeContext, thisArgument )

  1. Let thisMode be F.[[ThisMode]].
  2. If thisMode is lexical, return unused. (omitted)

this in arrow function goes to outer scope (lexical environment)

Going back to what we covered in ResolveThisBinding, since its own scope doesn't have this get set, its outer scopes are checked to find this.

Now the the cases with arrow functions are clear to us.

const obj = {
  dev: 'bfe',
  c: () => {
    return this.dev
  }
  d: function() {
    return (() => {
      return this.dev
    })()
  },
  g: function() {
    return this.c()
  },
  h: function() {
    return this.c
  },
  i: function() {
    return () => {
      return this.dev
    }
  }
}

console.log(obj.c())
// undefined, since c itself has no `this` and it goes up to global scope which results in undefined

console.log(obj.d())

// 'bfe'
// the outer function has `this` set to `obj`. Though arrow function doesn't have `this`, it tries to get `this` though outer scope.

console.log(obj.g())
// undefined
// same as `obj.c()`

console.log(obj.h()())
// undefined
// same as above

console.log(obj.i()())
// 'bfe'
// same as above analysis

this breaks if expression is not connected as property access.

Last case of f is a bit tricky

const obj = {
  dev: 'bfe',
  b() {
    return this.dev
  },
  f: function () {
    return this.b
  }
}
console.log(obj.f()())

How we handle expression like obj.f()()? In Call Expression, it defines such case recursively.

CallExpression[Yield, Await] :
    CallExpression[?Yield, ?Await] Arguments[?Yield, ?Await]

In the evaluation process, we can see that the first CallExpression is first evaluated.

  1. Let ref be ? Evaluation of CallExpression.
  2. Let func be ? GetValue(ref).
  3. Let thisCall be this CallExpression.
  4. Let tailCall be IsInTailPosition(thisCall).
  5. Return ? EvaluateCall(func, ref, Arguments, tailCall).

Notice that in step 2, GeValue() is used to get the function out, so ref is no longer a reference record which we cover at the beginning of this long explanation

And in EvaluateCall, because ref is not reference record, thisValue is set to undefined. Again global scope is used as this in the body, so undefined is logged.

You might also be able to find a solution fromcommunity posts or fromAI solution.