Does Javascript writable descriptor prevent changes on instances?
Asked Answered
S

3

4

Answers (please read them below, their respective authors provided valuable insights):

  • "writable: false" prevents assigning a new value, but Object.defineProperty is not an assignement operation and therefore ignores the value of "writable"
  • property attributes are inherited, therefore a property will remain non writable on every subclasses/instances until one subclass (or instance of subclass) changes the value of "writable" back to true for itself

Question:

MDN documentation concerning the property "writable" descriptor states:

writable true if and only if the value associated with the property may be changed with an assignment operator. Defaults to false.

The official ECMA-262 6th edition more or less states the same. The meaning is clear but, to my understanding, it was limited to the original property (i.e. the property on that specific object)

However, please consider the following example (JSFiddle):

//works as expected, overloading complete       
var Parent = function() {};
Object.defineProperty(Parent.prototype, "answer", {
    value: function() { return 42; }
});

var Child = function() {};
Child.prototype = Object.create(Parent.prototype, {
    answer: {
        value: function() { return 0; }
    }
});

var test1 = new Parent();
console.log(test1.answer()); //42
var test2 = new Child();
console.log(test2.answer()); //0

//does not work as expected
var Parent2 = function() {};
Object.defineProperty(Parent2.prototype, "answer", {
    value: function() { return 42; }
});

var Child2 = function() {};
Child2.prototype = Object.create(Parent2.prototype);

test3 = new Parent2();
console.log(test3.answer()); //42
test4 = new Child2();
test4.answer = function() { return 0; };
console.log(test4.answer()); //42

Following this example, we see that, although the property is not writable, it can be overloaded on the prototype of a subclass (test2), as I would expect.

However, when trying to overload the method on an instance of a subclass (test4), it fails silently. I would have expected it to work just as test2. The same happens when trying to overload the property on an instance of Parent.

The same thing occurs in both NodeJS and JSFiddle and, under some conditions, overloading on the instance throws a TypeError concerning the readonly nature of the property.

Could you please confirm to me that this is the expected behaviour ? If so, what is the explanation ?

Scaleboard answered 3/2, 2016 at 12:53 Comment(4)
You do realize you have a typo, right? anwserGaslit
Thank you. The example shown here is not my original code but an example made for this question for the sake of clarity. With the typo corrected, I am now realising it does not demonstrate correctly the issue at hand. I update the post accordinglyScaleboard
I've wondered this for a long time, you gave me the push I needed to do the research.Gaslit
@JuanMendes same here! @Scaleboard in your answers, the first answer is incorrectly stated. defineProperties on child.prototype is possible because child.prototype has no descriptor for answer. that descriptor is defined in child.prototype.constructor.prototype. So it doesn't so much as ignore it, it just doesn't exist yet :-) child.prototype.constructor.prototype equals parent.prototypeParish
W
3

Yes, this is expected behaviour.

it fails silently.

Not exactly. Or: Only in sloppy mode. If you "use strict" mode, you'll get an

Error { message: "Invalid assignment in strict mode", … }

on the line test4.answer = function() { return 0; };

it can be overloaded on the prototype of a subclass (test2), but not an instance of a subclass (test4)

This has nothing to do with instances vs. prototypes. What you didn't notice is that you're using different ways to create the overloading property:

  • an assignment to a property that is inherited and non-writable fails
  • an Object.defineProperty call just creates a new property, unless the object is non-extensible

You can do the same for your instance:

Object.defineProperty(test4, "answer", {
    value: function() { return 42; }
});
Wilmoth answered 3/2, 2016 at 15:1 Comment(5)
i've been going through this same question, and it seems that the defined properties for parent get copied over to child, including the writable flag. However, the configurable flag isn't being copied over, because i am able to redefine a property on a child, even though it's non-configurable on the parent. Any clue?Parish
@Parish "re-create" is not "re-define", even though both have the effect of overwriting.Wilmoth
@DoXicK: No, nothing is copied over actually - neither properties nor their attributes. When accessing a property, inheritance of the value comes into play, and when assigning a property, even attributes are inherited. But when you use Object.defineProperty, it just creates a new property (with default attributes) and completely ignores inheritance.Wilmoth
Thanks for all the comments. I was somehow amalgamating Object.defineProperty with an assignment operationScaleboard
thanks for the explanation @Bergi, combined with the link to your other answer this gave me a way better view.Parish
G
3

