Foreword
Going through MDN Docs, I found 2 interesting points about object property inheritance in JS.
The article on Object.defineProperty() points out that
Unlike accessor properties, data properties are always set on the object itself, not on a prototype. However, if a non-writable data property is inherited, it is still prevented from being modified on the object.
To my understanding, it means that any accessor property or non-writable data property of an object cannot be overridden by it’s child object unless such a property is defined on the child object explicitly using Object.defineProperty() static method.
Also, there’s this peculiar aspect of super which sets a property on the current this
object as stated as follows:
Setting properties of
super
, such assuper.x = 1
, behaves likeReflect.set(Object.getPrototypeOf(objectLiteral), "x", 1, this)
. This is one of the cases where understanding super as simply "reference of the prototype object" falls short, because it actually sets the property onthis
instead.
But using super
also doesn’t allow overriding of accessor or non-writable data property as highlighted below:
However,
super.x = 1
still consults the property descriptor of the prototype object, which means you cannot rewrite non-writable properties, and setters will be invoked.
The Question
Consider the following piece of code:
class A {
get ap() { return this.x;}
set ap(arg) { this.x = arg; }
}
Object.defineProperty(A.prototype, 'dp', {writable: true, value: 'writable'});
Object.defineProperty(A.prototype, 'nwdp', {writable: false, value: 'locked'});
class B extends A {
method() {
super.dp = 200; // Override it
super.ap = 'bird'; // Doesn't override it; Invokes setter, ap at A.prototype
super.nwdp = 'unlocked'; // Doesn't override it; Throws TypeError
}
}
b = new B()
When called b.method()
, it sets dp
and x
on the instance, b
(checked using Object.getOwnPropertyNames(b)
) but will throw TypeError
.
Uncaught TypeError: Cannot assign to read only property 'nwdp' of object '#B'
Similarly, when super.prop = <any_value>
is used in an object literal, it functions the same way but it doesn’t throw any error.
let obj1 = {
dp: 'writable',
get ap() { return this.x; },
set ap(arg) { this.x = arg; }
};
Object.defineProperty(obj1, 'nwdp', {writable: false, value: 'locked'});
let obj2 = {
method() {
super.dp = 200; // Overrides it
super.ap = 'bird'; // Doesn't override it; Invokes setter, ap at obj1
// Doesn't override it; But doesn't throw any kind of error as if it doesn't do anything
super.nwdp = 'unlocked';
}
};
Object.setPrototypeOf(obj2, obj1);
obj2.method();
Using Object.getOwnPropertyNames(obj2)
shows properties dp
, x
and method
on the object, obj2
.
So, the question actually is why does JS behave like this?
In both the scenarios, it behaves exactly the same except when it is about overriding a non-writable data property on the child object using super.prop = <any_value>
, it throws TypeError
for properties meant to be defined for a class instance whereas it seems like it ignores the same statement in case of object literals.
Is this intentional? If so, what’s the reason behind it? Is it documented anywhere?
2
Answers
All parts of
class
bodies are "strict" mode code, and that’s when you get that Type Error. Your second example is not "strict". You can make it so if you want, and then you’ll get the error.The distinction is not between
class
methods and object literal methods but rather between strict mode and sloppy mode. That assignments to non-writable properties are silently ignored is a legacy behaviour, it should not happen in modern code that does"use strict"
mode.Of course, everything in ES6
class
definitions is strict by default, whereas normal top-level code is sloppy by default for compatibility reasons. You should use ES6 modules, or always begin your scripts with the"use strict";
directive.