What survives

Does obfuscation break prototype chains?

Delegation is a naming mechanism: a lookup that misses on one object continues to the next by the same name. That makes it the most reasonable place to expect a renaming pass to go wrong. Measured against the real engine in five configurations it does not, and the one place a rename does become visible is worth knowing precisely.

Why delegation looks like it should break

A prototype chain resolves a property by name at run time. Read child.greet, and the engine looks on the object, then on its prototype, then on the next link, until it finds that name or runs out of chain. Nothing binds the two ends together in advance.

So if a transform rewrote the definition on the prototype but not the read on the instance, the lookup would walk the whole chain and return undefined. Calling it would throw, and the message would point at the call site rather than at the definition that moved. That is a plausible failure and it is why the question comes up.

Measured, it does not happen, on either target and under renaming. The rename is applied consistently to both the definition and every dotted read of it, so the chain still resolves. Where a rename does become visible is reflection: the places where you name a property with a string rather than with syntax.

What was measured

The sample built the whole structure: Object.create with a prototype and a lookup that walks to it; own against inherited properties; Object.keys listing own enumerables only; shadowing an inherited property and confirming the prototype was not mutated; getPrototypeOf and setPrototypeOf; the __proto__ accessor; Object.create(null) having no inherited members at all; a constructor function whose prototype method is shared between instances; instanceof through two levels; a method added to a prototype after instances existed; a two-level chain calling the parent implementation explicitly; for...in against hasOwnProperty; Object.assign copying own enumerables only; a property descriptor; and a non-enumerable own property.

That file ran in Node, was protected, and ran again, across the default ES5 target, the modern target, a renaming build with identifier replacement and global renaming, the same on the modern target, and a string-table build with MoveStrings and EncodeStrings enabled.

All five matched the original. The delegated call still produced its value, shadowing still left the prototype's own value intact, an object created with a null prototype still had no inherited hasOwnProperty, two instances still shared one function object for a prototype method, a method added late was still visible to an instance built earlier, and a two-level chain calling the parent implementation still produced the combined result. instanceof was still true at both levels and the constructor property still pointed back to the right function.

Why it holds: the rename is applied to the whole name

Member renaming rewrites a property name in three places: an identifier key in an object literal, a string key in an object literal, and a dotted member access. Those three cover both ends of a delegated lookup. The definition on the prototype is an object-literal key or a dotted assignment; the read on the instance is a dotted access. Both are rewritten, and they are rewritten to the same replacement.

That is why the chain survives. The name changed, but it changed everywhere the transform can see it, so the lookup still matches at the same link. Adding a method to a prototype later, or calling a parent implementation explicitly, are both ordinary dotted forms and are covered by the same rule.

Everything else the chain depends on is not a name at all. The links between objects are references, and instanceof compares those references rather than any text, which is why it holds through renaming and on both targets.

Where a rename does show: naming a property with a string

The exception is reflection, and it is the same asymmetry that appears elsewhere in this family: syntax is rewritten, strings are not.

Object.prototype.hasOwnProperty.call(obj, "kind") names a property with a string argument. So does Object.getOwnPropertyDescriptor(proto, "kind"), and so does "kind" in obj. None of those is one of the three rename sites, so the string keeps saying kind while the property itself has been renamed. The check then answers about a property that no longer exists.

Measured, this is not subtle in its consequences. With a pattern that matched a property also queried by string, hasOwnProperty returned false for a property the object genuinely had, and getOwnPropertyDescriptor returned undefined, so the next line reading a field off that descriptor threw a TypeError. The exception surfaced at the descriptor read rather than at the rename, which is what makes it worth recognising by shape.

There is a second, milder version that does not throw. for...in enumerates the names the object actually has, so after a rename it yields the replacement name. Measured, that is exactly what happened: the loop reported the new name for the renamed property and the original names for the rest. Any code that compares an enumerated key against a hard-coded string needs the same attention as reflection by name.

The rule is one sentence: if a property is named by a string anywhere in your program, keep it out of the member pattern, or rename it in the string too. Where the two spellings must stay in step, an explicit mapping is easier to audit than a pattern.

The chain is still fully visible to a reader

One expectation worth correcting: protection does not hide your object model. It changes what the names look like, not whether the structure can be seen.

A reader with developer tools can expand any object and walk its prototype chain link by link, on a protected build exactly as on an unprotected one. The shape of the model, which objects delegate to which, how many levels there are, which methods live where, is visible from a single inspection, because that structure exists in memory at run time and is not a property of the source text.

What renaming removes is the vocabulary, and vocabulary is a real part of how quickly someone understands unfamiliar code. Losing meaningful names raises the effort of working out what a method does. It does not conceal that the method is there, and it does not stop anyone from calling it from a console.

So the same rule applies here as everywhere else on the client: this raises the cost of casual reading, and anything that must remain unknown belongs behind an API call rather than behind a naming scheme.

Scope, and how to check your own build

This article covers prototype-based delegation: Object.create, constructor functions and their prototypes, shadowing, instanceof, enumeration and reflection, measured on the default configuration and the four variants above. It does not cover class syntax, whose lowering on the ES5 target is a separate question with its own moving parts.

For your own build, the check that matters is not that the program runs. It is a search for the places where you name a property with a string: calls to hasOwnProperty, getOwnPropertyDescriptor, defineProperty, the in operator, and any comparison of an enumerated key against a literal. Those are the lines to cross-reference against your member pattern.

If member renaming is off, which is the default, none of this applies and the whole family is unaffected. If it is on, the exercise above is a few minutes with a search across the codebase, and it finds the silent cases as well as the ones that throw.

Frequently asked questions

Does obfuscation break prototype delegation?

No. Measured across five configurations, a lookup that resolves on a prototype still resolved after protection. Renaming rewrites both the definition and every dotted read to the same replacement name, so the chain still matches at the same link.

Does instanceof still work through a renamed prototype chain?

Yes. The links between objects are references and instanceof compares references, not text. Measured, instanceof was still true at both levels of a two-level chain, and the constructor property still pointed back to the right function.

Why does hasOwnProperty start returning false after member renaming?

Because it names the property with a string argument, and a string is not one of the three places renaming rewrites a name. The property was renamed, the string was not, so the check asks about a name the object no longer has. The same applies to the in operator.

Why would getOwnPropertyDescriptor throw a TypeError after protection?

It does not throw itself; it returns undefined because the string it was given no longer names an existing property, and the next line reading a field off that undefined descriptor throws. Measured, this is exactly what a renamed-but-reflected property produces.

Does for...in return the original property names after renaming?

No. It enumerates the names the object actually has, so a renamed property appears under its replacement name. Measured, the loop returned the new name for the renamed property and the originals for the rest, without throwing. Code comparing enumerated keys against literals needs the same care as reflection.

Does protection hide my object model from someone reading the page?

No. A reader can expand any object in developer tools and walk its prototype chain, because that structure exists in memory at run time. Renaming removes the vocabulary, which raises the effort of understanding the code; it does not conceal the structure or prevent calls from a console.

Related reading