Objects, Prototypes & Classes
Roughly 15% of a typical loop — and 15% of the mock exam here.
this is decided by the call, not the definition
For an ordinary function, this is the receiver at the call site. obj.fn() sets it to obj; the same function extracted and called bare gets undefined, because class bodies and modules are always strict. That is the whole story behind the most common runtime error in interview code:
const fn = user.getName;
fn(); // TypeError: Cannot read properties of undefined
Fix it with user.getName.bind(user) or an arrow wrapper. Arrow functions have no this of their own — they read it lexically, which makes them wrong as methods and right as callbacks inside one. They also have no arguments, no super and no new.target.
When several rules compete, the precedence is: new, then bind, then the call-site receiver, then the default binding — and an arrow ignores all four. Calling a bound function with new discards the bound receiver but keeps the bound arguments.
The prototype chain
Every object has an internal [[Prototype]], read properly with Object.getPrototypeOf. Property lookup walks that chain until it hits null. Only functions have a .prototype property, and it is the object that new Foo() will install as the instance’s parent — the two names are not the same thing.
new Foo() creates an object linked to Foo.prototype, runs the body with this bound to it, and returns it unless the body returns another object, which overrides the instance. Returning a primitive is ignored.
Object.create(null) produces an object with no chain at all: no toString, no hasOwnProperty, and no way for a user-supplied key to collide with something inherited. It is the standard dictionary object, and one reason to prefer Object.hasOwn(obj, key) over the method form.
Object.setPrototypeOf on a hot path is a known deoptimization — it invalidates inline caches and hidden-class assumptions. Build the object with the right prototype instead.
Classes are prototypes with better syntax
typeof MyClass is 'function', and methods live on the prototype, shared by every instance. Classes do add real semantics: they always run in strict mode, they cannot be called without new, and in a derived constructor this is uninitialized until super() runs — reading it first throws ReferenceError.
A class field holding an arrow function is created per instance and captures this, which is why it survives being passed to addEventListener where a prototype method would not. The cost is one closure per object.
#private fields are enforced by the engine: access from outside the class body is a SyntaxError, and calling a method that reads #x on a foreign object throws a TypeError — a brand check. #x in obj was added to test membership without throwing, and it is the sturdiest type guard for a class.
super.method() resolves from the [[HomeObject]] of the method where it is written, not from the runtime receiver. That fixed link is why mixins copied with Object.assign onto a prototype lose super, and why the class-factory form — const M = (Base) => class extends Base {} — is the better mixin.
Copying, descriptors and comparison
Spread and Object.assign are both shallow. Spread defines new own properties on a fresh object and skips the prototype, so spreading a class instance loses its methods; Object.assign mutates the target and triggers any setters it already defines. Object.freeze is shallow too: obj.nested.x = 1 still works, and a write to a frozen property fails silently in sloppy mode but throws in strict mode.
Object.defineProperty defaults every descriptor flag to false, so a property defined that way is non-writable, non-enumerable and non-configurable — invisible to Object.keys and JSON.stringify unless you say otherwise. Assigning through an inherited accessor calls its setter; assigning over an inherited data property just creates an own property that shadows it.
Sample questions
6 of the 30 questions this domain carries in practice mode — expand one to check yourself before drilling.
1. What determines the value of this inside an ordinary function?
- how the function is called — the receiver of the call, not where the function was written
- the object the function is defined inside, fixed permanently when the function is created
- the nearest enclosing class, or the global object when the function is not in a class
- the first argument passed to the function, which the engine reserves for the receiver
Answer: A. obj.fn() sets this to obj; the same function called bare gets undefined in strict mode. Arrow functions are the exception — they have no this of their own.
2. Reading obj.toString when obj never defines it works because:
- the engine falls back to a built-in table of default methods for any missing property name
- toString is injected into every object literal at creation time as an own property
- toString is a global function that any expression may call without qualification
- the lookup walks the prototype chain and finds it on Object.prototype
Answer: D. Property lookup walks the chain until it hits null. Object.create(null) makes an object with no chain, so obj.toString is simply undefined.
3. An object literal defines greet: () => `hi ${this.name}`. Calling obj.greet() reads this from:
- obj, because the arrow function is a property of obj and inherits it as its receiver
- the scope enclosing the literal — module scope or the outer function, never obj itself
- undefined always, since arrow functions throw when their body mentions this at all
- globalThis in every case, because object literals do not create a lexical scope of their own
Answer: B. An arrow captures this lexically at definition, so it is the wrong choice for a method — but exactly right for a callback inside one.
4. const fn = user.getName; fn() throws "Cannot read properties of undefined". The standard fixes are:
- user.getName.bind(user), or wrapping the call in an arrow such as () => user.getName()
- declaring getName as a static method so it no longer depends on any receiver at all
- calling fn.call() with no arguments, which restores the receiver the method was defined on
- converting getName into a getter, since getters keep their receiver when they are extracted
Answer: A. Extracting a method drops the receiver. Class bodies are strict, so this is undefined rather than the global object — hence the TypeError.
5. When several binding rules apply at once, which order does the engine use for this?
- the receiver at the call site wins, then bind, then new, with lexical this considered last
- new, then bind, then the call-site receiver, then the default binding — and an arrow ignores all of them
- lexical this from an arrow function, then bind, then the call-site receiver, then new
- the default binding is checked first and the others only apply when the function is a method
Answer: B. new on a bound function ignores the bound receiver but keeps the bound arguments — the reason bind is described as partial application.
6. A prototype defines a setter for name. Assigning child.name = 'x' on an instance does what?
- calls the inherited setter with the instance as its receiver, creating no own property
- creates an own data property on the instance, shadowing the accessor from then on
- throws a TypeError, since an inherited accessor may not be assigned through an instance
- copies the accessor pair onto the instance and then invokes the copied setter
Answer: A. Inherited accessors intercept assignment; inherited data properties do not — those get shadowed by a new own property instead.
Drill this domain in practice mode →
Independent community study resource — not affiliated with or endorsed by Oracle, Microsoft or Ecma International. JavaScript is a trademark of Oracle Corporation; TypeScript is a trademark of Microsoft Corporation. All questions and study notes are original, written from MDN, the ECMAScript specification and the TypeScript handbook. Everything runs in your browser; nothing you answer is stored or transmitted.