POSTS
JavaScript: When a Property Is Not a Property
Explaining an often-overlooked “aspect” of the programming languageWhen you create an object in JavaScript, it appears to come with a bunch of properties you didn’t define:
// Ostensibly, this object is empty...
let myObject = {};
// ...so what are these properties doing here?
console.log(typeof myObject.constructor); // "function"
console.log(typeof myObject.toString); // "function"
console.log(typeof myObject.valueOf); // "function"
This is thanks to a feature of the language called “prototypal inheritance.” It’s not the most intuitive behavior, but this isn’t a post about prototypal inheritance (maybe you’ve noticed there are already a few articles about that on the Internet). Instead, I’d like to explain the following strange-but-true statement:
An object’s “prototype” property is not the object’s prototype.1
Or, in JavaScript:
myObject.prototype !== Object.getPrototypeOf(myObject)
I can see you eyeing the scroll bar. Yes, this is a lengthy essay. Even if we
limit the scope of our investigation to those short statements, we still need
to do a little work to get an answer. If you’re not feeling up to it, then
here’s the short version: every object’s prototype is tracked internally, and
the property named prototype
only matters for constructor functions.
If that feels unsatisfying, then read on! Getting into the weeds on this one won’t just prepare you for one of those academic tech interview questions; it will reinforce some foundational concepts in JavaScript. It all hinges JavaScript’s imaginative use of the word “property.”
Misleading jargon
In English, we use the term “property” to describe any aspect of a thing. Take a kitchen refrigerator for example. Color is a property of the refrigerator. So is height. So is its manufacturer. Really, there’s nothing you can say about it which can’t be called a “property.”
At first glance, the word “property” appears to mean the same thing in
JavaScript. Here, we’ll create an object named refrigerator
with properties
for color, height, and manufacturer:
let refrigerator = {
color: 'beige',
height: 123,
manufacturer: 'GE'
};
One thing that makes JavaScript confusing, though, is that some aspects of the
refrigerator
object aren’t “properties” in this technical sense. The object’s
“extensibility” is such an aspect. Extensible objects are the ones to which we
can add new properties; non-extensible objects refuse new properties. We can
change the refrigerator
object from extensible to non-extensible using the
built-in method named
Object.preventExtensions
:
refrigerator.age = '5 years';
Object.preventExtensions(refrigerator);
refrigerator.education = 'B.Sc. of Civil Engineering';
console.log(refrigerator);
// {
// color: 'beige',
// height: 123,
// manufacturer: 'GE',
// age: '5 years'
// };
Although we tried to create a property named “age” and a property named
“education”, the object only gained an “age.” That’s not because most electric
iceboxes lack the academic rigor to earn a four-year degree; it’s because we
locked the thing down just before we tried to add the second property. The code
above proves that Object.preventExtensions
changes the state of an object,
but it doesn’t explain how JavaScript keeps track of that change. The
refrigerator
object didn’t receive a new property like (for example)
extensible: false
. Something definitely changed, but we can’t see it.
In other words: there’s more to a JavaScript object than just its properties!
Internals
So what’s going on? Behind the scenes, JavaScript objects have a set of
“internal slots” that help describe their state. They’re called “internal”
because us developers can’t observe them directly (e.g. we can’t print them to
the screen with code like console.log(myObject.internalSlots)
), but they
still influence each object’s behavior.
One of these slots ought to be familiar by now, even though we’ve never seen in
in code: “[[Extensible]]”. Deep in the underbelly of JavaScript, beneath our
workaday world of variables and loops and functions,
Object.preventExtensions
is setting the value of that slot to false
, and
the change causes our subsequent property definitions to fail.
While we can’t write myObject.Extensible
or even myObject["[[Extensible]]"]
to verify this, JavaScript comes with a built-in method that lets us peek under
the covers:
Object.isExtensible
.
let myObject = {};
console.log(Object.isExtensible(myObject)); // true
Object.preventExtensions(myObject);
console.log(Object.isExtensible(myObject)); // false
To recap: an “internal slot” is an aspect of an object, but it isn’t a property of the object…and that is confusing.
If we revisit the original example (because who doesn’t enjoy a lovingly-stretched metaphor?), an object’s properties are less like the traits describing the refrigerator and more like the things you put inside. Tossing some food in there would be like writing the code:
refrigerator.grapes = 19;
refrigerator.juice = '64 oz.';
With this version of the mental model, hopefully it will be easier to see why
the JavaScript statement refrigerator.Extensible = false
doesn’t change the
extensibility of the object. It would be like throwing a padlock inside your
fridge and expecting that to seal the door shut.
The [[Prototype]] internal slot
Awkward though they may seem, we need to understand internal slots to explain the strange statement from the beginning of this post:
An object’s “prototype” property is not the object’s prototype.1
Just like an object’s extensibility, the object’s prototype is defined by an
internal slot. This one’s named “[[Prototype]]”, and JavaScript includes a few
built-in methods to let us peep at this one, too:
Object.getPrototypeOf
and
Object.setPrototypeOf
2:
let myObject = {};
console.log(Object.getPrototypeOf(myObject) === Object.prototype); // true
The prototype of myObject
is the Object prototype because it’s an Object
instance, so we won’t find Array methods on it:
console.log(typeof myObject.forEach); // "undefined"
If we forcibly change the prototype, though…
Object.setPrototypeOf(myObject, Array.prototype);
…then myObject
appears to be inheriting from the Array prototype instead:
console.log(Object.getPrototypeOf(myObject) === Object.prototype); // false
console.log(Object.getPrototypeOf(myObject) === Array.prototype); // true
And sure enough, we can now find the Array methods:
console.log(typeof myObject.forEach); // "function"
But through it all, myObject
never gained a property that describes its
prototype. Remember, JavaScript tracks that with an internal slot!
console.log(myObject);
// { }
It was just slang, all along
When someone says something like, “that object’s prototype,” they’re actually using a shortcut. If you want to be really precise, you would instead say, “that object’s [[Prototype]] internal slot.” Pedantry won’t make you many friends (I ought to know), but it sometimes helps avoid confusion. Take that original statement, for example:
An object’s “prototype” property is not the object’s prototype.1
It looks somewhat less absurd if we use more descriptive language:
An object’s “prototype” property is not the object’s [[Prototype]] internal slot.1
Whether you prefer to think in terms of internal slots or kitchen appliances, the takeaway here is that a JavaScript object’s properties don’t tell you everything there is to know about that object. Keep that in mind for the next time your code’s not doing what you expect!
Thanks to Marsha Tiisa for inspiring and reviewing this post!
-
There’s an exception (of course there is!): the built-in Function object. The built-in Function object is a constructor function. It inherits from the Function prototype (meaning its [[Prototype]] is the Function prototype), and it constructs Function instances (meaning its
prototype
property is the Function prototype). ↩︎ ↩︎ ↩︎ ↩︎ -
While
Object.getPrototypeOf
andObject.setPrototypeOf
can be helpful when exploring the inner-workings of the language, they’re rarely used in production code. That’s because they can subvert folks’ assumptions and consequently make behavior harder for them to predict. If you find yourself reaching for those methods in some project or another, maybe pause and think if there’s another way to complete the task at hand. ↩︎