You cannot write to an instance property if its prototype defines that property as unwritable (and the object instance doesn't have a descriptor) because the set operation goes up to the parent (prototype) to check if it can write, even though it would write to the child (instance). See EcmaScript-262 Section 9.1.9 1.c.i

4. If ownDesc is undefined, then
  a. Let parent be O.[[GetPrototypeOf]]().
  b. ReturnIfAbrupt(parent).
  c. If parent is not null, then
      i. Return parent.[[Set]](P, V, Receiver).

However, if you are trying to get around that, you can set the descriptor of the instance itself.

var proto = Object.defineProperties({}, {
  foo: {
    value: "a",
    writable: false,  // read-only
    configurable: true  // explained later
  }
});

var instance = Object.create(proto);
Object.defineProperty(instance, "foo", {writable: true});
instance.foo // "b"
Gaslit answered 3/2, 2016 at 14:56 Comment(2)
Thank you, that explains everything. I can't say I agree with the principle of going up to the parent but, well, it is the way it is. I suppose it has its uses in other use cases I have yet to meet.Scaleboard
@Scaleboard I'm on the other side, I'm glad this is the behavior. I want to be able to define at the prototype that a property is not writable. This way, you still have the option to change the behavior.Gaslit
W
3

Yes, this is expected behaviour.

it fails silently.

Not exactly. Or: Only in sloppy mode. If you "use strict" mode, you'll get an

Error { message: "Invalid assignment in strict mode", … }

on the line test4.answer = function() { return 0; };

it can be overloaded on the prototype of a subclass (test2), but not an instance of a subclass (test4)

This has nothing to do with instances vs. prototypes. What you didn't notice is that you're using different ways to create the overloading property:

  • an assignment to a property that is inherited and non-writable fails
  • an Object.defineProperty call just creates a new property, unless the object is non-extensible

You can do the same for your instance:

Object.defineProperty(test4, "answer", {
    value: function() { return 42; }
});
Wilmoth answered 3/2, 2016 at 15:1 Comment(5)
i've been going through this same question, and it seems that the defined properties for parent get copied over to child, including the writable flag. However, the configurable flag isn't being copied over, because i am able to redefine a property on a child, even though it's non-configurable on the parent. Any clue?Parish
@Parish "re-create" is not "re-define", even though both have the effect of overwriting.Wilmoth
@DoXicK: No, nothing is copied over actually - neither properties nor their attributes. When accessing a property, inheritance of the value comes into play, and when assigning a property, even attributes are inherited. But when you use Object.defineProperty, it just creates a new property (with default attributes) and completely ignores inheritance.Wilmoth
Thanks for all the comments. I was somehow amalgamating Object.defineProperty with an assignment operationScaleboard
thanks for the explanation @Bergi, combined with the link to your other answer this gave me a way better view.Parish
P
0

I've taken your example code and structured all the possible ways to change the possible outcome: https://jsfiddle.net/s7wdmqdv/1/

var Parent = function() {};
Object.defineProperty(Parent.prototype,"type", {
    value: function() { return 'Parent'; }
});
var oParent = new Parent();
console.log('parent', oParent.type()); // Parent


var Child1 = function() {};
Child1.prototype = Object.create(Parent.prototype, {
    type: {
        value: function() { return 'Child1'; }
    }
});
var oChild1 = new Child1();
console.log('child1', oChild1.type()); // Child1


var Child2 = function() {};
Child2.prototype = Object.create(Parent.prototype);
Object.defineProperty(Child2.prototype, 'type', {
    value: function() { return 'Child2'; }
});
var oChild2 = new Child2();
console.log('child2', oChild2.type()); // Child2


var Child3 = function() {};
Child3.prototype = Object.create(Parent.prototype);
var oChild3 = new Child3();
oChild3.type = function() { return 'Child3'; };
console.log('child3', oChild3.type()); // Parent


var Child4 = function() {};
Child4.prototype = Object.create(Parent.prototype);
Child4.prototype.type = function() { return 'Child4'; };
var oChild4 = new Child4();
console.log('child4', oChild4.type()); // Parent


Object.defineProperty(Parent.prototype,"type", {
    value: function() { return 'Parent2'; }
});
var oParent2 = new Parent();
console.log('parent2',oParent2.type());

When you use Object.create(...) to clone the prototype, the original descriptors are still attached higher up the prototype chain.

When assigning something to child.answer = 10 it will use Child.prototype.answer.writable. If that doesn't exist it will try Child.prototype.constructor.prototype.answer.writable if it does.

However, if you try Object.defineProperty(Child.prototype, ...) it won't check the prototype chain. It will test if it is defined on Child.prototype. if it's not defined, it will define it then.

Parish answered 3/2, 2016 at 15:1 Comment(1)
Interesting findings. They may come in handy later. Thank you.Scaleboard

© 2022 - 2024 — McMap. All rights reserved.